/* ShelfOS design system — token-based styling (replaces Pico.css).
 *
 * A calm, technical look for an electronics inventory: cool slate neutrals,
 * a single teal accent, monospace for data (part numbers, quantities, units).
 * Light and dark are both first-class; components read the theme via tokens. */

:root {
  --bg: #f6f7f9;
  --surface: #ffffff;
  --surface-2: #f1f4f8;
  --border: #e4e9f0;
  --border-strong: #cdd6e2;
  --text: #101826;
  --muted: #64748b;
  --faint: #94a3b8;

  --accent: #0d9488;
  --accent-strong: #0f766e;
  --accent-soft: #d5f4ee;
  --accent-ink: #0b4f47;
  --on-accent: #ffffff;
  --row-hover: #eef6f4;
  /* The picked row in a chooser. Opaque on purpose: a translucent tint
     composites against whatever is behind the row, which is unpredictable
     inside a dialog. Clearly stronger than --row-hover, and still a
     background the ordinary --text reads against. */
  --row-picked: #c7ece3;

  --ok: #15803d;      --ok-soft: #dcf5e3;
  --warn: #b45309;    --warn-soft: #fbeccb;
  --danger: #b91c1c;  --danger-soft: #fbe0e0;
  --neutral: #475569; --neutral-soft: #e8edf3;
  /* The unfilled-field tint, and the one soft fill that must be OPAQUE — it lands
     on a <select>, whose dropdown list inherits it with nothing behind it to
     composite against. Here that is --danger-soft itself; see the dark block. */
  --unfilled-tint: #fbe0e0;

  --ring: rgba(13, 148, 136, .32);
  --shadow-sm: 0 1px 2px rgba(16, 24, 38, .06);
  --shadow-md: 0 6px 22px -8px rgba(16, 24, 38, .18);

  --radius: 12px;
  --radius-sm: 8px;
  --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, Consolas, monospace;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0a1120;
    --surface: #111b2e;
    --surface-2: #17233a;
    --border: #22314b;
    --border-strong: #334564;
    --text: #e7edf6;
    --muted: #9fb0c9;
    --faint: #6b7d99;

    --accent: #2dd4bf;
    --accent-strong: #5eead4;
    --accent-soft: rgba(45, 212, 191, .14);
    --accent-ink: #7ff0e2;
    --on-accent: #04231f;
    --row-hover: #182a40;
    --row-picked: #1e3a52;

    --ok: #4ade80;      --ok-soft: rgba(74, 222, 128, .14);
    --warn: #fbbf24;    --warn-soft: rgba(251, 191, 36, .14);
    --danger: #f87171;  --danger-soft: rgba(248, 113, 113, .14);
    --neutral: #a7b6cc; --neutral-soft: rgba(167, 182, 204, .14);
    /* What --danger-soft already LOOKS like on a dialog: the same rgba flattened
       onto --surface. Opaque because a dropdown list draws it over the popup's own
       light ground, where 14% of anything comes out near-white and the light --text
       on it is unreadable. */
    --unfilled-tint: #312737;

    --ring: rgba(45, 212, 191, .38);
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);
    --shadow-md: 0 10px 30px -10px rgba(0, 0, 0, .6);
  }
}

/* Explicit theme toggle wins over the OS preference in both directions. */
:root[data-theme="light"] {
  --bg: #f6f7f9; --surface: #ffffff; --surface-2: #f1f4f8;
  --border: #e4e9f0; --border-strong: #cdd6e2;
  --text: #101826; --muted: #64748b; --faint: #94a3b8;
  --accent: #0d9488; --accent-strong: #0f766e; --accent-soft: #d5f4ee; --accent-ink: #0b4f47; --on-accent: #ffffff; --row-hover: #eef6f4; --row-picked: #c7ece3;
  --ok: #15803d; --ok-soft: #dcf5e3; --warn: #b45309; --warn-soft: #fbeccb;
  --danger: #b91c1c; --danger-soft: #fbe0e0; --neutral: #475569; --neutral-soft: #e8edf3;
  --ring: rgba(13, 148, 136, .32);
  --shadow-sm: 0 1px 2px rgba(16, 24, 38, .06); --shadow-md: 0 6px 22px -8px rgba(16, 24, 38, .18);
}
:root[data-theme="dark"] {
  --bg: #0a1120; --surface: #111b2e; --surface-2: #17233a;
  --border: #22314b; --border-strong: #334564;
  --text: #e7edf6; --muted: #9fb0c9; --faint: #6b7d99;
  --accent: #2dd4bf; --accent-strong: #5eead4; --accent-soft: rgba(45, 212, 191, .14); --accent-ink: #7ff0e2; --on-accent: #04231f; --row-hover: #182a40; --row-picked: #1e3a52;
  --ok: #4ade80; --ok-soft: rgba(74, 222, 128, .14); --warn: #fbbf24; --warn-soft: rgba(251, 191, 36, .14);
  --danger: #f87171; --danger-soft: rgba(248, 113, 113, .14); --neutral: #a7b6cc; --neutral-soft: rgba(167, 182, 204, .14);
  --ring: rgba(45, 212, 191, .38);
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .4); --shadow-md: 0 10px 30px -10px rgba(0, 0, 0, .6);
}

/* ---------- base ---------- */
* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
h1, h2, h3 { letter-spacing: -.015em; text-wrap: balance; }
h1 { font-size: 26px; margin: 0; }
h2 { font-size: 18px; margin: 0; }
a { color: var(--accent-strong); text-underline-offset: 2px; }

/* Use the full viewport width with a small, even gutter — a wide component
   table needs the room so columns stop truncating (was capped at 1120px). */
.page { padding: 26px 25px 72px; }
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.muted { color: var(--muted); }

:where(button, a, input, select):focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--ring);
  border-radius: var(--radius-sm);
}

/* ---------- app bar ---------- */
.appbar {
  position: sticky; top: 0; z-index: 20;
  background: color-mix(in srgb, var(--surface) 82%, transparent);
  backdrop-filter: saturate(1.4) blur(8px);
  border-bottom: 1px solid var(--border);
}
.appbar-inner {
  padding: 12px 25px;
  display: flex; align-items: center; gap: 16px;
}
.brand { display: flex; align-items: center; gap: 10px; font-weight: 650; letter-spacing: -.01em; }
.brand a { color: inherit; text-decoration: none; display: inline-flex; align-items: center; gap: 10px; }
.brand .mark {
  width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center;
  background: linear-gradient(150deg, var(--accent), var(--accent-strong));
  color: var(--on-accent); box-shadow: var(--shadow-sm); flex: none;
}
.brand .mark svg { width: 16px; height: 16px; }
.appbar nav { margin-left: 8px; display: flex; gap: 4px; }
.appbar nav a {
  color: var(--muted); text-decoration: none; font-size: 14px; font-weight: 500;
  padding: 6px 10px; border-radius: 7px;
}
.appbar nav a:hover { color: var(--text); background: var(--surface-2); }
.appbar nav a[aria-current] { color: var(--accent-ink); background: var(--accent-soft); }
.appbar .spacer { flex: 1; }
.user { display: flex; align-items: center; gap: 10px; font-size: 13.5px; color: var(--muted); }
.user .who { display: flex; align-items: center; gap: 8px; }
.user form { margin: 0; }
.avatar {
  width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center;
  background: var(--neutral-soft); color: var(--text); font-size: 12px; font-weight: 650; flex: none;
}

/* ---------- page header ---------- */
.head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 20px; flex-wrap: wrap; margin: 4px 0 20px;
}
.head p { margin: 4px 0 0; color: var(--muted); font-size: 14.5px; }
.head-actions { display: flex; align-items: flex-end; gap: 10px; }

/* ---------- header stats strip (Components page) ---------- */
/* One rounded surface with hairline dividers rather than seven separate cards:
   in a header, boxed tiles read as seven competing buttons. Sits between the
   title and the page actions, and is the first thing allowed to shrink — below
   roughly 1100px it wraps onto its own line (`.head` already wraps). */
.head-stats {
  display: flex; align-items: stretch; justify-content: center;
  /* Never grows — a stretched pill would be mostly empty box. It wraps onto its
     own line inside `.head` when the header runs out of room, and only on a
     screen too narrow even for that does it scroll itself.
     `nowrap` is what keeps the dividers honest: they hang off the left of every
     tile but the first, and a wrapped second row would start with one dangling. */
  flex: 0 1 auto; flex-wrap: nowrap; max-width: 100%; overflow-x: auto;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-sm);
  padding: 8px 4px;
}
.stat {
  display: flex; flex: none; flex-direction: column; gap: 2px; justify-content: flex-start;
  padding: 2px 14px; border-left: 1px solid var(--border);
}
.stat:first-child { border-left: none; }
.stat-label {
  font-size: 11px; font-weight: 600; color: var(--muted);
  letter-spacing: .04em; text-transform: uppercase; white-space: nowrap;
}
.stat-value {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: 22px; line-height: 1.1; white-space: nowrap;
}
.stat-value.accent { color: var(--accent); }
/* Only when there IS something out of stock — an orange 0 would be an alarm
   about the absence of a problem. */
.stat-value.is-warn { color: var(--warn); }
/* Reserved even when empty, so every tile is the same height whatever the
   numbers do — see the skeleton comment in index.html. */
.stat-sub { font-size: 11px; color: var(--muted); min-height: 15px; line-height: 15px; }
.stat-top-mpn {
  max-width: 130px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--font-mono);
}
.s-smt { color: var(--accent); }
.s-tht { color: var(--neutral); }
.stat-sep { color: var(--faint); margin: 0 5px; }
/* The SMT share of SMT+THT; the track showing through is the THT remainder.
   Other mounting types are not in this tile at all, which is what the label says. */
.stat-bar {
  display: block; width: 88px; height: 4px; margin-top: 5px;
  border-radius: 999px; background: var(--neutral-soft); overflow: hidden;
}
.stat-bar i { display: block; height: 100%; border-radius: 999px; background: var(--accent); }

.crumb { margin: 0 0 14px; font-size: 14px; }
.crumb a { color: var(--muted); text-decoration: none; }
.crumb a:hover { color: var(--text); }

/* ---------- form controls ---------- */
.field { display: flex; flex-direction: column; gap: 5px; }
.field label { font-size: 12px; font-weight: 600; color: var(--muted); letter-spacing: .01em; }
.control {
  appearance: none; font: inherit; width: 100%; color: var(--text);
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); padding: 8px 12px; min-height: 38px;
  box-shadow: var(--shadow-sm); transition: border-color .15s, box-shadow .15s;
}
select.control {
  padding-right: 34px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 10px center;
}
.control:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--ring); }
.control::placeholder { color: var(--faint); }

/* ---------- buttons ---------- */
.btn {
  appearance: none; font: inherit; font-weight: 600; font-size: 14px; cursor: pointer;
  display: inline-flex; align-items: center; gap: 7px; justify-content: center;
  padding: 9px 15px; min-height: 38px; border-radius: var(--radius-sm);
  border: 1px solid transparent; text-decoration: none;
  transition: background .15s, border-color .15s, transform .05s, box-shadow .15s;
}
.btn:active { transform: translateY(1px); }
.btn svg { width: 16px; height: 16px; }
.btn-primary { background: var(--accent); color: var(--on-accent); box-shadow: var(--shadow-sm); }
.btn-primary:hover:not(:disabled) { background: var(--accent-strong); }
.btn-secondary { background: var(--surface); color: var(--text); border-color: var(--border-strong); box-shadow: var(--shadow-sm); }
.btn-secondary:hover:not(:disabled) { background: var(--surface-2); border-color: var(--faint); }
.btn-ghost { background: transparent; color: var(--muted); }
.btn-ghost:hover:not(:disabled) { background: var(--surface-2); color: var(--text); }
.btn-danger { background: transparent; color: var(--danger); border-color: color-mix(in srgb, var(--danger) 32%, transparent); }
.btn-danger:hover:not(:disabled) { background: var(--danger-soft); }
/* A disabled button has to LOOK disabled. `disabled` already blocks the click,
   but until now nothing said so on screen: pressing a live-looking button and
   having nothing happen reads as the page ignoring you, not as "not yet". The
   hover rules above are narrowed for the same reason — a button that lights up
   under the pointer is promising something it will not do. */
.btn:disabled { opacity: .5; cursor: not-allowed; box-shadow: none; }
.btn-sm { min-height: 30px; padding: 4px 11px; font-size: 12.5px; border-radius: 7px; }
.btn-block { width: 100%; }

/* ---------- cards ---------- */
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-sm); }
.card-pad { padding: 22px; }
.card h2 { margin: 0 0 14px; font-size: 15px; }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start; }
.stack { display: flex; flex-direction: column; gap: 20px; }

/* ---------- static tables (detail page) ---------- */
/* Scroll inside the frame (both axes) and cap the height, so a long/wide table
   keeps its horizontal scrollbar reachable and its header in view instead of
   forcing a page-long scroll to the very bottom. Short tables never hit the cap. */
.table-wrap { overflow: auto; max-height: 70vh; }
table.data { width: 100%; border-collapse: collapse; font-size: 14px; }
table.data thead th {
  text-align: left; font-size: 11.5px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted); font-weight: 650; background: var(--surface-2);
  padding: 10px 14px; border-bottom: 1px solid var(--border); white-space: nowrap;
  position: sticky; top: 0; z-index: 1; /* keep the header while scrolling */
}
table.data tbody td { padding: 11px 14px; border-bottom: 1px solid var(--border); }
table.data tbody tr:last-child td { border-bottom: none; }
table.data tbody tr:hover { background: var(--row-hover); }
table.data td.num { text-align: right; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
table.data td.line-actions { text-align: right; white-space: nowrap; }
table.data td.line-actions .btn { margin-left: 4px; }

/* key/value table (overview, parameters) */
table.kv { width: 100%; border-collapse: collapse; font-size: 14px; }
table.kv th {
  text-align: left; font-weight: 600; color: var(--muted); white-space: nowrap;
  padding: 9px 16px 9px 0; width: 42%; vertical-align: top;
}
table.kv td { padding: 9px 0; border-bottom: 1px solid var(--border); }
table.kv th { border-bottom: 1px solid var(--border); }
table.kv tr:last-child th, table.kv tr:last-child td { border-bottom: none; }

/* A labelled field too long for table.kv's 42/58 split: the label sits above the
   value so the value gets the card's full measure. pre-wrap keeps the line breaks
   of a description someone typed by hand. */
.kv-long { margin-top: 14px; font-size: 14px; }
.kv-long > span {
  display: block; font-weight: 600; color: var(--muted); margin-bottom: 4px;
}
.kv-long p { margin: 0; white-space: pre-wrap; }

.empty { color: var(--muted); font-size: 14px; margin: 0; }

/* ---------- location tree (§7) ---------- */
.loc-tree { list-style: none; margin: 0; padding: 0; }
.loc-tree li { padding: 5px 0; }
.loc-tree .loc-tree { margin: 4px 0 0 10px; padding-left: 14px; border-left: 1px solid var(--border); }
.loc-tree .loc-name { font-weight: 550; margin-right: 8px; }
/* The hover wash spans the WHOLE row — name on the left, actions on the right —
   so the revealed Edit/Delete buttons visibly belong to one highlighted strip
   rather than floating next to whichever row the eye happens to land on. */
.loc-row { display: flex; align-items: center; gap: 8px; padding: 2px 6px; border-radius: 7px; }
.loc-row:hover, .loc-row:focus-within { background: var(--surface-2); }
/* Same caret as the picker's, and deliberately the same shape: one visual idea
   for "expand this branch" across the app. */
.tree-caret, .tree-caret-spacer { width: 20px; height: 24px; flex: none; }
.tree-caret {
  border: none; background: transparent; color: var(--muted);
  cursor: pointer; border-radius: 5px; padding: 0; font: inherit;
}
.tree-caret::before { content: "▸"; }
.tree-caret[aria-expanded="true"]::before { content: "▾"; }
.tree-caret:hover { background: var(--surface-2); color: var(--text); }
.tree-children { margin-top: 2px; }
/* A location kept on screen only as the path to a match below it. */
.loc-item.is-path > .loc-row { opacity: .6; }
/* Per-node actions, revealed on hover or keyboard focus so the tree stays calm.
   opacity (not visibility) keeps the buttons tabbable while hidden. */
.loc-actions { margin-left: auto; display: inline-flex; gap: 6px; opacity: 0; transition: opacity .12s; }
.loc-row:hover .loc-actions, .loc-actions:focus-within { opacity: 1; }
/* What a location holds. Indented like a child branch so contents and sub-
   locations read as one nesting, and laid out as rows of small facts. */
.loc-parts { list-style: none; margin: 4px 0 0 10px; padding-left: 14px;
  border-left: 1px solid var(--border); }
.loc-parts li { display: flex; align-items: center; gap: 8px; padding: 3px 0; }
.loc-parts .muted { font-size: 13px; }

/* One level row in the Generate-hierarchy dialog: type, count, name pattern.
   Rows are staggered by depth (JS sets margin-left) with an ↳ marker, so the
   dialog reads like the tree it is about to create. */
.bulk-level { display: flex; gap: 8px; margin-bottom: 8px; align-items: center; }
.bulk-depth { color: var(--muted); flex: none; }
.bulk-level [name="level-count"] { width: 76px; flex: none; }
.bulk-level [name="level-type"] { width: 130px; flex: none; }
.bulk-hint { font-size: 13px; margin: 0 0 8px; }
.bulk-preview-list { list-style: none; margin: 8px 0 0; padding: 8px 10px;
  border: 1px solid var(--border); border-radius: 8px; font-size: 13px; }
.bulk-preview-list li { padding: 2px 0; }

/* ---------- deleted-component banner (§20) ---------- */
/* Above the page rather than beside a field: every table below it is history,
   and a reader arriving from an old link must know that before reading them. */
.notice {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
  padding: 12px 14px; margin-bottom: 16px;
  border: 1px solid var(--border); border-radius: 10px; background: var(--surface-2);
  font-size: 14px;
}
.notice p { margin: 4px 0 0; font-size: 13.5px; }
.notice-danger {
  border-color: color-mix(in srgb, var(--danger) 32%, transparent);
  background: var(--danger-soft);
}
.notice-danger strong { color: var(--danger); }
/* The button and any refusal it earned, stacked so the message stays put. */
.notice-actions { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
.notice-actions .error { text-align: right; max-width: 34ch; }

/* ---------- delete-component dialog (§20) ---------- */
/* Consequences, listed. Bulleted on purpose — every other list in the app is a
   set of controls, this one is a set of losses and reads as prose. */
.delete-lead { margin: 0; }
.consequences { margin: 6px 0 0; padding-left: 20px; font-size: 14px; }
.consequences li { padding: 2px 0; }

/* A checkbox with its label, for filter bars. */
.check {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 14px; color: var(--text); cursor: pointer; white-space: nowrap;
}

/* ---------- location tree-picker (§7) ---------- */
.loc-picker-toggle { display: flex; align-items: center; text-align: left; cursor: pointer; }
.loc-picker-toggle::after { content: "▾"; margin-left: auto; padding-left: 8px; color: var(--muted); }
.loc-picker-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* An in-flow panel with its own scroll: the tree scrolls inside the widget, so
   a deep hierarchy never spills over and forces the whole dialog to scroll. */
.loc-picker-menu {
  margin-top: 5px; padding: 6px;
  max-height: min(300px, 45vh); overflow-y: auto;
  background: var(--surface-2); border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
}
.loc-picker-list { list-style: none; margin: 0; padding: 0; }
.loc-picker-children { padding-left: 16px; }
.loc-picker-row { display: flex; align-items: center; gap: 2px; }
.loc-picker-caret, .loc-picker-caret-spacer { width: 20px; height: 26px; flex: none; }
.loc-picker-caret {
  border: none; background: transparent; color: var(--muted);
  cursor: pointer; border-radius: 5px; padding: 0;
}
.loc-picker-caret::before { content: "▸"; }
.loc-picker-caret[aria-expanded="true"]::before { content: "▾"; }
.loc-picker-caret:hover { background: var(--surface-2); color: var(--text); }
.loc-picker-node {
  flex: 1; display: flex; align-items: center; gap: 8px; min-width: 0;
  text-align: left; border: none; background: transparent; color: var(--text);
  cursor: pointer; padding: 5px 8px; border-radius: 6px; font: inherit;
}
.loc-picker-node:hover { background: var(--row-hover); }
.loc-picker-type { color: var(--muted); font-size: 12px; }
.loc-picker-none { color: var(--muted); }
.loc-picker-empty { color: var(--muted); font-size: 13px; margin: 4px 8px; }
.loc-picker-new {
  display: block; width: 100%; text-align: left; font: inherit;
  color: var(--accent); background: transparent; cursor: pointer;
  border: none; padding: 5px 8px; border-radius: 6px;
  border-bottom: 1px solid var(--border); margin-bottom: 4px;
}
.loc-picker-new:hover { background: var(--row-hover); }

/* A node kept only as the path to a match below it: readable, but plainly not a
   target. `disabled` already blocks the click; this makes that visible. */
.loc-picker-node.is-unavailable {
  color: var(--muted); opacity: .55; cursor: not-allowed;
}
.loc-picker-node.is-unavailable:hover { background: transparent; }
.loc-picker-toggle:disabled { color: var(--muted); cursor: progress; }
/* The escape hatch from a mode filter, and the message when it filters to nothing. */
.loc-picker-showall {
  display: flex; align-items: center; gap: 6px; cursor: pointer;
  color: var(--muted); font-size: 13px; padding: 4px 8px;
  border-bottom: 1px solid var(--border); margin-bottom: 4px;
}
.loc-picker-nomatch { color: var(--muted); font-size: 13px; margin: 4px 8px; }

/* A heading that separates two tables on one page (the admin match-rules page).
   Sized between the page title and a card's own heading, since it is neither. */
.section-head { font-size: 17px; margin: 26px 0 8px; }
.section-head:first-of-type { margin-top: 0; }
.section-note { margin: 0 0 12px; font-size: 13.5px; max-width: 70ch; }

/* ---------- badges ---------- */
.badge {
  display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 600;
  padding: 3px 9px; border-radius: 999px; line-height: 1.4; white-space: nowrap;
}
.badge .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
.b-accent { background: var(--accent-soft); color: var(--accent-ink); }
.b-ok { background: var(--ok-soft); color: var(--ok); }
.b-warn { background: var(--warn-soft); color: var(--warn); }
.b-danger { background: var(--danger-soft); color: var(--danger); }
.b-neutral { background: var(--neutral-soft); color: var(--neutral); }

/* ---------- dialog ---------- */
dialog {
  border: 1px solid var(--border); border-radius: var(--radius); padding: 0;
  background: var(--surface); color: var(--text); box-shadow: var(--shadow-md);
  width: min(440px, calc(100vw - 32px));
}
dialog::backdrop { background: rgba(10, 17, 32, .45); backdrop-filter: blur(2px); }
dialog > article { margin: 0; }
dialog header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 16px 20px; border-bottom: 1px solid var(--border);
}
dialog header strong { font-size: 16px; }
dialog form { display: flex; flex-direction: column; gap: 14px; padding: 18px 20px; margin: 0; }
/* A dialog that carries no <form> still needs the inset a form would have given
   it: `dialog` itself is padding: 0, and every dialog until now took its
   breathing room from the rule above. Same numbers, so the two kinds of dialog
   cannot drift apart. */
.dialog-body {
  display: flex; flex-direction: column; gap: 14px; padding: 18px 20px;
}
dialog footer {
  display: flex; justify-content: flex-end; gap: 10px;
  padding: 4px 0 0; border: none; margin: 4px 0 0;
}
/* Pushes a leading footer action (e.g. "Open in shop") to the left while the
   primary actions stay right. */
dialog footer .footer-spacer { flex: 1; }
dialog .close {
  background: transparent; border: none; color: var(--muted); cursor: pointer;
  width: 28px; height: 28px; border-radius: 6px; display: grid; place-items: center; padding: 0;
}
dialog .close:hover { background: var(--surface-2); color: var(--text); }
dialog .close::before { content: "\00d7"; font-size: 20px; line-height: 1; }

.error { color: var(--danger); font-size: 13.5px; margin: 0; }
/* A failure and the button that undoes it, on one line. The message yields so a
   long server error cannot push Retry out of reach — the one control the row
   exists to offer. */
.error-row {
  display: flex; align-items: center; gap: 10px; margin: 0;
}
.error-row .error { flex: 1; min-width: 0; }
.error-row .btn { flex: none; }
/* Feedback under the stock dialog's "scan a location" field. */
#stock-loc-scan-msg { font-size: 13px; margin: 6px 0 0; }
/* A partial success — something landed, but not everything asked for. Red would
   overstate it; .muted would hide it. */
.warn { color: var(--warn); font-size: 13.5px; margin: 0; }

/* Secondary line under a table cell (e.g. an imported part's description). */
.small { font-size: 12px; }

/* ---------- "you may already have this part" ---------- */
/* A warning, not an error: what it lists may or may not be the same part, and
   carrying on is a legitimate answer. */
.mfr-conflict {
  gap: 8px; padding: 10px 12px; border-radius: var(--radius-sm);
  background: var(--warn-soft); border: 1px solid var(--warn);
}
.mfr-conflict p { margin: 0; }
.mfr-conflict-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.mfr-conflict-list li {
  display: flex; align-items: center; gap: 8px; font-size: 13.5px;
}
/* The name yields, the button never does. Without this a long manufacturer pushes
   "This is it" outside the dialog, where it takes a horizontal scroll to reach —
   the one control the whole warning exists to offer. */
.mfr-conflict-name {
  flex: 1; min-width: 0; font-weight: 650;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.mfr-conflict-list .btn { flex: none; }
/* The "this one also teaches a spelling" mark. Beside the name it applies to, not
   under the list: it is true of one row's button and routinely false of another's. */
.mfr-conflict-teaches {
  flex: none; font-size: 12px; color: var(--warn); white-space: nowrap;
}

/* The two invoice line tables share a grid (the line_columns macro supplies the
   colgroup). table-layout: fixed is what makes the browser honour those widths
   instead of re-measuring from content — without it the columns still size
   themselves and the two panels drift apart again. Fixed layout also stops a long
   part number from widening a column, so the Part cell wraps instead. */
.lines-grid { table-layout: fixed; min-width: 1100px; }
/* A floor, because the Part column is the one that absorbs every loss: without it
   a narrow window shrinks it to a few pixels and the cell shows nothing at all.
   Below the floor .table-wrap scrolls instead — the page body still never does. */
.lines-grid td { overflow-wrap: anywhere; }

/* "You may already have this part", on an invoice review row. Same question the
   New Component dialog asks, so it borrows that block's list shape — the name
   yields, the button never does. */
.badge.linkish {
  border: 0; cursor: pointer; font: inherit; font-size: 11.5px;
  margin-left: 6px; vertical-align: middle;
}
#invoice-review tr.existing-row > td {
  background: var(--warn-soft); box-shadow: inset 3px 0 0 var(--warn);
}
.existing-lead { margin: 0 0 8px; }
.existing-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.existing-list li { display: flex; align-items: center; gap: 8px; font-size: 13.5px; }
.existing-name {
  flex: 1; min-width: 0; font-weight: 650;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.existing-list .btn { flex: none; }

/* An imported-review row still missing a type or a location — a soft accent bar
   so it reads as "needs a touch" without shouting error red. */
#invoice-review tr.is-incomplete > td:first-child { box-shadow: inset 3px 0 0 var(--warn); }

/* ---------- dialogs with a location tree-picker (§7) ---------- */
/* Wider so the tree has room; cap the height and let the form scroll only as a
   fallback on a short viewport — the picker menu scrolls itself. */
#stock-dialog, #invoice-line-dialog {
  width: min(560px, calc(100vw - 32px)); max-height: calc(100vh - 48px);
}
#stock-dialog > article, #invoice-line-dialog > article {
  display: flex; flex-direction: column; max-height: calc(100vh - 48px);
}
#stock-dialog article > header, #invoice-line-dialog article > header { flex: none; }
#stock-dialog form, #invoice-line-dialog form { overflow-y: auto; }

/* ---------- new-component dialog (§16.5) ---------- */
/* A single-column form that can grow with a type's parameter list, so cap the
   height and let the form scroll while the header stays put. */
#component-dialog { width: min(520px, calc(100vw - 32px)); max-height: calc(100vh - 48px); }
#component-dialog > article { display: flex; flex-direction: column; max-height: calc(100vh - 48px); }
#component-dialog article > header { flex: none; }
#component-dialog form { overflow-y: auto; }
.shop-import { border-bottom: 1px solid var(--border); padding-bottom: 14px; margin-bottom: 4px; }
.shop-import-row { display: flex; gap: 8px; align-items: center; }
.shop-import-row .control { flex: 1; }
.type-row { display: flex; gap: 8px; align-items: center; }
.type-row .control { flex: 1; }
.type-row .btn { white-space: nowrap; }
#shop-import-status { margin: 6px 0 0; font-size: 13px; }
#shop-import-status.error { color: var(--danger); }

/* A field an import left unfilled (component_dialog.js). Advisory only: nothing here
   is required and nothing is blocked — the tint just shows which fields the shop or
   the invoice had nothing to say about, and it clears as soon as one is given a
   value.

   background-COLOR, not the shorthand: a select's chevron is a background-image and
   the shorthand would wipe it. And --unfilled-tint rather than --danger-soft, which
   it matches on screen: a select hands this colour to its dropdown list too, and a
   translucent one has nothing to composite against there. */
.control.is-unfilled { background-color: var(--unfilled-tint); }

/* ---------- new-type dialog (§13) ---------- */
/* Widen the dialog itself (the base rule caps it at 440px); it holds a
   two-column parameter builder plus an inherited-parameters side panel. */
#type-dialog { width: min(880px, calc(100vw - 32px)); max-height: calc(100vh - 48px); }
#type-dialog > article { display: flex; flex-direction: column; max-height: calc(100vh - 48px); }
#type-dialog article > header { flex: none; }
.type-body { display: flex; align-items: stretch; min-height: 0; flex: 1; overflow: hidden; }
.type-main { flex: 1 1 auto; min-width: 0; overflow-y: auto; }
/* The inherited list runs the full height of the dialog and scrolls on its own. */
.type-inherited {
  flex: 0 0 250px; overflow-y: auto;
  border-left: 1px solid var(--border); background: var(--surface-2);
  padding: 16px 18px; display: flex; flex-direction: column; gap: 10px;
}
.inherited-head { font-size: 12px; font-weight: 600; color: var(--muted); letter-spacing: .01em; }
.inherited-hint { color: var(--muted); font-size: 13px; margin: 0; }
.inherited-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.inherited-item { display: flex; flex-direction: column; gap: 2px; font-size: 13px; }
.inherited-item .ip-name { font-weight: 600; color: var(--text); }
.inherited-item .ip-meta { color: var(--muted); font-size: 12px; }
.params-head { display: flex; align-items: center; justify-content: space-between; }
.params-head label { font-size: 12px; font-weight: 600; color: var(--muted); letter-spacing: .01em; }
.params-empty { color: var(--muted); font-size: 13px; margin: 0; }
.params { display: flex; flex-direction: column; gap: 12px; }
.param-row {
  display: flex; flex-direction: column; gap: 10px;
  padding: 12px; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface-2);
}
.param-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.param-flags { display: flex; align-items: center; gap: 16px; }
.param-flags .param-remove { margin-left: auto; }
/* The parameters list inside the type-management dialog (§13 edit). Wider than a
   form dialog so the label + details + actions sit on one line, with a scrollable,
   divided list. */
#type-params-dialog { width: min(640px, calc(100vw - 32px)); }
.param-list {
  list-style: none; margin: 0; padding: 2px 0;
  max-height: min(58vh, 440px); overflow-y: auto;
}
.param-list-row {
  display: flex; align-items: center; gap: 16px;
  padding: 11px 20px; border-top: 1px solid var(--border);
}
.param-list-row:first-child { border-top: none; }
.param-list-meta { flex: 1; min-width: 0; }
.param-list-label { font-weight: 600; }
.param-list-detail {
  color: var(--muted); font-size: 13px; margin-top: 2px; overflow-wrap: anywhere;
}
.param-list-actions { display: inline-flex; gap: 6px; flex: none; }
#type-params-empty { padding: 16px 20px; margin: 0; }
#type-params-dialog footer {
  padding: 12px 20px; margin: 0; border-top: 1px solid var(--border);
}
.check { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--muted); }
.check input { accent-color: var(--accent); }

/* ---------- per-parameter matchers panel ---------- */
/* Wider than the default dialog, and a flex column so the two sections scroll as one
   body while the header and footer stay put. */
#param-matchers-dialog { width: min(620px, calc(100vw - 32px)); max-height: calc(100vh - 48px); }
#param-matchers-dialog > article {
  display: flex; flex-direction: column; max-height: calc(100vh - 48px); min-height: 0;
}
#param-matchers-dialog article > header { flex: none; }
.pm-body { overflow-y: auto; flex: 1 1 auto; min-height: 0; }
.pm-section + .pm-section { border-top: 6px solid var(--surface-2); }
.pm-heading {
  margin: 0; padding: 14px 20px 6px; font-size: 13px; font-weight: 600;
}
.pm-heading .muted { font-weight: 400; }
.pm-sub { padding: 2px 20px 10px; margin: 0; font-size: 13px; }
.pm-list { list-style: none; margin: 0; padding: 0; }
.pm-row, .pm-add {
  display: flex; align-items: center; gap: 10px; padding: 7px 20px;
}
.pm-row { border-top: 1px solid var(--border); }
.pm-row:first-child { border-top: none; }
.pm-add { border-top: 1px solid var(--border); }
/* min-width:0 lets these flex children shrink below their content — without it a
   <select> stretches to its longest option and squeezes the alias to nothing. The
   alias takes the slack; the value picker is capped. */
.pm-alias { flex: 1 1 auto; min-width: 0; }
.pm-arrow { flex: none; }
.pm-target { flex: 0 1 40%; min-width: 0; }
.pm-del, .pm-add .btn { flex: none; }
#param-matchers-error { padding: 10px 20px 0; margin: 0; }
#param-matchers-dialog footer {
  padding: 12px 20px; margin: 0; border-top: 1px solid var(--border); flex: none;
  justify-content: space-between; align-items: center;
}
.pm-status { margin: 0; font-size: 13px; }
.pm-status-ok { color: var(--accent); }

/* ---------- login ---------- */
.login { max-width: 380px; margin: 8vh auto 0; }
.login .card-pad { padding: 30px 28px; display: flex; flex-direction: column; gap: 18px; }
.login h1 { font-size: 20px; }
.login p.lede { margin: 2px 0 4px; color: var(--muted); font-size: 14px; }
.login form { display: flex; flex-direction: column; gap: 16px; margin: 0; }

@media (max-width: 760px) {
  .grid2 { grid-template-columns: 1fr; }
  .appbar nav { display: none; }
}

/* ---------- Tabulator skin ----------
 * Overrides the CDN theme (loaded before this file) with our tokens. */
#components-table { margin-top: 2px; }
/* The row opens the part (app.js rowClick), so it has to look like it does. Only
   this table: the other Tabulators are lists you read or edit in place. */
#components-table .tabulator-row { cursor: pointer; }
.tabulator {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-sm);
  font-family: var(--font-sans); font-size: 14px; overflow: hidden;
}
.tabulator .tabulator-header {
  background: var(--surface-2); border-bottom: 1px solid var(--border); color: var(--muted);
}
.tabulator .tabulator-header .tabulator-col {
  background: transparent; border-right: none;
}
.tabulator .tabulator-header .tabulator-col .tabulator-col-content { padding: 10px 12px; }
.tabulator .tabulator-header .tabulator-col .tabulator-col-title {
  font-size: 11.5px; text-transform: uppercase; letter-spacing: .06em;
  font-weight: 650; color: var(--muted);
}
/* per-column live text filter (app.js headerFilter). The whole header cell is
   sortable, so keep the input from triggering a sort when it is clicked. */
.tabulator .tabulator-header .tabulator-col .tabulator-header-filter { padding: 0 12px 8px; }
.tabulator .tabulator-header .tabulator-col .tabulator-header-filter input {
  appearance: none; font: inherit; font-size: 13px; text-transform: none;
  letter-spacing: normal; width: 100%; color: var(--text);
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); padding: 5px 9px; min-height: 30px;
  transition: border-color .15s, box-shadow .15s;
}
.tabulator .tabulator-header .tabulator-col .tabulator-header-filter input:focus {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--ring);
}
.tabulator .tabulator-header .tabulator-col .tabulator-header-filter input::placeholder {
  color: var(--faint); text-transform: none;
}
.tabulator .tabulator-header .tabulator-col.tabulator-sortable:hover {
  /* The header sits on --surface-2, which --row-hover barely differs from;
     use the stronger token so the hover reads. !important is needed to beat
     the CDN theme's own near-white header hover (same as the row rule below). */
  background: var(--border-strong) !important;
}
/* The separator is an inset shadow, not a border, because a shadow takes up no
   space. shared.js hands the library a fixed rowHeight and it applies that height
   to the CELL, so a 1px border on the row would sit outside it and make the row
   taller than the number the table was told. Drawn rather than laid out, it can't. */
.tabulator .tabulator-row {
  background: var(--surface); color: var(--text);
  box-shadow: inset 0 -1px 0 var(--border);
}
.tabulator .tabulator-row.tabulator-row-even { background: var(--surface); }
.tabulator .tabulator-row.tabulator-selectable:hover,
.tabulator .tabulator-row:hover { background: var(--row-hover) !important; }
/* Tabulator can also tint the individual cell on hover — keep it transparent
   so only the row-level --row-hover shows. */
.tabulator .tabulator-row:hover .tabulator-cell { background: transparent; }
/* The vertical padding is what sets the row height: it wraps the tallest thing a
   cell holds, a 28px .btn, and shared.js's TABLE_DEFAULTS.rowHeight has to be at
   least the sum. Change one and change the other. */
.tabulator .tabulator-row .tabulator-cell { border-right: none; padding: 6px 12px; }
.tabulator .tabulator-row:last-child { box-shadow: none; }
.tabulator .tabulator-tableholder .tabulator-placeholder-contents { color: var(--muted); font-size: 14px; }

/* Tabulator's hover tooltip (BOM substitutes, References): the CDN default is a
   near-white box that's unreadable on our theme, so restyle it with our tokens
   and honour the newlines our tooltip text uses. */
.tabulator-tooltip {
  max-width: 340px;
  padding: 7px 10px;
  border-radius: 8px;
  font-size: 12.5px;
  line-height: 1.5;
  white-space: pre-line;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
}

/* cell content helpers used by the Tabulator formatters (app.js) */
.cell-mpn { font-family: var(--font-mono); font-weight: 600; letter-spacing: -.01em; color: var(--text); }
.cell-mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.cell-qty { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.cell-qty.is-zero { color: var(--danger); }
/* Truncate rather than wrap. Tabulator's own .tabulator-cell already clips, but
   `display: block` is what makes the ellipsis apply to this inline span, and
   stating the rest keeps the behaviour ours rather than the vendor's. NOT muted:
   the description is often the only text that identifies a part at a glance. */
.cell-desc {
  display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* row actions: quiet until the row is hovered (spec §15) */
.row-actions { display: inline-flex; gap: 4px; opacity: 0; transition: opacity .12s; }
.tabulator-row:hover .row-actions { opacity: 1; }
/* Every button inside a table row is this tall — not just the ones in .row-actions.
   The tallest thing in a cell decides how tall the row wants to be, and a row that
   wants a different height than shared.js's TABLE_DEFAULTS.rowHeight either clips
   its own content or leaves a gap. One height here means one rowHeight there. */
.tabulator .tabulator-row .btn { min-height: 28px; }

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; }
}

/* A panel card header: the title and its "+ Add"/"Edit" button on one row. */
.widget-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; margin-bottom: 14px;
}
.widget-head h2 { margin: 0; }
/* Several buttons on the right of a card header. Deliberately NOT .row-actions:
   that one is opacity-0 until its table row is hovered. */
.widget-actions { display: inline-flex; gap: 6px; }

/* The immutable Type/MPN shown at the top of the edit dialog. */
.edit-readonly { display: flex; gap: 20px; }
.edit-readonly .field span { font-size: 14px; padding: 2px 0; }

/* ---------- attachments panel (§10) ---------- */
/* The add form lives in a dialog now (dialog form supplies the column layout), so
   the panel itself is just the list. */
.attachment-list { list-style: none; display: flex; flex-direction: column; gap: 8px; }
.attachment-item {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 0; border-bottom: 1px solid var(--border);
}
.attachment-item:last-child { border-bottom: none; }
.attachment-notes { font-size: 13px; }
.attachment-item .btn { margin-left: auto; }

/* ---------- links panel (external clickable URLs) ---------- */
.link-list { list-style: none; display: flex; flex-direction: column; gap: 8px; }
.link-item {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 0; border-bottom: 1px solid var(--border);
}
.link-item:last-child { border-bottom: none; }
.link-item a { overflow-wrap: anywhere; }
.link-notes { font-size: 13px; }
/* The first action button absorbs the free space so the button group sits at the
   row's right edge; later buttons reset to 0 (the flex `gap` spaces them) — else a
   second auto margin would split the space and strand Edit in the middle. */
.link-item .btn { margin-left: auto; }
.link-item .btn ~ .btn { margin-left: 0; }

/* ---------- component image gallery + lightbox (§10) ---------- */
.component-images {
  display: flex; flex-wrap: wrap; gap: 8px;
  justify-content: flex-end; align-content: flex-start; max-width: 60%;
}
.component-thumb {
  appearance: none; padding: 0; height: 72px; width: 72px; cursor: pointer;
  overflow: hidden; border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--surface-2);
  transition: border-color .12s;
}
.component-thumb img { display: block; height: 100%; width: 100%; object-fit: cover; }
.component-thumb:hover { border-color: var(--accent); }
.component-thumb:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }

/* The lightbox is a deliberately dark overlay in either theme. It fills the
   viewport and centres the image with flex — overriding the generic
   `dialog { width: min(440px, …) }` so a wide image can't overflow a narrow
   box and look off-centre. `display:flex` is scoped to [open] so the closed
   dialog stays hidden. */
dialog.lightbox {
  border: none; background: transparent; padding: 0; margin: 0;
  inset: 0; width: auto; max-width: none; max-height: none;
}
dialog.lightbox[open] { display: flex; align-items: center; justify-content: center; }
dialog.lightbox::backdrop { background: rgba(0, 0, 0, .82); }
.lightbox-img {
  display: block; max-width: 90vw; max-height: 85vh; object-fit: contain;
  border-radius: var(--radius); box-shadow: 0 12px 48px rgba(0, 0, 0, .55);
}
.lightbox-nav, .lightbox-close {
  position: fixed; background: rgba(0, 0, 0, .5); color: #fff; border: none;
  cursor: pointer; line-height: 1;
}
.lightbox-nav { top: 50%; transform: translateY(-50%); width: 56px; height: 72px; font-size: 40px; border-radius: var(--radius-sm); }
.lightbox-prev { left: 3vw; }
.lightbox-next { right: 3vw; }
.lightbox-close { top: 3vh; right: 3vw; width: 44px; height: 44px; font-size: 26px; border-radius: 50%; }
.lightbox-nav:hover, .lightbox-close:hover { background: rgba(0, 0, 0, .78); }

/* ---------- BOM report (§21) ---------- */
.bom-summary { margin-bottom: 20px; }
/* The remedies for an unresolved count, on the line below it. Only rendered
   while there is something unresolved, so it has no empty state. */
/* The take dialog. Three times the standard dialog width: this one carries a
   five-column table of the whole BOM, and at 440px the columns had nothing left
   to share. */
#bom-take-dialog { width: min(1320px, calc(100vw - 32px)); }

/* Fixed layout so the starting widths below actually hold — with `auto`, one long
   designator group sets the column width for the whole table. `resize` on the
   header keeps them a starting point rather than a decision: which column matters
   depends on the BOM in front of you. */
#take-table { table-layout: fixed; width: 100%; }
#take-table th { resize: horizontal; overflow: hidden; }
.take-col-refs { width: 220px; }
.take-col-mpn { width: 180px; }
.take-col-qty { width: 110px; }
.take-col-short { width: 90px; }
/* "From" takes what is left: it is the one column whose content is genuinely
   variable (a path, or several), and the only one worth giving the slack to. */

/* A designator group runs to whatever the board needs; show what fits and put the
   rest in the hover, exactly as the BOM report's References column does. */
.take-refs {
  display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The take dialog: its two controls on one row, and a table tall enough to show
   a real BOM without the dialog itself scrolling. */
.take-controls {
  display: flex;
  gap: 16px;
  align-items: flex-end;
  margin-bottom: 12px;
}

.take-source { min-width: 320px; }

.take-table-wrap { max-height: 50vh; overflow-y: auto; }

.take-qty { width: 90px; }

.take-choice { min-width: 220px; }

/* A line that cannot be taken is still listed — seeing which ones stop the run is
   the point — and tinted, the same tint the component dialog puts behind a field
   an import left unfilled. It carries further than a dimmed row or a name in a
   panel: the problem is marked where the problem is.

   On the cells rather than the row: a <tr> background sits behind its cells, so
   anything the table paints on a td would cover it. */
.take-blocked > td { background-color: var(--unfilled-tint); }

/* One short sentence now that the panel counts rather than names — the guards
   against an unbreakable designator group and against a wall of them went with
   the list they were protecting. */
.take-blockers { margin-bottom: 12px; }

/* The reversal banner and the past-takes list on a snapshot / a BOM report. */
.take-reversed {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 20px;
}

.take-list { margin: 0; padding-left: 18px; }
.take-list li { margin: 4px 0; }

.bom-summary-actions {
  display: flex;
  gap: 8px;
  margin: 10px 0 0;
}
/* Per-line actions on the BOM report. Deliberately always visible, unlike the
   hover-only `.row-actions` on the components table: "Assign…" is the point of
   the row, not a shortcut for something reachable elsewhere. */
.bom-row-actions { display: inline-flex; gap: 6px; }

/* ---------- assign-a-component picker ---------- */
/* A table needs far more room than the 440px default dialog (same reasoning as
   #stock-dialog above), and the body scrolls while the confirm bar stays put. */
/* `overflow: hidden` on the dialog itself: a <dialog> defaults to `auto`, so if its
   content ever measured a pixel over the cap it would grow a scrollbar of its own —
   a second one beside the table's, and the one that can't be the answer, since the
   table is the only part with anything to scroll. */
#bom-pick-dialog {
  width: min(1280px, calc(100vw - 32px)); max-height: calc(100vh - 48px);
  overflow: hidden;
}
/* While the picker is open the page behind it can't be reached, so its scrollbar is
   nothing but a second bar next to the table's. Standard modal scroll-lock. */
html.bom-pick-open, html.bom-pick-open body { overflow: hidden; }
#bom-pick-dialog > article {
  display: flex; flex-direction: column; max-height: calc(100vh - 48px); min-height: 0;
}
/* The title carries user data (a designator group), so it must never be what
   decides the dialog's width — it wraps and yields instead. */
#bom-pick-dialog article > header { flex: none; }
#bom-pick-dialog article > header strong { min-width: 0; overflow-wrap: anywhere; }
.bom-pick-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 12px 20px; border-bottom: 1px solid var(--border); flex: none;
}
.bom-pick-bar p { margin: 0; font-size: 13px; }
.bom-pick-actions { display: flex; gap: 10px; flex: none; }
/* `overflow: hidden`, NOT auto: the table sizes itself to this box (see fitTable in
   bom_pick.js) and scrolls internally, so a scrollbar here would be a second one
   wrapped around a table that already has its own — and a dead one, since there is
   nothing left to scroll. */
.bom-pick-body {
  display: flex; gap: 20px; padding: 16px 20px;
  overflow: hidden; flex: 1 1 auto; min-height: 0;
}
.bom-pick-line { flex: 0 0 210px; }
.bom-pick-line h4 { margin: 0 0 8px; font-size: 13px; }
.bom-pick-line dl {
  margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 4px 10px; font-size: 13px;
}
.bom-pick-line dt { color: var(--muted); }
.bom-pick-line dd { margin: 0; overflow-wrap: anywhere; }
/* A column, so the table takes the height left over below the type picker rather
   than pushing the box taller. */
.bom-pick-inventory {
  flex: 1 1 auto; min-width: 0;
  display: flex; flex-direction: column; min-height: 0;
}
.bom-pick-inventory .field { margin-bottom: 10px; max-width: 240px; flex: none; }
/* Rows here are picked, not read: double-click confirms a choice, and without this
   it would also drag a text selection across the row. */
#bom-pick-table { min-height: 0; user-select: none; }
/* The chosen row has to read as chosen from across the dialog. */
/* The picked row has to read as picked from across the dialog, and stay readable
   under the cursor — so it takes the accent tint the active nav item uses (a
   different HUE from the grey hover, not just a shade of it) plus an accent edge.
   `!important` because the hover rule above is itself `!important`; without it,
   pointing at the picked row would erase the only sign of what is about to be
   committed. */
#bom-pick-table .tabulator-row.is-picked,
#bom-pick-table .tabulator-row.is-picked:hover {
  background: var(--row-picked) !important;
  box-shadow: inset 3px 0 0 var(--accent);
}
/* The cells keep the ordinary text colour: this is a row of data being compared
   against other rows, so tinting it would cost legibility to say something the
   background and the edge already say. */
#bom-pick-error { padding: 0 20px 10px; margin: 0; flex: none; }
/* "Boards" sits among the head actions: a small count box, labelled above like a
   .field, not a full-width control stretching the header. Its input borrows the
   .btn-sm metrics (30px, not the 38px of a full .control) so it lines up with the
   small buttons beside it instead of standing a head taller. */
.bom-boards {
  display: flex; flex-direction: column; gap: 5px;
  font-size: 12px; font-weight: 600; color: var(--muted); letter-spacing: .01em;
}
.bom-boards .control {
  width: 88px; min-height: 30px; padding: 4px 10px;
  font-size: 12.5px; border-radius: 7px;
}
#bom-status { margin: 0 0 12px; }
/* Substitutes stay on one line (uniform row height keeps Tabulator fast); links
   are dot-separated and the cell truncates, with full detail in the tooltip. */
.subs-inline a + a { margin-left: 2px; }

/* Transient notices (see showToast). Fixed to the viewport corner so they never
   shift page content, and above dialogs so a notice raised as one closes is seen. */
.toast-tray {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  z-index: 100;
  display: flex;
  flex-direction: column;
  gap: .5rem;
  max-width: min(28rem, calc(100vw - 2rem));
}
.toast {
  padding: .7rem .9rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-md);
  cursor: pointer;
  font-size: .92rem;
}
.toast-warn {
  border-color: var(--warn);
  background: var(--warn-soft);
  color: var(--warn);
}
.toast-ok {
  border-color: var(--ok);
  background: var(--ok-soft);
  color: var(--ok);
}

/* The scan panel's message line. Fixed to one line whatever it says (or when
   it says nothing), so the panel's height is constant: the components table is
   sized to the whole page, and a panel that grows re-lays out the table, which
   rebuilds the row button a click is landing on. Long messages ellipsize here
   and arrive in full as a toast. */
.scan-status {
  /* A fixed box, not min-height: an empty <p> and a line of text must measure
     the SAME, so height and line-height are pinned together. */
  height: 1.4rem;
  line-height: 1.4rem;
  margin: 6px 0 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The scan field currently collecting keystrokes (scan putaway): an always-on
   ring, since it is armed by state, not by focus. */
.scan-armed {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* What-to-do-with-this-bag chooser. It has no form, so it can't lean on the
   `dialog form` padding the other dialogs get — state it here instead. */
.scan-choice { display: flex; flex-direction: column; gap: 14px; padding: 18px 20px; }
/* Full-width rows rather than a button bar: these are three destinations, not a
   confirm-or-cancel, and a stack reads as a menu at a glance. */
.scan-choice-actions { display: flex; flex-direction: column; gap: 8px; }
.scan-choice-actions .btn { justify-content: flex-start; gap: 10px; }
/* The key that also does it. Sized to a fixed square so the three labels line up
   whatever letter is in the box. */
.scan-choice kbd {
  font-family: var(--font-mono); font-size: 12px; font-weight: 700;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 22px; padding: 2px 5px; flex: none;
  color: var(--muted); background: var(--surface-2);
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
}

/* --- Label printing (§7) --------------------------------------------------- */
/* The preview is the bitmap the printer would receive, so it is shown at the
   tape's real proportions: a checkerboard behind it makes the white label
   visible as an object rather than a hole in the dialog, and the pixelated
   rendering keeps a 300 dpi QR honest instead of smoothing it into a blur. */
.label-preview {
  display: flex; align-items: center; justify-content: center;
  padding: 10px; margin: 10px 0; min-height: 90px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: repeating-conic-gradient(var(--surface-2) 0 25%, transparent 0 50%)
    50% / 16px 16px;
}
.label-preview img { max-width: 100%; max-height: 220px; image-rendering: pixelated; }
.label-preview img:not([src]) { display: none; }
.hint { color: var(--muted); font-size: 12px; margin: 4px 0 0; }
.row-actions-inline { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }

/* --- Label-printer setup (§7) ---------------------------------------------- */
/* A form of few fields with a lot of explanation around it, so the prose is
   given room rather than squeezed beside the controls. */
.setup-card { max-width: 760px; margin-bottom: 18px; }
.setup-card .field { margin-bottom: 20px; }
.setup-card .muted { margin: 6px 0 0; font-size: 13px; line-height: 1.5; }
/* Fields that read as one sentence — user @ host port — stay on one line. */
.row-fields { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.row-fields .control { flex: 1 1 12ch; min-width: 0; }
.control-narrow { flex: 0 0 11ch !important; }
.row-fields .at, .inline-label { color: var(--muted); font-size: 13px; }
/* The sudo warning: heading, prose and commands stacked, unlike the app's other
   notices, which are a line of text with a button beside it. */
.setup-card .notice { display: block; margin: 22px 0; }
.setup-card .notice pre {
  margin: 10px 0 0; padding: 10px 12px; overflow-x: auto;
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
  font-size: 12.5px; line-height: 1.6;
}
.setup-actions { display: flex; justify-content: flex-end; }
.probe-result { margin: 12px 0 0; font-size: 13.5px; }
.probe-result[data-state="good"] { color: var(--ok); }
.probe-result[data-state="bad"] { color: var(--danger); }
.probe-result[data-state="waiting"] { color: var(--muted); }

/* ---------- `hidden` means hidden ---------- */
/* Last in the file AND !important, and it needs both.

   The UA rule is `[hidden] { display: none }` at origin level, so ANY author
   `display` beats it — and this app puts `display` on nearly everything it
   toggles: buttons, badges, flex rows, the location picker's parts. That was
   paid for nine separate times, each with its own one-line reset and its own
   comment explaining the trap; the ninth was written by someone who had read the
   other eight and walked into it anyway. The failure is silent in the worst way:
   the element is hidden as far as `el.hidden` and every test asserting it are
   concerned, and plainly visible on screen with stale content.

   `!important` because `[hidden]` is specificity (0,1,0): being last beats a
   single class, but still loses to any two-class selector that sets `display` —
   `.user .who` and `.login .card-pad` are the two here. Only the important form
   actually holds in a browser.

   LAST because jsdom's cascade is source-order only and ignores `!important`
   outright — verified, not assumed — so the same rule at the top of this file
   would work in every browser and read as broken to the computed-style checks
   that exist to guard it. Those checks are the only thing that can see this
   class of bug, so the rule goes where they can see it too.

   Nothing here has a legitimate reason to out-rank the attribute — an element
   that must show while carrying `hidden` is a contradiction, not a design —
   with ONE standardised exception, which is why the selector is not a bare
   `[hidden]`. `hidden="until-found"` marks a collapsed region that deliberately
   stays in the layout tree so find-in-page can reach it; the browser then fires
   `beforematch` and drops the attribute to reveal it. It still matches
   `[hidden]`, so the blanket form would pin it to `display: none` — and being
   important, no author rule could take that back. The section would simply never
   appear, with nothing in the stylesheet to explain it: the same silent failure
   this rule exists to end, pointed the other way. Nothing uses it today; the
   next collapsible section is exactly where someone would reach for it.

   No script in this app writes `style.display`, so there is no inline `display`
   for this to overrule either. */
[hidden]:not([hidden="until-found"]) { display: none !important; }
