/* Flintt — web-native layout on the shared design tokens (tokens.css).
   Left sidebar shell + content column + the two page bands + cards + the docked inspector.
   There is no drawer and no scrim: overlay drawers are forbidden (DESIGN.md §1). */
*{box-sizing:border-box;margin:0}
html,body{height:100%}
body{background:var(--surface-page);color:var(--text-primary);
  font:var(--font-size-body)/var(--line-body) var(--font-sans);
  letter-spacing:var(--tracking-body);font-feature-settings:"cv11","ss01";
  -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
a{color:var(--accent);text-decoration:none}
a:hover{text-decoration:underline}
button{font:inherit;cursor:pointer}
input,select{font:inherit;color:var(--text-primary)}
::selection{background:var(--accent-soft-line)}
:is(a,button,summary,[role="button"]):focus-visible{outline:2px solid var(--focus-ring);
  outline-offset:2px;border-radius:var(--radius-sm)}
.num,.mono{font-family:var(--font-mono);font-variant-numeric:tabular-nums}
.muted{color:var(--text-tertiary)}
.small{font-size:var(--font-size-label)}
/* ---- icons: one set, no glyphs (DESIGN.md §4) — geometry from core/icons.js, stroke
   rides currentColor, size is the only knob (20 nav · 16 buttons/inline · 14 dense rows) */
.ic{display:block;flex:none}

/* ---- app frame: fixed left sidebar + content column ------------------------------------- */
/* ONE frame, ONE width (DESIGN.md §2):
     .appframe = sidebar 220px (56px collapsed <900px) + .content
     .content  = flex:1, min-width:0 — a flex row so the inspector can dock after .page
     .page     = max-width 1440px · padding 20px 24px 80px · margin 0 auto
   Every page uses .page UNCHANGED. No page-specific max-width, no per-tab padding: tabs of
   the same page (BOM / Settings) are the same width, always. A width that changes when you
   switch tabs is a bug, not a style — do not add a `.page:has(...)` override here. */
/* --demobar-h is the height of the STRIP above the frame, whichever strip that is: the demo
   bar, or the agent bar (core/agentbar.js). ONE at a time, enforced in app.js, and a demo
   session wins — what a session IS outranks what you could set up. Both toggle a class on
   <html> rather than only their own `hidden`, because this variable is what the frame, the
   sidebar and the inspector subtract from 100vh; setting only `hidden` leaves them a full
   viewport tall and pushes the whole app past the bottom of the window.
   The full-height rules below subtract it so the frame plus the bar is exactly one viewport.
   Without this the page is 100vh + 24px tall: the whole app gains a permanent 24px scroll and
   the sticky 100vh sidebar hangs off the bottom by the height of the bar. At 0px every calc
   below is a no-op, so the non-demo path is byte-for-byte the old layout. */
.appframe{display:flex;min-height:calc(100vh - var(--demobar-h));align-items:stretch}
/* Width is a custom property with the old constant as its fallback (core/layout.js sets it from
   the reader's stored answer, clamped). Unset — the common case on a fresh browser — resolves to
   exactly the historical 220px, so the default frame is byte-for-byte what it was. */
.sidebar{position:sticky;top:var(--demobar-h);height:calc(100vh - var(--demobar-h));
  width:var(--sidebar-w,220px);flex:none;display:flex;flex-direction:column;
  /* A stacking level so the resize handle's overhang stays grabbable: it straddles the border by
     design, and .content paints after this in the flex row, so without one the outer ~3px of the
     strip belongs to the content column and only a sliver of it responds. */
  z-index:4;
  background:var(--surface-side);border-right:1px solid var(--border);
  padding:var(--space-3) var(--space-2)}
/* The sidebar's top edge: wordmark on the left, the rail toggle on the right. The row owns the
   rule under it, not .brand, so the toggle sits above the line with the mark rather than
   below it. */
.sidehead{display:flex;align-items:center;gap:4px;padding:6px 10px 12px;margin:0 2px var(--space-3);
  border-bottom:1px solid var(--border)}
.brand{display:flex;align-items:center;gap:9px;flex:1;min-width:0}
.brand:hover{text-decoration:none}
.railtoggle{flex:none;padding:4px}
.railtoggle .glyph{display:inline-flex;align-items:center;justify-content:center;line-height:1}
/* One tile, two homes: the sidebar brand and the sign-in screen. Scoping it to .brand was
   why the login had to reach for a raster instead.
   min-width is NOT redundant with width. The in-app feedback shutter snapshots the page with
   snapDOM, and snapDOM drops `width` when it serializes a <span> — every other geometry
   property survives. The tile then shrank to its 13px glyph, and a 6px radius on a 13px-wide
   box is a pill, so every screenshot anyone sent us had an oval where the logo should be.
   min-width survives, so the box keeps its 22px either way. */
.brand .mark,.loginmark .mark{display:inline-flex;align-items:center;justify-content:center;
  border-radius:6px;background:var(--accent);color:var(--text-on-accent);
  width:22px;min-width:22px;height:22px;font:var(--font-weight-selected) 12px var(--font-sans);flex:none}
.brand .mark svg,.loginmark .mark svg{display:block}
/* Bigger on the sign-in screen, where it is the only thing on the page. */
.loginmark .mark{width:44px;min-width:44px;height:44px;border-radius:11px}
.loginmark .mark svg{width:24px;height:24px}
.brand .word{font:var(--font-weight-strong) 13px var(--font-sans);letter-spacing:.26em;text-transform:uppercase;
  color:var(--text-primary)}
.sidenav{display:flex;flex-direction:column;gap:2px;flex:1;min-height:0;padding:0 2px}
.sidenav a{display:flex;align-items:center;gap:10px;padding:7px 10px;border-radius:var(--radius-sm);
  color:var(--text-secondary);font-size:var(--font-size-label);font-weight:var(--font-weight-medium)}
.sidenav a:hover{color:var(--text-primary);background:var(--surface-raised);text-decoration:none}
.sidenav a.on{color:var(--text-primary);background:var(--accent-soft);
  box-shadow:inset 0 0 0 1px var(--accent-soft-line);font-weight:var(--font-weight-strong)}
.sidenav .glyph{width:20px;flex:none;display:inline-flex;align-items:center;justify-content:center;
  line-height:1}
.sidefoot{flex:none;border-top:1px solid var(--border);margin:0 2px;padding:var(--space-2) 0 2px;
  display:flex;flex-direction:column;gap:4px}
/* The sidebar bottom's links (Feedback, Settings): the same anatomy as the main nav. */
.footlink{display:flex;align-items:center;gap:10px;width:100%;padding:7px 10px;
  border:none;border-radius:var(--radius-sm);background:none;text-align:left;text-decoration:none;
  color:var(--text-secondary);font-size:var(--font-size-label);font-weight:var(--font-weight-medium);
  cursor:pointer}
.footlink:hover{color:var(--text-primary);background:var(--surface-raised);text-decoration:none}
.footlink.on{color:var(--text-primary);background:var(--accent-soft);
  box-shadow:inset 0 0 0 1px var(--accent-soft-line);font-weight:var(--font-weight-strong)}
.footlink .glyph{width:20px;flex:none;display:inline-flex;align-items:center;justify-content:center;
  line-height:1}
.content{flex:1;min-width:0;display:flex;align-items:flex-start}
/* No `margin:0 auto` here: a main-axis auto margin eats the free space BEFORE flex-grow, so
   docking the 360px inspector would slide the whole page 180px left (DESIGN.md §2: "Page
   chrome never shifts horizontally when it opens"). flex-grow does the sizing instead — the
   page's left edge is pinned to the content column's, and the space past 1440px is exactly
   where the inspector docks. */
.page{flex:1 1 auto;min-width:0;max-width:1440px;margin:0;padding:20px 24px 80px}
@media (max-width:900px){
  .sidebar{width:56px;padding:var(--space-3) 6px}
  .sidehead{justify-content:center;padding:6px 4px 12px;margin:0 0 var(--space-3)}
  .brand{justify-content:center;flex:none}
  .brand .word,.sidenav .navlabel,.footlink .navlabel{display:none}
  .sidenav{padding:0}
  .sidenav a{justify-content:center;padding:7px 4px}
  .footlink{justify-content:center;padding:7px 4px}
  .sidefoot{margin:0}
}

/* ---- buttons / inputs ------------------------------------------------------------- */
.btn{display:inline-flex;align-items:center;gap:6px;border-radius:var(--radius-md);
  padding:7px 13px;font:var(--font-weight-strong) var(--font-size-label) var(--font-sans);
  border:1px solid transparent;transition:all var(--dur-fast) var(--ease);white-space:nowrap}
.btn.primary{background:var(--accent);color:var(--text-on-accent);border-color:var(--accent)}
.btn.primary:hover{background:var(--accent-strong)}
.btn.outline{background:var(--surface-card);color:var(--text-secondary);border-color:var(--border-strong)}
.btn.outline:hover{border-color:var(--text-tertiary);color:var(--text-primary)}
.btn.ghost{background:transparent;color:var(--text-secondary);border-color:transparent}
.btn.ghost:hover{background:var(--surface-raised);color:var(--text-primary)}
.btn.danger{color:var(--error)}
.btn.danger:hover{border-color:var(--error)}
.btn.sm{padding:4px 9px;font-size:11px}
.btn:disabled{opacity:.45;cursor:default;pointer-events:none}
/* icon + word inline (external links, row actions) — icons never appear alone where a
   word fits (DESIGN.md §4) */
.iconlink{display:inline-flex;align-items:center;gap:5px}
.input,select.input{border:1px solid var(--border-strong);border-radius:var(--radius-md);
  padding:7px 11px;background:var(--surface-card);font-size:var(--font-size-label);
  outline:none;min-width:0}
.input:focus{border-color:var(--focus-ring);box-shadow:0 0 0 2px var(--accent-soft-line)}
.input::placeholder{color:var(--text-muted)}
label.field{display:flex;flex-direction:column;gap:4px;font-size:10px;letter-spacing:.06em;
  text-transform:uppercase;color:var(--text-tertiary)}
label.field .input,label.field select.input{text-transform:none;letter-spacing:0;
  font-size:var(--font-size-label);color:var(--text-primary)}
.check{display:inline-flex;align-items:center;gap:6px;font-size:var(--font-size-label);
  color:var(--text-secondary);cursor:pointer;white-space:nowrap}
.check input{accent-color:var(--accent);width:14px;height:14px;margin:0;cursor:pointer}
.segc{display:inline-flex;border:1px solid var(--border-strong);border-radius:var(--radius-md);
  overflow:hidden;background:var(--surface-card)}
.segc button{border:none;background:transparent;padding:5px 11px;font-size:var(--font-size-label);
  color:var(--text-tertiary)}
.segc button:hover{color:var(--text-primary);background:var(--surface-raised)}
.segc button.on{background:var(--surface-inset);color:var(--text-primary);
  font-weight:var(--font-weight-strong)}
.segc button.on[data-tone="warn"]{color:var(--warning)}
.segc button.on[data-tone="ok"]{color:var(--success)}

/* ---- cards ------------------------------------------------------------------------- */
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:var(--space-3)}
.card{background:var(--surface-card);border:1px solid var(--border);border-radius:var(--radius-lg);
  padding:var(--space-card);transition:border-color var(--dur-fast) var(--ease)}
.card.hoverable:hover{border-color:var(--border-strong)}
.card h3{font:var(--font-weight-selected) var(--font-size-body) var(--font-sans)}
.cardlink{display:block;color:inherit}
.cardlink:hover{text-decoration:none;border-color:var(--accent)}
.kv{display:flex;gap:8px;align-items:baseline;font-size:var(--font-size-label)}
.kv .k{color:var(--text-tertiary);flex:none}
.stack{display:flex;flex-direction:column;gap:var(--space-3)}
.rowflex{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap}

/* ---- chips: TWO kinds only (DESIGN.md §6) ------------------------------------------------ */
/* status chip = semantic color, read-only. The filter chip (.fchip) lives with band 2. */
.chip{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:var(--font-weight-strong);
  letter-spacing:.04em;padding:1.5px 8px;border-radius:6px;border:1px solid var(--border-strong);
  color:var(--text-tertiary);white-space:nowrap;text-transform:uppercase}
.chip.ok{color:var(--success);border-color:var(--success-line);background:var(--success-soft)}
.chip.warn{color:var(--warning);border-color:var(--warning-line);background:var(--warning-soft)}
.chip.err{color:var(--error);border-color:var(--error-line);background:var(--error-soft)}
/* Standalone warn, for text that is not a chip. `.chip.warn` above needs BOTH classes, so the
   part-picker's loose-match line — a plain div.muted.small — toggled `warn` and matched no rule
   at all, rendering the one warning that keeps a relaxed hit off a BOM in the same grey as an
   ordinary status. `.muted` also sets a colour, hence the specificity bump. */
.warn:not(.chip){color:var(--warning)}
.muted.warn:not(.chip){color:var(--warning)}
.chip.accent{color:var(--accent);border-color:var(--accent-soft-line);background:var(--accent-soft)}
.chip.sm{padding:0 6px}

/* Seller intent chips: preset toggles with a monogram tile. The tile's hue comes from the
   seller's canonical slot (inline --h), so it is stable and distinct per seller — the ONE
   place per-seller colors exist, which the seller-identity feature requires. */
.schip{display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 10px 0 4px;
  border-radius:999px;border:1px solid var(--border);background:var(--surface-card);
  color:var(--text-secondary);font:inherit;font-size:var(--font-size-label);cursor:pointer}
.schip:hover{border-color:var(--border-strong);color:var(--text-primary)}
.mtile{width:18px;height:18px;border-radius:5px;flex:none;display:inline-flex;
  align-items:center;justify-content:center;font-size:9px;
  font-weight:var(--font-weight-selected);color:var(--text-on-tool);
  background:hsl(var(--h,210) 50% 42%)}
/* A seller's logo mark (sellerLogo in dom.js): square, rounded 2px, centered with the name
   it sits next to. The monogram tile (.mtile) is the no-asset fallback. */
.sellerlogo{border-radius:2px;flex:none;object-fit:contain;vertical-align:-2px}
span.sellerlogo{vertical-align:middle}
.schip.on{border-color:var(--accent-soft-line);background:var(--accent-soft);
  color:var(--text-primary);box-shadow:inset 0 0 0 1px var(--accent-soft-line);
  font-weight:var(--font-weight-medium)}
.schips.never .schip.on{border-color:var(--warning-line);background:var(--warning-soft);
  box-shadow:inset 0 0 0 1px var(--warning-line)}

/* Seller combobox (the intent editors): type to filter the canonical sellers, click or
   Enter adds; chosen sellers are the removable chips beneath. No free text. */
.scombo{display:flex;flex-direction:column;gap:8px}
.scombo-box{position:relative;max-width:300px}
.scombo-box .input{width:100%}
/* FIXED, not absolute, and placed from JS. This list is used inside the supplier-rules
   panel, whose body is an overflow:auto scrollport — and an absolutely-positioned descendant
   of a scrollport is CLIPPED by it. Opening the Avoid field showed one and a half suggestions
   with the rest cut off at the panel's footer, which is the same clipping the panel itself had
   to escape one level up. Fixed positioning takes it out of every ancestor's overflow, so the
   list can use the room the window has rather than the room its container has. */
.scombo-list{position:fixed;z-index:70;max-height:224px;overflow-y:auto;padding:4px;
  background:var(--surface-card);border:1px solid var(--border-strong);
  border-radius:var(--radius-md);box-shadow:var(--shadow-pop)}
.scombo-opt{display:flex;align-items:center;gap:8px;width:100%;padding:6px 8px;border:none;
  border-radius:var(--radius-sm);background:none;font:inherit;font-size:var(--font-size-label);
  color:var(--text-primary);text-align:left;cursor:pointer}
.scombo-opt:hover{background:var(--surface-raised)}
.schip.picked{cursor:default}
.schip-x{display:inline-flex;align-items:center;justify-content:center;padding:2px;
  margin:0 -4px 0 2px;border:none;border-radius:50%;background:none;color:inherit;
  opacity:.55;cursor:pointer;line-height:1}
.schip-x:hover{opacity:1}

/* ---- table identity bits (the tables themselves live with .bomtbl below) ----------------- */
.mpnlink{font-family:var(--font-mono);color:var(--accent);cursor:pointer;background:none;
  border:none;padding:0;font-size:inherit;text-align:left}
.mpnlink:hover{text-decoration:underline}
.subl{font-size:10px;color:var(--text-tertiary);margin-top:1px}

/* ---- the agent strip (core/agentbar.js) --------------------------------------------------- */
/* The one place the agent hue appears in the frame. It reads as a different KIND of thing from
   the app around it — which is the point, since everything else in this stylesheet is monochrome
   so that colour can mean something about the data. */
.agentbar{position:sticky;top:0;z-index:60;height:var(--demobar-h);
  display:flex;align-items:center;gap:var(--space-2);padding:0 14px;
  font-size:var(--font-size-body);color:var(--text-secondary);
  background:var(--ai-soft);border-bottom:1px solid var(--ai-line);
  white-space:nowrap;overflow:hidden}
.agentbar .ab-mark{display:flex;align-items:center;color:var(--ai);flex:none}
.agentbar .ab-copy{min-width:0;overflow:hidden;text-overflow:ellipsis}
.agentbar .ab-copy b{color:var(--text-primary);font-weight:var(--font-weight-strong)}
.agentbar .ab-go{margin-left:auto;flex:none;display:flex;align-items:center;gap:2px;
  padding:4px 10px;border-radius:var(--radius-pill);
  background:var(--ai);color:var(--ai-on);font-weight:var(--font-weight-strong);
  font-size:var(--font-size-label);text-decoration:none}
.agentbar .ab-go:hover{background:var(--ai-strong);text-decoration:none}
.agentbar .ab-x{flex:none;display:flex;align-items:center;justify-content:center;
  width:22px;height:22px;border:0;background:none;color:var(--text-tertiary);
  border-radius:var(--radius-sm);cursor:pointer}
.agentbar .ab-x:hover{color:var(--text-primary);background:var(--ai-soft)}
:root.has-agentbar{--demobar-h:38px}
/* Narrow: the sentence is the first thing to go, never the action. A strip that keeps its copy
   and drops its button is an ad you cannot act on. */
@media (max-width:640px){
  .agentbar .ab-copy{font-size:var(--font-size-label)}
  .agentbar .ab-copy b{display:block}
}

/* ---- resize handles ---------------------------------------------------------------------- */
/* A 5px grab strip straddling the panel's border, painted only on hover/focus/drag: a visible
   rule on both panel edges at rest would add two vertical lines to every screen for a gesture
   most readers use once. It sits ABOVE the panel's own z-index so the strip stays grabbable
   over sticky content. */
.resizer{position:absolute;top:0;bottom:0;width:5px;z-index:6;cursor:col-resize;
  background:transparent;border:0;padding:0;touch-action:none}
.resizer.right{right:-3px}
.resizer.left{left:-3px}
.resizer::after{content:"";position:absolute;top:0;bottom:0;left:2px;width:1px;
  background:var(--accent);opacity:0;transition:opacity var(--dur-fast) var(--ease)}
.resizer:hover::after,.resizer.dragging::after{opacity:.55}
.resizer:focus-visible{outline:none}
.resizer:focus-visible::after{opacity:1;width:2px}
/* During a drag the cursor and selection belong to the drag, not to whatever is under it. */
html.resizing{cursor:col-resize;user-select:none}
html.resizing *{cursor:col-resize !important}
/* The manually collapsed rail: the same 56px shape the <900px media query produces, so there is
   ONE definition of "collapsed sidebar" and they cannot drift apart. */
html.railed .sidebar{width:56px;padding:var(--space-3) 6px}
/* 44px of usable width will not hold the mark and the toggle side by side, so the head stacks
   rather than hiding one of them. The toggle in particular has to stay: it is the only way back
   out of the rail, and the <900px query below hides it only because the media query owns the
   collapse there and the button would have nothing to toggle. */
html.railed .sidehead{flex-direction:column;gap:6px;padding:6px 4px 12px;margin:0 0 var(--space-3)}
html.railed .brand{justify-content:center;flex:none}
html.railed .brand .word,html.railed .sidenav .navlabel,
html.railed .footlink .navlabel{display:none}
html.railed .sidenav{padding:0}
html.railed .sidenav a,html.railed .footlink{justify-content:center;padding:7px 4px}
html.railed .sidefoot{margin:0}
/* A viewport that cannot fit a preference is not a place to express one: below the breakpoints
   the panels are already media-query-driven, so the handles go away rather than storing a width
   the layout will ignore. */
@media (max-width:900px){
  .sidebar .resizer{display:none}
  /* The media query already forces the rail here, so the toggle would rewrite flintt.layout
     without changing anything visible — and a Collapse click while media-collapsed would
     replace a stored custom width with the rail flag, losing it for wider viewports later. */
  .railtoggle{display:none}
}
/* A railed sidebar has no width to drag either, and JS refusing the gesture is not enough: a
   strip that keeps its col-resize cursor and hover line looks like a working control that does
   nothing. Hidden, exactly as the narrow path hides it. */
html.railed .sidebar .resizer{display:none}
@media (max-width:1100px){ .inspector .resizer{display:none} }
@media (prefers-reduced-motion:reduce){ .resizer::after{transition:none} }

/* ---- inspector: the ONE docked detail surface (DESIGN.md §1.2) ------------------------- */
/* 360px, a sibling of .page inside .content: it NARROWS the page, never covers it. No
   scrim, no fixed positioning — the page reflows around it. */
/* top/height subtract --demobar-h for the same reason .appframe and .sidebar do: a strip above
   the frame (demo or agent) shortens the viewport the panel is sticky inside. Pinned at 0/100vh
   the docked panel slid UNDER the strip and hung past the bottom of the window by its height. */
.inspector{flex:none;width:var(--inspector-w,360px);position:sticky;top:var(--demobar-h);
  height:calc(100vh - var(--demobar-h));z-index:3;
  display:flex;flex-direction:column;background:var(--surface-card);
  border-left:1px solid var(--border);animation:fade var(--dur-fast) var(--ease)}
/* z-index:3 is the paint-order backstop, not decoration: if page content ever overflows its
   column (a table whose min-content beats the page's), it slides UNDER this opaque surface
   instead of colliding with it — the sticky thead at z-index:2 painted its labels over the
   drawer's text before this. Everything that must float above the drawer (menus, toasts)
   lives at z-index >= 30 and is unaffected. */
.inspector .ihead{flex:none;display:flex;align-items:flex-start;gap:var(--space-2);
  padding:var(--space-4) var(--space-3) var(--space-3) var(--space-4);
  border-bottom:1px solid var(--border)}
.inspector .ihead h2{font:var(--font-weight-selected) var(--font-size-title) var(--font-sans);word-break:break-all}
/* identity case: an MPN is the row's name — mono, never truncated, never ellipsized (§5) */
.inspector .ihead h2.mono{font-family:var(--font-mono);font-size:14px;letter-spacing:-.01em}
.inspector .ihead .sub{color:var(--text-tertiary);font-size:var(--font-size-label);margin-top:2px}
.inspector .ibody{flex:1;overflow:auto;padding:var(--space-4)}
/* The wide variant: picker-style flows (the library's add-part search) whose result rows
   need more than 360px. Same docked surface, same reflow — only the width differs. */
/* The wide variant is now a STARTING width, not a second class: once the reader has dragged
   the panel their answer applies to both, because they were sizing the panel, not the flow. */
.inspector.wide{width:var(--inspector-w,600px)}
/* Below the split breakpoint there is no room to dock BESIDE the table, so the ONE detail
   surface reflows rather than disappearing: the same panel, the same content, full width
   under the table it describes (never a drawer, never a modal — §1). openInspector scrolls
   it into view there. Hiding it outright would delete part detail, alternates, identify and
   revert on every viewport under 1100px. */
@media (max-width:1100px){
  .content{flex-wrap:wrap}
  .inspector,.inspector.wide{position:static;flex:1 0 100%;width:auto;height:auto;
    border-left:none;border-top:1px solid var(--border)}
  .inspector .ibody{overflow:visible}
}

/* The inspector's supply block (§1.2 slot 4): the SHAPE of the supply answer — how many will
   sell it, how cheap, how fresh — and the way into the row expansion that answers it in full.
   Not a seller table: that lives in exactly one place, the row. */
.supply{display:flex;flex-direction:column;gap:4px}
.supply .supply-n{display:flex;align-items:baseline;gap:8px;
  font:var(--font-weight-strong) var(--font-size-title) var(--font-sans)}
.supply .supply-n .num{margin-left:auto;font-family:var(--font-mono);font-size:var(--font-size-mono);
  color:var(--text-secondary);font-weight:var(--font-weight-medium)}
.supply .btn{margin-top:6px}

/* The alternates preferred-only filter: right-aligned above the first section head, so it reads
   as acting on the list below it rather than on the panel. */
.altfilter{display:flex;justify-content:flex-end;margin:0 0 8px}

/* ---- panel bits shared by the inspector ------------------------------------------------- */
@keyframes fade{from{opacity:0}to{opacity:1}}
.xbtn{margin-left:auto;flex:none;width:30px;height:30px;border:none;background:transparent;
  color:var(--text-tertiary);border-radius:var(--radius-md);
  display:inline-flex;align-items:center;justify-content:center}
.xbtn:hover{background:var(--surface-raised);color:var(--text-primary)}
.sec{margin-bottom:var(--space-4)}
/* NOT child-scoped: `.secttl` is the app's one small-caps label, and Settings' field() and
   the login card use it outside any `.sec`. Scoping it to `.sec >` left every Settings field
   label and the "Sign in" heading rendering as plain body text. */
.secttl{font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--text-tertiary);
  margin-bottom:var(--space-2)}
.spectbl{width:100%;border-collapse:collapse;font-size:var(--font-size-label)}
.spectbl td{padding:4px 8px;border-bottom:1px solid var(--border)}
.spectbl td:first-child{color:var(--text-tertiary);width:40%}
.spectbl td:last-child{font-family:var(--font-mono);font-size:var(--font-size-mono)}
.caption{font-size:10px;color:var(--text-muted);line-height:1.5}

/* ---- candidate explanations (the alternates mode's "how does it differ" block) ------------ */
.expldiffs,.explsame{margin-top:4px;font-size:var(--font-size-label)}
.expldiffs>summary,.explsame>summary{cursor:pointer;color:var(--text-tertiary)}
.expldiffs>summary:hover,.explsame>summary:hover{color:var(--text-primary)}
.explsame{margin-left:8px}
.expltbl{margin-top:4px}
.expltbl th{padding:4px 8px;border-bottom:1px solid var(--border);text-align:left;
  font-weight:var(--font-weight-medium);color:var(--text-tertiary)}
.expltbl td:first-child{width:32%}
.expltbl td:last-child{font-family:inherit;font-size:inherit}
/* Six levels, not three. The old scale could not say which DIRECTION a difference went, so a
   50V part offered for a 16V one was painted the same amber as a 6.3V one that will fail. */
/* `blocking` and `worse` are NOT the same news and must not paint the same. One says the swap
   is physically impossible, the other says it costs margin — and once the legend names them
   separately, painting both amber promises a mapping the table cannot make. Red for the
   refusal matches the `err` verdict chip that accompanies it. */
.expltbl tr.blocking td{color:var(--error);background:var(--error-soft)}
.expltbl tr.worse td{color:var(--warning);background:var(--warning-soft)}
/* `changed` is neither a warning nor a win: the two parts differ here and nothing upstream
   ranks the difference. A LEFT RULE and a word in the legend, never a fill -- the fill was
   --surface-raised, the same tone this app uses for hover and for skeletons, so the commonest
   row in the table read as a rendering artifact. Colour would file it on the worse/blocking
   scale it is not on: it is UNKNOWN, not bad. */
/* --border-strong is ~18% alpha: a 2px rule at that weight is not a mark, it is a rounding
   error, and this is the commonest level in the table for anything that is not a passive. */
.expltbl tr.changed td:first-child{box-shadow:inset 3px 0 0 var(--text-tertiary);color:var(--text-secondary)}
.expltbl tr.better td{color:var(--success)}
.expltbl tr.missing td{color:var(--text-muted);font-style:italic}
.expltbl tr.same td{opacity:.55}
/* The legend keys carry the same mark as the row they explain, so the two read as one thing. */
.speclegend{display:flex;gap:10px;flex-wrap:wrap;align-items:baseline}
.speclegend .lgk{display:inline-flex;align-items:baseline;gap:4px}
/* flex:0 0 8px, or the swatch — the only non-text part of the key — is the first thing to
   shrink when a sentence wraps inside the 360px inspector. */
.speclegend .lgk::before{content:"";flex:0 0 8px;height:2px;border-radius:1px;background:var(--text-tertiary)}
.speclegend .lgk.blocking::before{background:var(--error)}
.speclegend .lgk.worse::before{background:var(--warning)}
.speclegend .lgk.better::before{background:var(--success)}
.speclegend .lgk.missing::before{background:var(--text-muted)}
.expltbl td.note{font-size:10px;color:var(--text-tertiary);font-family:var(--font-sans)}

/* The spec verdict: ONE quiet line — the diff expander's summary, or the bare headline
   when there is nothing to expand. No banner: the verdict chip already carries the color. */
.vline{margin-top:4px;font-size:var(--font-size-label);color:var(--text-tertiary);line-height:1.45}

/* ---- offer rows ------------------------------------------------------------------------ */
.offer{border:1px solid var(--border);border-radius:var(--radius-md);padding:10px 12px;
  background:var(--surface-card)}
.offer+.offer{margin-top:8px}
.offer.avoided{opacity:.62}
.offer .oline{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}
.seller{display:inline-flex;align-items:center;gap:6px}
.offer .seller{font-weight:var(--font-weight-selected);color:var(--text-primary)}
.offer .stock{font-family:var(--font-mono);font-size:var(--font-size-mono)}
.offer .stock.out{color:var(--text-muted)}
.breaks{width:100%;border-collapse:collapse;margin-top:6px;font-size:var(--font-size-mono);
  font-family:var(--font-mono)}
.breaks td{padding:2px 0;color:var(--text-secondary)}
.breaks td:last-child{text-align:right;color:var(--text-primary)}
/* the candidate's ONE offers disclosure: the seller grid loads on first open */
.altofd{margin-top:6px;font-size:var(--font-size-label)}
.altofd>summary{cursor:pointer;color:var(--text-tertiary)}
.altofd>summary:hover{color:var(--text-primary)}
.altof{margin-top:6px;border-top:1px solid var(--border);padding-top:2px}
/* ONE seller row, in the identity lane and inside every Offers disclosure: a fixed
   four-slot grid that never wraps — a long name ellipsizes, the facts hold their slots. */
.srow{display:grid;grid-template-columns:minmax(0,1fr) auto auto auto;align-items:center;
  gap:8px;padding:3px 0}
.srow.avoided{opacity:.62}
/* Price over its observation stamp: one cell, so the seller name keeps the row's flex track
   even in the 360px panel (§5 wants the stamp on the figure, not in a column of its own). */
.srow .srowp{display:flex;flex-direction:column;align-items:flex-end;line-height:1.3}
.srow .srowstamp{font-size:9px;color:var(--text-muted);white-space:nowrap}
/* The seller's other packagings of the same part (reel vs cut tape, different minimums):
   under its row, quieter than it, because it explains a figure rather than adding one. */
.srowalso{padding:0 0 4px 26px}
/* The quantity every price in a section is FOR, beside that section's title. */
.secttl .secnote{margin-left:6px;text-transform:none;letter-spacing:0;color:var(--text-muted)}
.srow .dslink{display:inline-flex;color:var(--text-tertiary)}
.srow .dslink:hover{color:var(--accent);text-decoration:none}
.srow .seller{display:inline-flex;align-items:center;gap:6px;min-width:0;
  font-weight:var(--font-weight-selected);color:var(--text-primary)}
.srow .sname{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.srow .stock{font-family:var(--font-mono);font-size:var(--font-size-mono);white-space:nowrap}
.srow .stock.out{color:var(--text-muted)}
.srow .nop{color:var(--text-muted);white-space:nowrap}
.srow .iconlink{white-space:nowrap}

/* ---- menus ----------------------------------------------------------------------------- */
.menu{position:absolute;right:0;top:calc(100% + 4px);z-index:35;min-width:180px;
  background:var(--surface-card);border:1px solid var(--border-strong);border-radius:var(--radius-md);
  box-shadow:var(--shadow-float);padding:4px;display:none}
.menu.open{display:block;animation:fade var(--dur-fast) var(--ease)}
.menu button{display:flex;width:100%;align-items:center;gap:8px;border:none;background:none;
  padding:7px 10px;border-radius:var(--radius-sm);font-size:var(--font-size-label);
  color:var(--text-secondary);text-align:left}
.menu button:hover{background:var(--surface-raised);color:var(--text-primary)}
.menu button.danger{color:var(--error)}

/* ---- states ------------------------------------------------------------------------------ */
.empty{padding:48px 24px;text-align:center;color:var(--text-tertiary)}
.empty .big{font-size:var(--font-size-title);color:var(--text-secondary);margin-bottom:6px}
.empty .hint{font-size:var(--font-size-label);max-width:420px;margin:0 auto 14px;line-height:1.6}
.firstrun{padding-top:64px}
.firstrun .hint{margin-bottom:0}
/* auto-fit, not auto-fill: with two paths the row must stay two even columns rather than
   leaving a phantom third track that shoves both cards left of centre. */
.firstrun .paths{display:grid;grid-template-columns:repeat(auto-fit,minmax(258px,1fr));
  gap:var(--space-3);max-width:660px;margin:24px auto 0;text-align:left}
.path{display:flex;flex-direction:column;gap:8px}
.pathhead{display:flex;align-items:center;gap:8px;color:var(--text-primary)}
.pathbody{flex:1;margin:0;color:var(--text-tertiary);
  font-size:var(--font-size-label);line-height:1.6}
.path .btn{align-self:flex-start}
.pathnote{display:flex;align-items:center;gap:6px;
  font-size:var(--font-size-label);color:var(--text-tertiary)}
.pathnote svg{animation:spin 1.6s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
/* The one destructive surface. Marked, not shouted: a red-tinted edge separates it from the
   settings above so it is never clicked on the way past, without turning the page into a
   warning. The button stays disabled until the board name is typed exactly. */
.danger-zone{border-color:var(--error-line)}
.danger-zone h3{color:var(--error)}
.danger-zone .input{max-width:260px}
.danger-zone .btn.danger:disabled{opacity:.45;cursor:not-allowed}
/* The part a pin replaced. Struck through because that is the one mark everyone already
   reads as "not this any more" without a legend. */
.wasmpn{text-decoration:line-through;text-decoration-thickness:1px;opacity:.75}
/* A command meant to be copied and pasted, not read as prose. */
.codeblock{margin:8px 0;padding:8px 10px;border:1px solid var(--border);
  border-radius:var(--radius-sm);background:var(--surface-raised);
  font:12px/1.5 var(--font-mono);color:var(--text-primary);overflow-x:auto}
.errbox{padding:24px;text-align:center;color:var(--error);font-size:var(--font-size-label)}
.infobox{padding:10px 14px;border:1px solid var(--accent-soft-line);border-radius:var(--radius-md);
  background:var(--surface-card);color:var(--text-secondary);font-size:var(--font-size-label);
  margin-bottom:var(--space-3)}
.skel{position:relative;overflow:hidden;background:var(--surface-raised);border-radius:var(--radius-md)}
.skel::after{content:"";position:absolute;inset:0;
  background:linear-gradient(90deg,transparent,var(--skel-sheen),transparent);
  animation:shimmer 1.2s infinite}
@keyframes shimmer{from{transform:translateX(-100%)}to{transform:translateX(100%)}}
.skelrow{height:44px;margin-bottom:8px}
/* Placeholder bars for the supplier cell while its offers are still in flight. Sized to the text
   they stand in for — seller name, stock line, price — so the row does not resize when the real
   values land, and the reader is not shown a dash that means "nobody stocks this".

   NO SHIMMER on these, deliberately. The table is not virtualised: it renders a row per BOM line,
   and a large board is hundreds of them, so inheriting .skel's infinite animation would mean
   three continuously compositing pseudo-elements PER ROW from first paint until the batch lands.
   A static tint carries the same "this is not a dash, something is coming" signal for no frame
   cost. The handful of full-page .skelrow placeholders keep the animation; a per-row one cannot
   afford it. */
.offerskel .skel{background:var(--surface-raised)}
.offerskel .skel::after{content:none;animation:none}
.skel-s{height:12px;width:78px}
.skel-xs{height:10px;width:56px;margin-top:4px}
.skel-m{height:14px;width:64px}
.offerskel{opacity:.75}

/* ---- toast --------------------------------------------------------------------------------- */
#toasts{position:fixed;bottom:18px;left:50%;transform:translateX(-50%);z-index:60;
  display:flex;flex-direction:column;gap:8px;align-items:center}
.toast{background:var(--surface-card);border:1px solid var(--border-strong);color:var(--text-primary);
  border-radius:var(--radius-md);padding:9px 16px;font-size:var(--font-size-label);
  box-shadow:var(--shadow-float);animation:fade var(--dur-fast) var(--ease);
  display:flex;align-items:center;gap:8px}
.toast .ic{color:var(--success)}
.toast.err{border-color:var(--error);color:var(--error)}
.toast.err .ic{color:var(--error)}
/* The one action a toast may carry (DESIGN.md §1). Styled as the page's links are
   (`a{color:var(--accent)}`, line 10) rather than as a button, so it reads as the way back
   and not as a second primary competing with the page's own. */
.toast-undo{background:none;border:0;padding:0 0 0 4px;margin-left:2px;font:inherit;
  color:var(--accent);cursor:pointer;text-decoration:underline}
.toast-undo:hover{color:var(--accent-strong)}

/* ---- page-specific -------------------------------------------------------------------------- */
.presence{display:inline-flex;align-items:center;gap:6px;font-size:var(--font-size-label);
  color:var(--text-secondary)}
.presence .pulse{width:7px;height:7px;border-radius:50%;background:var(--success)}
/* the pair page's "watching for the first sync" wait */
.presence .pulse.watchdot{animation:watchpulse 1.6s var(--ease) infinite}
@keyframes watchpulse{0%,100%{opacity:1}50%{opacity:.3}}
.chipset{display:flex;gap:6px;flex-wrap:wrap;align-items:center}

/* ---- the library table (.stbl) ---------------------------------------------------------
   Was the search page's result table; the search page is gone and the Library's own table
   inherited these. Everything that was scoped to .stwrap went with the page — that wrapper
   had exactly one renderer. The library table carries its own container query on .libtblwrap
   further down, so the drop order lives there rather than being duplicated here. */
.bomtbl.stbl tbody td,.bomtbl.stbl thead th{padding:0 8px} /* tighter gutters */
.bomtbl.stbl tbody tr.hoverable{cursor:pointer}
.bomtbl.stbl td.mpnc{white-space:nowrap} /* mono MPN never truncated */
.bomtbl.stbl td.mfrc{max-width:120px;overflow:hidden;text-overflow:ellipsis;font-size:12px}

/* ---- feedback modal (approved design H2: pen always on, no tool picker) ----
   H2 dropped the tool picker for friction, which left nothing on screen saying
   the pen was armed: a heading, a crosshair that needs a mouse AND a hover, and
   an Undo link that only appears once you have already guessed right. So the
   stage carries the affordance at rest (design C2, ~/.gstack designs
   feedback-draw-affordance-20260805): a dashed ink inset and a pinned tab, no
   timer and no first-run state, so it works on visit one and visit fifty, on
   mouse and on touch. Both recede once there is ink, so the guide never competes
   with the user's own marks. */
#fb-modal { position:fixed; inset:0; background:var(--scrim); z-index:90;
  display:flex; align-items:center; justify-content:center; }
.fb-card { background:var(--surface-card); border-radius:16px; width:min(940px, 94vw);
  max-height:92vh; display:flex; flex-direction:column; padding:16px;
  box-shadow:var(--shadow-float); }
.fb-head { display:flex; align-items:baseline; gap:10px; margin:0 2px 12px; }
.fb-head h2 { font-size:var(--font-size-title); font-weight:var(--font-weight-strong); color:var(--text-primary); }
.fb-undo { margin-left:auto; background:none; border:none; cursor:pointer; font-size:12px;
  color:var(--text-secondary); text-decoration:underline; text-underline-offset:2px;
  padding:6px 8px; }
.fb-x { background:none; border:none; cursor:pointer; line-height:1;
  color:var(--text-tertiary); padding:6px 8px;
  display:inline-flex; align-items:center; justify-content:center; }
.fb-x:hover { color:var(--text-primary); }
.fb-capturing { position:relative; overflow:hidden; height:180px;
  display:flex; align-items:center; justify-content:center; }
.fb-capturing::after { content:""; position:absolute; inset:0;
  background:linear-gradient(100deg, transparent 30%, var(--skel-sheen) 50%, transparent 70%);
  animation:fbshimmer 1.1s infinite; }
@keyframes fbshimmer { from { transform:translateX(-100%); } to { transform:translateX(100%); } }
.fb-capmsg { font-size:13px; color:var(--text-tertiary); }
.fb-stage { border:1px solid var(--border); border-radius:10px; overflow:hidden;
  min-height:120px; min-width:0; display:flex; justify-content:center; align-items:center;
  background:var(--surface-inset); }
.fb-shotbox { position:relative; max-width:100%; min-width:0; font-size:0; }
.fb-shot { display:block; max-width:100%; max-height:calc(92vh - 230px);
  max-height:calc(92dvh - 230px); width:auto; height:auto; }
.fb-ink { position:absolute; inset:0; width:100%; height:100%; cursor:crosshair;
  touch-action:none; z-index:1; }
/* The guide: this is a surface you mark, not a picture you look at. Ink-coloured
   on purpose, so the frame pre-announces what colour your marks come out. Kept
   at .28 because saturation is reserved for status (DESIGN.md §5). */
.fb-shotbox::after { content:""; position:absolute; inset:5px; z-index:0;
  pointer-events:none; border:2px dashed var(--ink); border-radius:6px;
  opacity:.28; transition:opacity var(--dur-fast) ease-out; }
.fb-tab { position:absolute; left:9px; bottom:9px; z-index:2; pointer-events:none;
  display:inline-flex; align-items:center; gap:5px;
  background:var(--ink); color:#fff; font-size:11px; font-weight:var(--font-weight-medium);
  padding:4px 9px; border-radius:var(--radius-pill); box-shadow:var(--shadow-pop);
  transition:opacity var(--dur-fast) ease-out; }
.fb-tab svg { width:12px; height:12px; }
/* Ink on the page means the message landed: the guide gets out of the way. Tied
   to the stroke count, so undoing back to an empty canvas brings it back. */
.fb-shotbox.inked::after { opacity:.1; }
.fb-shotbox.inked .fb-tab { opacity:0; }
@media (prefers-reduced-motion:reduce) {
  .fb-shotbox::after, .fb-tab { transition:none; }
}
.fb-noshot { display:flex; align-items:center; justify-content:center; height:160px;
  color:var(--text-tertiary); font-size:13px; }
.fb-foot { display:flex; gap:12px; margin-top:12px; align-items:flex-end; }
.fb-fieldwrap { flex:1; }
.fb-field { width:100%; border:1px solid var(--border-strong); border-radius:10px;
  padding:10px 12px; font-size:13.5px; font-family:var(--font-sans); background:var(--surface-card);
  color:var(--text-primary); resize:none; overflow:hidden; line-height:1.5; outline:none; }
.fb-field:focus { border-color:var(--focus-ring); box-shadow:0 0 0 2px var(--accent-soft-line); }
.fb-cap { font-size:11px; color:var(--text-tertiary); margin:7px 0 0 2px; }
/* It only carries text when the capture failed; otherwise it must not leave a gap. */
.fb-cap:empty { display:none; }
.fb-send { background:var(--text-primary); color:var(--text-on-accent); border:none; border-radius:10px;
  padding:12px 28px; font-size:13.5px; font-weight:var(--font-weight-strong); cursor:pointer; margin-bottom:24px; }
.fb-send:disabled { opacity:.7; }
@media (max-width:640px){
  #fb-modal { align-items:stretch; }
  .fb-card { width:100%; max-height:none; height:100dvh; border-radius:0;
    padding:calc(12px + env(safe-area-inset-top, 0px)) 12px calc(12px + env(safe-area-inset-bottom, 0px)); }
  .fb-stage { min-height:0; }
  .fb-shot { max-height:46dvh; }
  .fb-field { font-size:16px; }
  .fb-foot { align-items:stretch; flex-direction:column; }
  .fb-send { margin-bottom:0; padding:14px 0; }
}

/* ---- board page: TWO bands, then data (DESIGN.md §3) -------------------------------------- */
/* No .page width override here — one frame law (§2). Detail docks in .inspector (§1.2), so
   there is no page-local panel and no split wrapper: the shared inspector narrows .page. */

/* band 1 (56px) — back · title (inline-editable where relevant) · meta chips · the
   query bar | right: the page's ONE primary action. Every page builds it from ui.js band1() */
/* Band 1 NEVER wraps: a wrapped band is ~112px of chrome, which turns a two-band page into
   a de-facto three-band one — exactly the budget §3 sets. The title (and the meta strip) are
   the elastic items and ellipsize; the controls keep their size. */
.band1{display:flex;align-items:center;gap:var(--space-3);min-height:56px;flex-wrap:nowrap}
.band1 .spacer{flex:1}
.band1>.b1title{flex:0 1 auto;min-width:0}
.band1>.bmeta,.band1>.segc,.band1>.qtyfield,.band1>.btn,.band1>a.btn,.band1>label{flex:none}
.band1>.bmeta{min-width:0} /* chips still ellipsize inside via .mchip's max-width */
.b1title{display:flex;flex-direction:column;min-width:0}
/* ONE band-1 title type, whether the page renders a plain h1 or the board's editable name */
.b1title h1,.bname{font:var(--font-weight-selected) 20px var(--font-sans);letter-spacing:-.01em;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.b1title .sub{color:var(--text-tertiary);font-size:var(--font-size-label);margin-top:2px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bnamewrap{display:flex;align-items:center;gap:2px;min-width:0}
.bname{cursor:text;max-width:260px}
.bnameinput{font:var(--font-weight-selected) 18px var(--font-sans);padding:4px 8px;width:260px}
.bmeta{display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0}
.mchip{display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 8px;max-width:220px;
  border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface-card);
  font-size:var(--font-size-label);color:var(--text-tertiary);white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
a.mchip:hover{color:var(--text-primary);border-color:var(--border-strong);text-decoration:none}
.mchip .ic{color:var(--text-muted)}
.mchip .pulse{width:6px;height:6px;border-radius:50%;background:var(--success);flex:none}
.qtyfield{display:inline-flex;align-items:center;gap:6px;font-size:var(--font-size-label);
  color:var(--text-tertiary);white-space:nowrap}
/* Compact build-qty stepper: tab-bar height, spinner-free input between −/+ buttons. */
.qstep{display:inline-flex;align-items:center;height:28px;border:1px solid var(--border-strong);
  border-radius:var(--radius-md);background:var(--surface-card);overflow:hidden}
.qstep .qbtn{border:none;background:none;width:24px;height:26px;padding:0;line-height:1;
  color:var(--text-tertiary);font-size:13px;cursor:pointer}
.qstep .qbtn:hover{color:var(--text-primary);background:var(--surface-raised)}
.qstep .qtynum{width:38px;border:none;background:none;padding:0;text-align:center;outline:none;
  font-size:var(--font-size-label);color:var(--text-primary)}
.qstep .qtynum::-webkit-outer-spin-button,.qstep .qtynum::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
.qstep .qtynum[type=number]{-moz-appearance:textfield;appearance:textfield}

/* The Supplier cell is the ONE expansion toggle: the whole cell reads as a button. */
.bomtbl td.offc.xto{cursor:pointer}
.bomtbl tbody tr.line:hover td.offc.xto{background:var(--surface-inset)}

/* Text tab bar (the board page): left-aligned link tabs under the title row, the active
   one underlined, the tab's right-side actions pinned to the row's right edge. */
.tabs{display:flex;align-items:center;gap:14px;min-height:36px;margin-bottom:var(--space-3);
  border-bottom:1px solid var(--border)}
.tabs a{padding:8px 2px;font-size:var(--font-size-label);color:var(--text-tertiary);
  border-bottom:2px solid transparent;margin-bottom:-1px}
.tabs a:hover{color:var(--text-primary);text-decoration:none}
.tabs a.on{color:var(--text-primary);font-weight:var(--font-weight-strong);
  border-bottom-color:var(--accent)}
.tabs .spacer{flex:1}
.tabs>.btn,.tabs>a.btn,.tabs>.qtyfield,.tabs>label{flex:none}

/* band 2 (44px) — the health summary IS the filter row; neutral chips only (§6) */
.band2{display:flex;align-items:center;gap:8px;min-height:44px;flex-wrap:wrap}
.band2 .spacer{flex:1}
.band2 select.input{padding:5px 8px}
/* Search-within: deliberately minor next to the bucket chips. */
.band2 .rowfilter{width:150px;padding:5px 9px;font-size:12px}
/* the ONE filter chip: neutral, toggles, carries its count; `.x` makes it a removable
   applied filter (query tokens, facet selections, supplier lists — all the same chip) */
.fchip{display:inline-flex;align-items:center;gap:7px;height:26px;padding:0 11px;border-radius:999px;
  border:1px solid var(--border);background:var(--surface-card);color:var(--text-secondary);
  font:inherit;font-size:var(--font-size-label);cursor:pointer}
.fchip:hover:not(:disabled){border-color:var(--border-strong);color:var(--text-primary)}
.fchip.on{background:var(--accent-soft);border-color:var(--accent-soft-line);color:var(--text-primary)}
.fchip .n{font-family:var(--font-mono);font-variant-numeric:tabular-nums;color:var(--text-primary)}
.fchip .kind{color:var(--text-tertiary)}
.fchip b{font-weight:var(--font-weight-medium)}
.fchip:disabled{opacity:.45;cursor:default}
.fchip:disabled .n{color:var(--text-muted)}
.fchip:has(.x){padding-right:4px;cursor:default}
.fchip .x{border:none;background:none;color:var(--text-muted);width:16px;height:16px;
  border-radius:6px;padding:0;cursor:pointer;line-height:1;
  display:inline-flex;align-items:center;justify-content:center}
.fchip .x:hover{background:var(--surface-raised);color:var(--text-primary)}
/* the inline fix action an attention row carries in the filtered state */

/* dense 38px table, 1px separators, no zebra, status pinned right */
.bomcard{background:var(--surface-card);border:1px solid var(--border);
  border-radius:var(--radius-md);overflow:hidden}
/* The table scrolls, not the page under it, so the header can stay put.
   `position:sticky` resolves against the nearest SCROLLPORT, and .bomwrap is already one
   (overflow-x:auto makes overflow-y compute to auto). Sticking the header to the viewport
   was therefore never going to work from in here; bounding the wrap's height so it owns the
   vertical scroll is what makes the header hold. The footer totals sit outside it and stay
   visible too. --bomtop is the chrome above the table (bands + tabs + page padding). */
.bomwrap{overflow:auto;max-height:calc(100vh - var(--bomtop,232px))}
table.bomtbl{width:100%;border-collapse:separate;border-spacing:0;
  font-size:var(--font-size-label);font-variant-numeric:tabular-nums}
.bomtbl thead th{position:sticky;top:0;z-index:2;
  height:30px;padding:0 10px;text-align:left;font-size:10px;
  font-weight:var(--font-weight-medium);letter-spacing:.06em;text-transform:uppercase;
  color:var(--text-muted);background:var(--surface-page);
  border-bottom:1px solid var(--border-strong);white-space:nowrap}
/* border-collapse:separate is required for a sticky header (a collapsed table's borders
   belong to the table, not the cell, and scroll away with it) — so the row separator that
   used to be border-top moves to a box-shadow, which does not need a border box. */
.bomtbl tbody td{box-shadow:inset 0 1px 0 var(--border)}
.bomtbl tbody tr:first-child td{box-shadow:none}
@media (max-width:1100px){
  /* The inspector reflows under the table here, so a bounded table would trap it below a
     scroll area. Let the page scroll instead; the header gives up stickiness with it. */
  .bomwrap{max-height:none}
  .bomtbl thead th{position:static}
}
.bomtbl th.num,.bomtbl td.num{text-align:right}
.bomtbl td.num{font-family:var(--font-mono);font-size:var(--font-size-mono)}
.bomtbl tbody td{height:38px;padding:0 10px;
  vertical-align:middle;white-space:nowrap}
/* density control (band 2): 34 dense · 38 default · 44 comfortable (§6) */
.bomtbl.dense tbody td{height:34px}
.bomtbl.roomy tbody td{height:44px}
.bomtbl tbody tr.line{cursor:pointer}
.bomtbl tbody tr.line:hover td{background:var(--surface-raised)}
.bomtbl tbody tr.sel td{background:var(--accent-soft)}
.bomtbl tr.dim td{opacity:.5}
/* the DNP group header: a quiet divider row between the orderable lines and the
   not-populated ones sunk to the table bottom (no hover, no status column accent) */
.bomtbl tbody tr.dnpsep td{height:26px;padding:0 10px;border-top:1px solid var(--border-strong);
  background:var(--surface-inset);font-size:9.5px;letter-spacing:.07em;text-transform:uppercase;
  color:var(--text-muted);font-weight:var(--font-weight-medium);cursor:default}
.bomtbl td.ckc,.bomtbl th.ckc{width:26px;padding:0 0 0 8px}
.rowck{opacity:0;width:13px;height:13px;accent-color:var(--accent);cursor:pointer;margin:0;
  transition:opacity var(--dur-fast) var(--ease)}
.bomtbl tr.line:hover .rowck,.rowck:checked,.rowck:focus-visible{opacity:1}
.xchev{border:none;background:none;color:var(--text-muted);width:16px;height:16px;
  padding:0;cursor:pointer;margin-right:2px;
  display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}
.xchev:hover{color:var(--text-primary)}
.bomtbl td.refc{font-family:var(--font-mono);font-size:var(--font-size-mono);
  max-width:130px;overflow:hidden;text-overflow:ellipsis}
.bomtbl td.qtyc{text-align:right;color:var(--text-secondary);
  font-family:var(--font-mono);font-size:var(--font-size-mono)}
.bomtbl td.valc{max-width:150px;overflow:hidden;text-overflow:ellipsis;font-size:12px}
.bomtbl td.fpc{font-family:var(--font-mono);font-size:10.5px;color:var(--text-muted);
  max-width:150px;overflow:hidden;text-overflow:ellipsis}
.bomtbl td.mpnc{font-family:var(--font-mono);font-size:var(--font-size-mono)} /* never truncated */
.bomtbl td.mpnc .mpnlink{font-size:inherit}
.bomtbl td.stcell,.bomtbl th.stcell{position:sticky;right:0;background:var(--surface-card);
  border-left:1px solid var(--border);z-index:1}
.bomtbl th.stcell{background:var(--surface-page)}
.bomtbl tr.line:hover td.stcell{background:var(--surface-raised)}
.bomtbl tr.sel td.stcell{background:var(--accent-soft)}
/* ---- the library table: recognition first ---------------------------------------------------
   The row used to lead with the MPN in mono at full size, with the human label small and grey
   underneath. That is backwards for the way a part gets found: an engineer reaches for "the 4.7k
   0805", never for ESR10EZPJ472, and the order code is what you copy AFTER you have found the
   row. So value leads, package sits beside it, and the MPN keeps a column of its own. */
.libtbl td.clsgut,.libtbl th.clsgut{width:26px;padding-right:0;text-align:center;
  font-family:var(--font-mono);font-size:9.5px;color:var(--text-muted);
  letter-spacing:.02em;user-select:none}
.libtbl td.valc{max-width:none;font-size:13px;font-weight:var(--font-weight-medium)}
.vallink{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer;
  text-align:left}
.vallink:hover{text-decoration:underline;text-decoration-color:var(--text-tertiary)}
.libtbl td.pkgc{font-family:var(--font-mono);font-size:11px;color:var(--text-secondary)}
.libtbl td.mpnc{color:var(--text-tertiary);font-size:11.5px}
/* Drawn only when a part is NOT placeable, so the mark carries meaning by being rare. */
.stmark{display:inline-flex;vertical-align:middle;margin-left:7px}
/* The classes partStatus actually produces: `pending` while a drawing is being fetched, `nodata`
   when there is none. There is no `warn` and no `none` — styling those meant the mark rendered
   unstyled for every real case, which is the whole colour it was meant to carry. */
.stmark.pending{color:var(--text-tertiary)}
.stmark.nodata{color:var(--warning)}

/* Group headers ride inside the tbody so every group shares one set of column widths. */
.libtbl tr.grouprow td.grouphead{padding:14px 10px 5px;border-bottom:1px solid var(--border)}
.libtbl tr.grouprow:first-child td.grouphead{padding-top:2px}
.groupname{font-size:10.5px;font-weight:var(--font-weight-strong);letter-spacing:.07em;
  text-transform:uppercase;color:var(--text-tertiary)}
.groupcount{margin-left:7px;font-family:var(--font-mono);font-size:10px;color:var(--text-muted)}

.stbadge{display:inline-flex;align-items:center;gap:6px;font-size:11px;
  font-weight:var(--font-weight-medium)}
.stbadge svg{display:block;flex:none}
.stbadge.ok{color:var(--success)}
/* A failed check on data we trust: the user's problem, so it gets the warning colour. */
.stbadge.risk{color:var(--warning)}
.stbadge.unsourced{color:var(--error)}
/* "No data" and "Stale" are OUR problem and a refresh respectively, not findings about the
   board — so they read muted. Alarm colour on a line we simply could not check is how a status
   column teaches people to ignore it. */
.stbadge.nodata{color:var(--text-muted)}
.stbadge.stale{color:var(--text-secondary)}
.stbadge.dnp{color:var(--text-muted)}
/* .stbadge.pending is gone: a pending change is not a health level and no longer competes
   for this column. It renders on the MPN cell as .pendl below. */

/* ---- pending change: the marker, the fact and the way out, on ONE line ------------------ */
/* Rides UNDER the MPN (same slot as .subl), never beside it: the MPN is the row's identity
   (DESIGN.md §5), and a control on that line widens the column and competes with the thing
   it is attached to. */
.pendl{display:flex;align-items:center;gap:5px;margin-top:1px;font-family:var(--font-sans);
  font-size:10px;line-height:1.4;color:var(--text-tertiary)}
.pendl .ic{flex:none;color:var(--text-muted)}
.pendl .pf{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pendl .wasmpn{font-family:var(--font-mono)}
/* NEUTRAL, not amber. Amber means "a check failed on data we trust" and nothing else; a
   pending change is a decision the user made, and dressing it as a fault is exactly how the
   old one-amber-word-for-three-states column taught people to stop reading. */
.acceptb{height:17px;padding:0 7px;margin-left:2px;border:1px solid var(--border);
  border-radius:999px;background:var(--surface-card);color:var(--text-secondary);
  font:inherit;font-family:var(--font-sans);font-size:9.5px;
  font-weight:var(--font-weight-medium);line-height:1;cursor:pointer;flex:none}
.acceptb:hover{border-color:var(--border-strong);color:var(--text-primary);
  background:var(--surface-raised)}
.acceptb:disabled{opacity:.5;cursor:default}
/* Visible at rest — NOT summoned by hover. A hover-gated affordance does not exist on touch
   and is easy to miss with a mouse; this codebase already shipped that mistake once, on the
   feedback modal's pen, and had to undo it. */

/* ---- the shared confirm dialog (core/ui.js confirmDialog) ------------------------------ */
/* DESIGN.md §1.3 permits a modal for confirmation and for capture that must complete, and
   for nothing else. Narrow on purpose: this asks one question and gets out of the way, so
   it is nowhere near the feedback modal's 940px working surface. */
.cdscrim{position:fixed;inset:0;z-index:95;background:var(--scrim);
  display:flex;align-items:center;justify-content:center;padding:20px;
  animation:fade var(--dur-fast) var(--ease)}
.cdcard{width:min(440px,100%);padding:18px;border-radius:var(--radius-lg);
  background:var(--surface-card);box-shadow:var(--shadow-float)}
.cdcard h2{font:var(--font-weight-selected) var(--font-size-title) var(--font-sans);
  margin-bottom:9px}
.cdbody{font-size:12.5px;line-height:1.55;color:var(--text-secondary)}
.cdbody p+p{margin-top:8px}
.cdbody .mono{font-family:var(--font-mono);color:var(--text-primary)}
.cdfoot{display:flex;align-items:center;gap:8px;margin-top:16px}
.cdagain{display:inline-flex;align-items:center;gap:6px;font-size:11px;
  color:var(--text-tertiary);cursor:pointer;user-select:none}
.cdagain input{margin:0;cursor:pointer}
@media (max-width:520px){
  .cdfoot{flex-wrap:wrap}
  .cdagain{order:3;flex-basis:100%;margin-top:4px}
}

/* A candidate whose land does not match the line's: the Use gesture is withdrawn, and this
   says why in both directions. Amber, unlike .pendl — this one IS a finding: the part will
   not physically sit on the board. */
.fitno{display:flex;align-items:flex-start;gap:6px;margin-top:6px;padding:6px 8px;
  border:1px solid var(--warning-line);border-radius:var(--radius-sm);
  background:var(--warning-soft);color:var(--warning);font-size:10.5px;line-height:1.45}
.fitno .ic{flex:none;margin-top:1px}

/* The inspector's reconciliation block: both parts named side by side, then the two exits. */
.recon{padding:11px;border:1px solid var(--border);border-radius:var(--radius-md);
  background:var(--surface-inset)}
.recon .rl{display:flex;justify-content:space-between;gap:10px;align-items:baseline;
  font-size:11.5px;padding:2px 0}
.recon .rk{color:var(--text-tertiary)}
.recon .rv{font-size:11.5px}
.recon .rnote{margin:9px 0 11px;font-size:10.5px;color:var(--text-tertiary);line-height:1.5}
.recon .btn.wide{width:100%;justify-content:center}
.recon .btn.wide+.btn.wide{margin-top:5px}

/* best-offer cell: seller + stock left, price @qty + honest as-of stamp right */
.offer2{display:flex;align-items:center;justify-content:space-between;gap:10px;min-width:190px}
.offer2 .os{min-width:0}
/* The seller line carries a name plus up to two markers (preferred / not preferred / avoid /
   pinned). Held on one nowrap line they overflowed the column and the marker — the one thing
   here you cannot afford to miss — was what got clipped. Wrapping is the right give: the row
   is 38px and a wrapped marker still fits inside it. */
.offer2 .os .s1{font-size:12px;display:flex;align-items:center;gap:5px;line-height:1.3;
  flex-wrap:wrap;row-gap:1px}
.offer2 .os .s1 .chip{flex:none}
.offer2 .os .s2{font-size:10px;color:var(--text-muted);line-height:1.3;font-variant-numeric:tabular-nums}
.offer2 .op{text-align:right;flex:none}
.offer2 .op .p{font-family:var(--font-mono);font-size:12px;font-weight:var(--font-weight-strong);line-height:1.3}
.offer2 .op .p small{font-family:var(--font-sans);font-size:9px;font-weight:400;
  color:var(--text-muted);margin-left:2px}
.offer2 .op .ts{font-size:9px;color:var(--text-muted);line-height:1.3}
/* The per-line expansion affordance: a quiet inline ghost action that rides the stock
   line, so the labeled "Sellers · N" costs no row height. */
.offc .xoff{border:none;background:transparent;padding:1px 5px;margin-left:5px;
  border-radius:var(--radius-sm);font-size:10px;color:var(--text-tertiary);cursor:pointer;
  white-space:nowrap}
.offc .xoff:hover{background:var(--surface-raised);color:var(--text-primary)}

/* inline seller expansion (price-break mini-table) */
.bomtbl tr.xrow>td{padding:0;background:var(--surface-inset);border-top:1px solid var(--border);
  height:auto;white-space:normal}
.xp{padding:10px 16px 12px 44px}
.xp-head{display:flex;justify-content:space-between;align-items:baseline;gap:12px;margin-bottom:6px}
.xp-cap{font-size:10px;letter-spacing:.07em;text-transform:uppercase;color:var(--text-muted);
  font-weight:var(--font-weight-medium)}
.xp-asof{font-size:10px;color:var(--text-muted)}
table.xt{border-collapse:collapse;min-width:560px}
.xt th{height:22px;padding:0 10px;font-size:9.5px;letter-spacing:.06em;text-transform:uppercase;
  color:var(--text-muted);font-weight:var(--font-weight-medium);text-align:right;
  border-bottom:1px solid var(--border);white-space:nowrap}
.xt th:first-child{text-align:left;padding-left:0}
.xt td{height:26px;padding:0 10px;font-size:11px;border-bottom:1px solid var(--border);
  text-align:right;font-family:var(--font-mono);font-variant-numeric:tabular-nums;
  color:var(--text-secondary);white-space:nowrap}
.xt tr:last-child td{border-bottom:none}
.xt td.xseller{text-align:left;font-family:var(--font-sans);font-size:12px;
  color:var(--text-primary);font-weight:var(--font-weight-medium);padding-left:0}
.xt td.xseller .sellerlogo{margin-right:6px}
.xt td.xbest{color:var(--text-primary);font-weight:var(--font-weight-strong)}
/* A seller who cannot fill this line at the needed quantity: still listed, never hidden,
   but visibly not an option. The reason rides the seller cell. */
.xt tr.xdim td{opacity:.55}
/* Sole-source exception rows (avoid-listed but the only offers): kept, and dimmed like
   the inspector's .offer.avoided so the state reads at a glance. */
.xt tr.avoided td{opacity:.62}

/* ---- Sourcing plan -----------------------------------------------------------------------
   The plans rail left (Yours, the live system presets, the saved forks), then the supplier
   rail + interactive line table inside .planboard. Active plan name, Spares, totals and the
   risk strip ride the header row; sourcing problems are one expandable strip under it. */
.planwrap{padding:12px 16px 24px;display:flex;flex-direction:column;gap:14px}
.planhead{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap}
.planhead .spacer{flex:1}
.planname{display:flex;align-items:center;gap:4px;min-width:0}
.planname .pn-name{font-size:var(--font-size-body);font-weight:var(--font-weight-strong);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer}
.planname .input{width:200px;padding:3px 8px}
.spares{display:inline-flex;align-items:center;gap:6px;font-size:var(--font-size-label);
  color:var(--text-tertiary)}
.spares select.input{width:auto;padding:3px 6px}
.plantotals{display:flex;gap:22px;flex-wrap:wrap;align-items:baseline;font-family:var(--font-mono)}
.plantotals .pt-total{font-weight:var(--font-weight-strong)}
.planwarn{max-width:70ch}
/* the problems strip: one compact warn row (count + reason chips) that expands in place */
.planprobswrap{display:flex;flex-direction:column;gap:6px}
.planprobs{display:flex;align-items:center;gap:8px;width:100%;text-align:left;
  padding:8px 12px;border:1px solid var(--warning-line);border-radius:var(--radius-md);
  background:var(--warning-soft);color:var(--warning);font:inherit;
  font-size:var(--font-size-label);cursor:pointer}
.planprobs:hover{border-color:var(--warning)}
.planprobs .pp-count{font-weight:var(--font-weight-strong)}
.planprobs>svg:last-child{margin-left:auto;flex:none}
.planprobs-detail{border:1px solid var(--border);border-radius:var(--radius-md);
  background:var(--surface-card);padding:8px 12px;display:flex;flex-direction:column;gap:4px}
.pp-row{display:flex;gap:12px;align-items:baseline;font-size:var(--font-size-label)}
.pp-row .plpart{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pp-row .pp-need{width:60px;flex:none;text-align:right;font-family:var(--font-mono);
  font-variant-numeric:tabular-nums}
.planbody{display:grid;grid-template-columns:230px minmax(0,1fr);gap:14px;align-items:start}
@media (max-width:1200px){.planbody{grid-template-columns:1fr}}
.planboard{display:grid;grid-template-columns:290px minmax(0,1fr);gap:14px;align-items:start}
@media (max-width:1000px){.planboard{grid-template-columns:1fr}}
.planrail{display:flex;flex-direction:column;gap:10px;min-width:0}
/* a plan card: the whole face switches, the fork's delete rides the top-right corner */
.plancard{position:relative;border:1px solid var(--border);border-radius:var(--radius-md);
  background:var(--surface-card)}
.plancard:hover{border-color:var(--border-strong)}
.plancard.on{border-color:var(--accent-soft-line);box-shadow:inset 0 0 0 1px var(--accent-soft-line)}
.pc-main{display:flex;flex-direction:column;gap:3px;width:100%;padding:10px 12px;border:none;
  background:none;font:inherit;color:inherit;text-align:left;cursor:pointer;
  border-radius:var(--radius-md)}
.pc-name{font-weight:var(--font-weight-strong);overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap;padding-right:20px}
.pc-sub{font-size:10px;color:var(--text-muted);font-variant-numeric:tabular-nums}
.pc-del{position:absolute;top:6px;right:6px;width:22px;height:22px;padding:0;border:none;
  background:none;color:var(--text-muted);cursor:pointer;border-radius:var(--radius-sm);
  display:inline-flex;align-items:center;justify-content:center}
.pc-del:hover{color:var(--error);background:var(--surface-raised)}
/* per-seller monogram tile: the ONE .mtile rule lives with .sellerlogo above; the tile's
   hue is the seller's canonical slot (inline --h), the one place per-seller colors exist */
.supcard{display:flex;flex-direction:column;gap:6px;text-align:left;padding:12px 14px;
  border:1px solid var(--border);border-radius:var(--radius-md);background:var(--surface-card);
  cursor:pointer;font:inherit;color:inherit}
.supcard:hover{border-color:var(--border-strong)}
.supcard.on{border-color:var(--accent-soft-line);box-shadow:inset 0 0 0 1px var(--accent-soft-line)}
.supcard .mtile{width:22px;height:22px;font-size:11px}
.sc-top{display:flex;align-items:center;gap:8px;min-width:0}
.sc-name{font-weight:var(--font-weight-strong);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sc-row{display:flex;justify-content:space-between;gap:10px;font-size:var(--font-size-label)}
.sc-row .k{color:var(--text-tertiary)}
.sc-row .v{font-family:var(--font-mono);font-variant-numeric:tabular-nums}
.sc-total{display:flex;justify-content:space-between;gap:10px;font-size:var(--font-size-label);
  border-top:1px solid var(--border);padding-top:6px;font-weight:var(--font-weight-strong)}
.sc-total .v{font-family:var(--font-mono);font-variant-numeric:tabular-nums}
/* the line table: same .bomtbl look as every other table in the app; fixed layout so the
   supplier cell keeps the room its dropdown button needs */
.bomtbl.planlines{table-layout:fixed}
.bomtbl.planlines .w-n{width:70px}
.bomtbl.planlines .w-b{width:150px}
.bomtbl.planlines .w-s{width:110px}
.bomtbl.planlines .w-u{width:90px}
.bomtbl.planlines .w-c{width:90px}
.bomtbl.planlines .sellc{width:250px}
.bomtbl.planlines td{overflow:hidden}
.stockcell{display:inline-flex;align-items:center;gap:5px;justify-content:flex-end}
.stockwarn{color:var(--warning)}
.riskstrip{display:flex;gap:10px;align-items:center;font-size:var(--font-size-label)}
.riskbit{color:var(--warning);font-weight:var(--font-weight-strong);cursor:default;
  border-bottom:1px dotted currentColor}
.plpart{font-family:var(--font-mono);font-size:var(--font-size-mono)}
.plrefs{display:inline-block;max-width:140px;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap;vertical-align:bottom;font-size:10px;margin-left:6px}
.buycell{display:inline-flex;flex-direction:column;gap:1px}
.buytop{display:inline-flex;align-items:baseline;gap:4px}
.buycap{font-size:9px;white-space:nowrap}
.buyin{width:64px;text-align:right;padding:2px 6px}
.buyin.ov{border-color:var(--accent-soft-line)}
.bomtbl tbody tr.hl td{background:var(--accent-soft)}
/* the supplier cell: a quiet button that opens the line's seller dropdown */
.sellpick{display:inline-flex;align-items:center;gap:6px;max-width:100%;margin:-3px -6px;
  padding:3px 6px;background:transparent;border:1px solid transparent;border-radius:var(--radius-sm);
  cursor:pointer;font:inherit;color:var(--text-primary)}
.sellpick:hover{background:var(--surface-raised)}
.sellpick .sp-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sellpick .ic{color:var(--text-muted)}
/* no seller yet (a Yours row without a pick): the same dropdown affordance, muted */
.sellpick.sellnone{color:var(--text-muted)}
.sellpick.sellnone:hover{color:var(--text-primary)}
.sellstatic{display:inline-flex;align-items:center;gap:6px}
/* the seller dropdown itself: fixed so no table wrapper can clip it */
/* The price ladder: a seller's real quantity breaks, from any price figure in the app. Fixed and
   pointer-transparent to nothing — you must be able to move onto it and read it. */
.ladderpop{position:fixed;z-index:40;background:var(--surface-card);
  border:1px solid var(--border-strong);border-radius:var(--radius-md);
  box-shadow:var(--shadow-float);padding:8px 10px;min-width:220px;
  animation:fade var(--dur-fast) var(--ease)}
.laddertitle{font-size:10px;letter-spacing:.08em;text-transform:uppercase;
  color:var(--text-tertiary);margin-bottom:6px}
.ladder{border-collapse:collapse;font-size:var(--font-size-label);width:100%}
.ladder th{font-weight:var(--font-weight-normal);color:var(--text-tertiary);
  padding:0 0 4px;text-align:left}
.ladder td{padding:2px 0;color:var(--text-secondary)}
.ladder th.num,.ladder td.num{text-align:right;font-variant-numeric:tabular-nums;
  padding-left:14px}
/* The break this order actually lands on. Only ever marked when a real quantity is known. */
.ladder tr.on td{color:var(--text-primary);font-weight:var(--font-weight-strong)}
.ladderpop .caption{margin-top:6px}
/* A price figure that has a ladder behind it. Underlined on hover only: a dotted underline on
   every price in a dense table is noise, and the figure must stay a figure first. */
.hasladder{cursor:help;border-radius:3px}
.hasladder:hover,.hasladder:focus-visible{text-decoration:underline dotted;
  text-underline-offset:2px;outline:none}
.sellmenu{position:fixed;z-index:35;background:var(--surface-card);border:1px solid var(--border-strong);
  border-radius:var(--radius-md);box-shadow:var(--shadow-float);padding:4px;overflow-y:auto;
  animation:fade var(--dur-fast) var(--ease)}
.sellopt{display:flex;align-items:center;gap:8px;width:100%;border:none;background:none;
  padding:6px 8px;border-radius:var(--radius-sm);font-size:var(--font-size-label);
  color:var(--text-secondary);text-align:left;cursor:pointer}
.sellopt:hover{background:var(--surface-raised);color:var(--text-primary)}
/* menu items that are anchors (the part view's "Open at" menu): never underlined */
a.sellopt,a.sellopt:hover{text-decoration:none}
.sellopt .so-check{width:16px;flex:none;display:inline-flex;justify-content:center;color:var(--success)}
.sellopt .so-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap;color:var(--text-primary)}
.sellopt .chip{flex:none}
.sellopt .so-lead{width:36px;flex:none;text-align:right;color:var(--text-muted);
  font-family:var(--font-mono);font-variant-numeric:tabular-nums;font-size:10px}
.sellopt .so-price{flex:none;display:flex;gap:10px;font-family:var(--font-mono);
  font-variant-numeric:tabular-nums}
.sellopt .so-unit{color:var(--text-tertiary)}
.sellopt .so-cost{color:var(--text-primary);font-weight:var(--font-weight-medium)}
/* the table's totals row: parts with and without minimum-order excess */
.bomtbl.planlines tfoot td{padding:8px 10px;border-top:1px solid var(--border-strong);
  font-family:var(--font-mono);font-size:var(--font-size-mono);font-variant-numeric:tabular-nums;
  color:var(--text-secondary);white-space:nowrap}
.bomtbl.planlines tfoot td.num{text-align:right}
.bomtbl.planlines tfoot td:first-child{font-family:var(--font-sans);font-size:var(--font-size-label);
  color:var(--text-tertiary)}

/* table footer strip */
.bomfoot{display:flex;justify-content:space-between;align-items:center;gap:12px;min-height:32px;
  padding:6px 12px;border-top:1px solid var(--border);background:var(--surface-page);
  font-size:var(--font-size-label);color:var(--text-muted);flex-wrap:wrap}
.bomfoot b{color:var(--text-secondary);font-weight:var(--font-weight-medium)}


/* inspector body vocabulary (identity · status · facts · offers · actions) — the panel
   chrome itself lives with .inspector above; these are the blocks pages paint into it */
.isub{font-size:var(--font-size-label);color:var(--text-tertiary);margin-top:2px}
.istatus{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px}
.istatus .stbadge{font-size:12px}
.idesc{font-size:var(--font-size-label);color:var(--text-tertiary);line-height:1.5;margin-top:10px}
.iact{display:flex;flex-direction:column;gap:8px;margin-top:var(--space-4)}
.centerlink{justify-content:center}
/* A state sitting where the actions are ("In library"): centered like the action links,
   quiet, never clickable. */
.iact .inlib{display:flex;align-items:center;justify-content:center;gap:6px;
  color:var(--text-tertiary);font-size:var(--font-size-label)}
.ibackbtn{margin:-4px 0 8px -6px}

/* floating bulk-selection bar */
.bulkbar{position:fixed;bottom:18px;left:50%;transform:translateX(-50%);z-index:30;height:40px;
  display:flex;align-items:center;gap:10px;padding:0 8px 0 14px;background:var(--surface-tool);
  color:var(--text-on-tool);border-radius:var(--radius-md);box-shadow:var(--shadow-pop);
  white-space:nowrap;animation:fade var(--dur-fast) var(--ease)}
.bulkbar .cnt{font-size:12px;font-weight:var(--font-weight-strong)}
.bulkbar .vsep{width:1px;height:16px;background:var(--text-on-tool);opacity:.2}
.bulkbar button{background:none;border:none;color:var(--text-on-tool);font-size:12px;
  padding:5px 8px;border-radius:6px;cursor:pointer;opacity:.85;
  display:inline-flex;align-items:center;gap:6px}
.bulkbar button:hover{opacity:1}



/* login */
.loginwrap{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.logincard{width:300px}
.loginmark{display:flex;justify-content:center;margin-bottom:8px;color:var(--text-primary)}
.loginword{text-align:center;font:var(--font-weight-strong) 18px var(--font-sans);letter-spacing:.24em;text-transform:uppercase}
.loginsub{text-align:center;color:var(--text-tertiary);font-size:var(--font-size-label);margin:6px 0 16px}
.loginexpand{overflow:hidden;max-height:0;opacity:0;margin-top:0;transition:max-height .25s ease,opacity .2s ease,margin-top .25s ease}
.loginexpand.open{max-height:96px;opacity:1;margin-top:10px}

/* ==========================================================================================
   Billing: the demo strip.
   Tokens only, no literal colours: the app resolves light/dark from data-theme (tokens.css),
   so a hardcoded hex here would look correct in exactly one of the two themes.
   ========================================================================================== */

/* Spans sidebar and content both, and STICKS: a bar that scrolls away is not a status bar.
   Its height is the same custom property the frame subtracts, so the two can never disagree.
   24px and quiet on purpose: it is context, not a warning, so it uses the neutral raised
   surface rather than --warning-soft. */
:root{--demobar-h:0px}
:root.has-demobar{--demobar-h:24px}
.demobar{
  position:sticky;top:0;z-index:60;
  height:var(--demobar-h);display:flex;align-items:center;padding:0 14px;
  font-size:11px;letter-spacing:.01em;color:var(--text-tertiary);
  background:var(--surface-raised);border-bottom:1px solid var(--border);
  user-select:none;white-space:nowrap;overflow:hidden;
}
.demobar[hidden]{display:none}

/* ---- supplier column: the rules that decide it, and the pins that override them --------- */
/* The rules live at the top of the column they steer (pages/board/supplier-rules.js). */
.bomtbl th.supc{padding-right:4px}
.supcbtn{border:none;background:none;color:var(--text-muted);cursor:pointer;
  width:18px;height:18px;padding:0;margin-left:4px;border-radius:var(--radius-sm);
  display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}
.supcbtn:hover{background:var(--surface-raised);color:var(--text-primary)}
/* No rules set yet: the button names itself, so the affordance is findable at all. */
.supcbtn.labeled{width:auto;height:auto;padding:2px 8px;font-size:11px;
  font-weight:var(--font-weight-medium);color:var(--text-secondary);
  border:1px solid var(--border-strong)}
.supcbtn.labeled:hover{color:var(--text-primary)}

/* position:fixed on <body>, not absolute in the <th>: the header is sticky inside a
   scrolling table, and an absolutely-positioned child would be clipped by it. */
.suprules{position:fixed;z-index:60;background:var(--surface-card);
  border:1px solid var(--border-strong);border-radius:var(--radius-md);
  box-shadow:0 12px 32px rgba(0,0,0,.22);animation:fade var(--dur-fast) var(--ease);
  display:flex;flex-direction:column}
.suprules-head{flex:none;display:flex;align-items:center;gap:8px;
  padding:10px 8px 10px 12px;border-bottom:1px solid var(--border)}
.suprules-head strong{font-size:var(--font-size-label)}
.suprules-head .xbtn{margin-left:auto;width:26px;height:26px}
.suprules-body{flex:1;overflow:auto;padding:12px}
.suprules-foot{flex:none;display:flex;align-items:center;gap:8px;
  padding:10px 12px;border-top:1px solid var(--border)}

/* The pin marker carries its own undo: a pin you cannot see how to remove is a trap. */
.pinchip{display:inline-flex;align-items:center;gap:3px;padding:1px 3px 1px 5px;
  border-radius:var(--radius-sm);background:var(--accent-soft);color:var(--accent);
  font-size:10px;font-family:var(--font-sans);letter-spacing:.02em;white-space:nowrap}
.pin-x{border:none;background:none;color:inherit;cursor:pointer;padding:0;
  width:14px;height:14px;border-radius:3px;opacity:.6;
  display:inline-flex;align-items:center;justify-content:center}
.pin-x:hover{opacity:1;background:var(--surface-card)}

/* ---- export panel (pages/board/export.js), in the ONE docked surface -------------------- */
.exprow{display:flex;align-items:center;gap:10px;padding:8px 0}
.exprow .expname{flex:1;min-width:0;display:flex;align-items:center;gap:7px;
  font-size:var(--font-size-label);flex-wrap:wrap}
.exprow .expname .subl{flex-basis:100%;margin-top:0}
.exprule{height:1px;background:var(--border);margin:6px 0}
.expnote{display:flex;gap:7px;align-items:flex-start;margin-top:8px;padding:8px 10px;
  border-radius:var(--radius-md);background:var(--warning-soft);border:1px solid var(--warning-line);
  color:var(--text-secondary);font-size:var(--font-size-label);line-height:1.45}
.expnote svg{flex:none;margin-top:2px}

/* ---- library page (#/library): the folder RAIL, then the parts table ---------------------- */
/* The rail is DESIGN.md §1's selector rail, not a second sidebar: it lives inside .page, it
   only chooses which rows the table shows, and below the split breakpoint it COLLAPSES into
   a band-2 select (.libfoldsel) rather than stacking above the table — a stacked rail is a
   third band, which §3 forbids and which the old folders CARD did at every width under
   1100px (heading + sentence + New + the whole tree, above row 1). */
.libwrap{display:grid;grid-template-columns:196px minmax(0,1fr);gap:var(--space-5);align-items:start}
.libfoldsel{display:none}
@media (max-width:1100px){
  .libwrap{grid-template-columns:minmax(0,1fr)}
  .librail{display:none}
  .libfoldsel{display:inline-flex;max-width:180px}
}
/* Sticky against the DEMO BAR, not the viewport top (.inspector's `top:0` would slide under
   it in a demo session). No overflow:auto: the row menu is fixed-positioned, and a rail
   taller than the viewport should scroll with the page rather than trap its own tail. */
.librail{position:sticky;top:var(--demobar-h);padding-bottom:var(--space-4)}
.libtree{display:flex;flex-direction:column;gap:1px}
/* The rail's section label — the same small-caps as .secttl, with room for one ghost action. */
.libsect{display:flex;align-items:center;justify-content:space-between;gap:6px;
  padding:var(--space-4) 8px var(--space-1);font-size:10px;letter-spacing:.08em;
  text-transform:uppercase;color:var(--text-muted)}
.libsect:first-child{padding-top:2px}
.libsect .btn{margin:-4px -6px -4px 0}
.libfold{display:flex;align-items:center;gap:8px;min-height:30px;padding:4px 8px;
  border-radius:var(--radius-md);
  cursor:pointer;font-size:var(--font-size-label);color:var(--text-secondary)}
.libfold:hover{background:var(--surface-raised);color:var(--text-primary)}
.libfold.on{background:var(--accent-soft);box-shadow:inset 0 0 0 1px var(--accent-soft-line);
  color:var(--text-primary);font-weight:var(--font-weight-strong)}
.libfold .ic{color:var(--text-muted)}
.libfold.on .ic{color:var(--text-primary)}
.libfold .libfold-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The count and the actions share one right-hand slot of a FIXED width, so swapping them on
   hover cannot reflow the folder name beside them. */
.libfold .libfold-ct{flex:none;min-width:22px;text-align:right;
  font-family:var(--font-mono);font-variant-numeric:tabular-nums;
  font-size:10.5px;color:var(--text-muted)}
/* Actions replace the count on hover/selection, so the row never grows and the count never
   collides with the button. OPACITY, not display, and revealed by focus-within too — the
   board's own row-checkbox rule: an action that only exists on hover cannot be reached by a
   keyboard, and does not exist at all on a touch screen. */
.libfold-acts{display:inline-flex;opacity:0;gap:2px;flex:none;min-width:22px;justify-content:flex-end}
.libfold:hover .libfold-acts,.libfold:focus-within .libfold-acts,.libfold.on .libfold-acts{opacity:1}
.libfold:hover .libfold-ct,.libfold:focus-within .libfold-ct,.libfold.on .libfold-ct{display:none}
@media (hover:none){.libfold-acts{opacity:1}.libfold .libfold-ct{display:none}}
/* The row being moved stays put and marked, rather than vanishing into an editor. */
.libfold.moving{position:relative;background:var(--accent-soft);
  box-shadow:inset 0 0 0 1px var(--accent-soft-line)}
.libfold-acts .btn{padding:2px 4px}
/* The "new folder" affordance is a row of the same kind, quieter until hovered. */
.libfold.ghostrow{color:var(--text-muted)}
.libfold.editing{cursor:default;background:none;padding:2px 4px}
.libfold.editing:hover{background:none}
.libfold .input{flex:1;min-width:0;padding:4px 8px;font-size:var(--font-size-label)}
/* The rail's row menu. FIXED and placed from JS (the .scombo-list precedent): the rail is a
   196px column, so an absolutely-positioned menu would be clipped or would widen the track. */
.libmenu{position:fixed;z-index:70;min-width:186px;padding:4px;
  background:var(--surface-card);border:1px solid var(--border-strong);
  border-radius:var(--radius-md);box-shadow:var(--shadow-float)}
.libmenu button{display:flex;width:100%;align-items:center;gap:8px;border:none;background:none;
  padding:7px 10px;border-radius:var(--radius-sm);font:inherit;font-size:var(--font-size-label);
  color:var(--text-secondary);text-align:left;cursor:pointer}
.libmenu button:hover{background:var(--surface-raised);color:var(--text-primary)}
.libmenu button.danger{color:var(--error)}
.libmenu button.danger:hover{background:var(--error-soft)}
/* The parts table's own squeeze tiers ride its .libtblwrap container query (the width that
   changes is the TABLE's, when the inspector docks — a viewport query cannot see it).
   Drop order: added date, folder, package, manufacturer. The identity and the status column
   never drop: they are what the table exists for. */
.libtblwrap{container-type:inline-size}
.libtbl td.pkgc{font-family:var(--font-mono);font-size:11px;color:var(--text-secondary)}
/* What the part IS, which is what an EE scans a library by — the schematic's Value, falling
   back to the catalog description. */
.libtbl td.valc{max-width:170px;overflow:hidden;text-overflow:ellipsis}
/* The row's own action button: same opacity reveal as the rail's, so it is reachable by
   keyboard and present on touch. */
.libtbl td.rowact,.libtbl th.rowact{width:34px;padding:0 4px 0 0;text-align:right}
.libtbl td.rowact .btn{opacity:0}
.libtbl tr.line:hover .rowact .btn,.libtbl tr.sel .rowact .btn,
.libtbl .rowact .btn:focus-visible{opacity:1}
@media (hover:none){.libtbl td.rowact .btn{opacity:1}}
/* Two classes, to outrank the shared `.bomtbl.stbl td.mfrc{max-width:120px}` — the library
   has fewer columns than Search, so its manufacturer column can afford the extra 40px. */
.bomtbl.libtbl td.mfrc{max-width:160px}
.libtbl .mpnlink{font-size:var(--font-size-mono)}
.libtbl td.foldc{color:var(--text-tertiary);max-width:150px;overflow:hidden;text-overflow:ellipsis}
.libtbl td.addedc{color:var(--text-muted)}
/* Supply: is this saved part still orderable, and roughly at what price. Right-aligned
   tabular numerals (§5), with the observation stamp under the price it describes. */
.libtbl td.supc{white-space:nowrap;line-height:1.3}
.libtbl td.supc .stamp{display:block;font-family:var(--font-sans);font-size:9px;
  color:var(--text-muted)}
/* The project link, in the panel only: it is the way to the board, and nothing else on the line
   is clickable, so it has to read as a link. */
.usedlink{color:var(--accent);font-weight:var(--font-weight-strong)}
/* Drop order, worst-to-best for an EE scanning a library: the date goes first, then where it is
   filed, then the package, the maker, and only at the very end the price. Part, Value, Stock and
   Status never drop — what it is, whether you can buy it, and whether KiCad can place it are the
   reasons this table exists.
   The thresholds came DOWN again when Used-in and the datasheet icon left the table: they describe
   how much table there is to fit, so a column removed without moving them wastes the width it
   freed. */
@container (max-width:1080px){ .libtbl .coladded{display:none} }
@container (max-width:960px){ .libtbl .coladded,.libtbl .colfold{display:none} }
@container (max-width:860px){
  .libtbl .coladded,.libtbl .colfold,.libtbl .colpkg{display:none}
}
/* Roughly the width the docked inspector leaves. What survives is the reason you are scanning:
   what the part is, whether there is stock, what it costs, whether KiCad can place it. */
@container (max-width:740px){
  .libtbl .coladded,.libtbl .colfold,.libtbl .colpkg,.libtbl .colmfr{display:none}
  .libtbl td.valc{max-width:150px}
}
@container (max-width:560px){
  .libtbl .coladded,.libtbl .colfold,.libtbl .colpkg,.libtbl .colmfr,
  .libtbl .colprice{display:none}
}
/* One project this part sits on, in the panel: the link, then what it does there. */
.userow{padding:4px 0;border-bottom:1px solid var(--border)}
.userow:last-child{border-bottom:none}
.userow .subl{margin-top:1px}

/* The inspector's fact grid: labels in one column so values line up (the old .kv rows put
   every value at a different x, which is why the panel read as a pile). */
.factgrid{display:grid;grid-template-columns:84px minmax(0,1fr);gap:6px var(--space-3);
  font-size:var(--font-size-label);align-items:baseline}
.factgrid .k{color:var(--text-tertiary)}
.factgrid .v{min-width:0;overflow-wrap:anywhere}
/* Symbol and footprint side by side: one glance answers "what does this place as". Each is a
   BUTTON: a 48-pin part is an illegible smudge in a 148px pane, so pressing one expands it to
   the panel's full width. It expands IN PLACE — a drawing is reading, and §1.3 keeps reading
   out of modals. The drawings sit on their own canvas (--eda-paper), because KiCad's outline
   red is drawn for a light sheet and disappears on a dark surface. */
.assetduo{display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2)}
/* The drawing's report action sits INSIDE the credit line it belongs to, on its own line so a
   long "Converted from …" sentence never has a button hanging off its last word. Quiet by
   default: it is the escape hatch for a drawing that is wrong, not something to invite. */
.reportdraw{display:inline-flex;margin-top:4px;height:24px;padding:0 7px;font-size:11px;
  color:var(--text-tertiary)}
.reportdraw:hover{color:var(--text-primary)}
.assetfig{display:block;width:100%;margin:0;padding:6px 6px 4px;text-align:center;
  background:var(--eda-paper);border:1px solid var(--border);border-radius:var(--radius-md);
  cursor:zoom-in;transition:border-color var(--dur-fast) var(--ease)}
.assetfig:hover{border-color:var(--border-strong)}
.assetfig svg{display:block;width:100%;height:112px}
.assetfig figcaption,.assetfig .cap{display:block;margin-top:3px;font-size:10px;
  color:var(--text-tertiary)}
.assetfig.expanded{grid-column:1 / -1;cursor:zoom-out}
.assetfig.expanded svg{height:320px}
/* A dense symbol (>16 pins) drops its pin labels until expanded: 48 names inside a 150px box
   render as grey texture, which reads as a dirty drawing rather than as data. Small parts
   keep their labels at every size, where they are legible. */
.assetfig:not(.expanded) svg.dense .eda-lbl{display:none}
/* A part's documents: one row each, the primary marked because it is the one KiCad gets. */
.docrow{display:flex;align-items:center;gap:7px;min-height:28px;font-size:var(--font-size-label)}
.docrow>.ic{color:var(--text-muted);flex:none}
.docrow .docname{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  color:var(--accent)}
.docrow .btn{flex:none}
/* Delete is separated by a rule, not by distance: it is the one irreversible thing here. */
.dangersep{margin-top:var(--space-5);padding-top:var(--space-3);border-top:1px solid var(--border)}
/* The inspector's sections and the ONE disclosure that holds the long fact list (§1.2). */
.isect{margin-top:var(--space-4)}
.factmore summary{display:flex;align-items:center;gap:5px;padding:4px 0;cursor:pointer;
  font-size:var(--font-size-label);color:var(--text-tertiary);list-style:none}
.factmore summary::-webkit-details-marker{display:none}
.factmore summary:hover{color:var(--text-primary)}
.factmore[open] summary .ic{transform:rotate(180deg)}
/* The add-from-search picker's result rows (in the inspector): a full-width option button,
   MPN + price hint on top, manufacturer · package under. */
.libpick{display:block;width:100%;padding:6px 8px;border:1px solid var(--border);
  border-radius:var(--radius-md);background:var(--surface-card);color:var(--text-primary);
  text-align:left;font-size:var(--font-size-label)}
.libpick:hover{background:var(--surface-inset)}
.libpick:disabled{opacity:.6;cursor:default}
/* The library picker's result rows. A GRID, not a flex row of boxes: MPN, facts, and the
   decision cluster each own a track, so ten results scan as three straight columns instead
   of ten differently-shaped cards. Bordered boxes are gone — separators do that work. */
.pickrows{display:flex;flex-direction:column}
/* TWO lines, not three columns: the decision cluster (lifecycle, stock, price, datasheet,
   Use) owns the first line's right end, and the description gets the full panel width on the
   second. Squeezing all of it onto one line left the manufacturer at "Texas In…". */
.prow{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:2px var(--space-3);
  align-items:center;padding:8px;border-radius:var(--radius-sm)}
.prow:hover{background:var(--surface-raised)}
.prow + .prow{box-shadow:inset 0 1px 0 var(--border)}
.prow .pmpn{grid-column:1;grid-row:1;font-family:var(--font-mono);font-size:12px;
  word-break:break-all}
.prow .pright{grid-column:2;grid-row:1;display:flex;align-items:center;gap:var(--space-3);
  justify-content:flex-end}
.prow .pmeta{grid-column:1 / -1;grid-row:2;min-width:0;display:flex;gap:8px;align-items:baseline}
.prow .pmfr{flex:none;font-size:11px;color:var(--text-secondary);white-space:nowrap}
.prow .pdesc{min-width:0;flex:1;font-size:11px;color:var(--text-tertiary);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* ONE stamp per row, under the pair it describes: stock and price come from the same
   observation, so two identical "as of" lines per row is noise, not more honesty (§5). */
.prow .pfig{display:flex;flex-direction:column;align-items:flex-end;gap:0;
  font-family:var(--font-mono);font-variant-numeric:tabular-nums;font-size:11px;
  white-space:nowrap;line-height:1.35}
.prow .pfig .figs{display:flex;align-items:center;gap:8px}
.prow .pfig .stamp{font-family:var(--font-sans);font-size:9px;color:var(--text-muted)}
.prow .dslink{display:inline-flex;color:var(--text-tertiary)}
/* What KiCad gets for this part. Neutral by default and semantic ONLY for an official KiCad
   symbol, which is the one state that is a guarantee rather than an expectation. */
.prow .pcad{flex:none;display:inline-flex;align-items:center;gap:4px;font-size:11px;
  color:var(--text-secondary);white-space:nowrap}
.prow .pcad.ok{color:var(--success)}
.prow .pcad.muted{color:var(--text-muted)}
.prow .dslink:hover{color:var(--text-primary)}
/* Already-saved rows say so instead of offering a second Add: a re-add merges server-side,
   but a button that looks like it adds a copy is a lie about what will happen. */
.prow .inlib{display:inline-flex;align-items:center;gap:4px;font-size:10px;color:var(--success);
  white-space:nowrap}
/* ---- the folder picker (core/folderpicker.js) -------------------------------------------- */
/* A button that opens a TREE. The popover is FIXED and placed from JS for the same reason the
   seller combobox's is: both open inside the inspector, whose body is an overflow:auto
   scrollport that would clip an absolutely-positioned child. */
.fpick{display:inline-flex;align-items:center;gap:7px;max-width:100%;
  padding:6px 10px;border:1px solid var(--border-strong);border-radius:var(--radius-md);
  background:var(--surface-card);color:var(--text-primary);font-size:var(--font-size-label);
  text-align:left}
.fpick:hover{border-color:var(--text-tertiary)}
.fpick .ic{color:var(--text-muted);flex:none}
.fpick-face{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fpick-pop{position:fixed;z-index:70;max-width:340px;padding:4px;
  background:var(--surface-card);border:1px solid var(--border-strong);
  border-radius:var(--radius-md);box-shadow:var(--shadow-float)}
.fpick-inner{display:flex;flex-direction:column;gap:4px}
.fpick-head:empty{display:none}
.fpick-filter{width:100%;padding:5px 9px}
.fpick-list{max-height:240px;overflow-y:auto;display:flex;flex-direction:column;gap:1px}
.fpick-opt{display:flex;align-items:center;gap:7px;padding:6px 10px;border-radius:var(--radius-sm);
  font-size:var(--font-size-label);color:var(--text-secondary);cursor:pointer}
.fpick-opt:hover,.fpick-opt.on{background:var(--surface-raised);color:var(--text-primary)}
.fpick-opt.sel{background:var(--accent-soft);box-shadow:inset 0 0 0 1px var(--accent-soft-line);
  color:var(--text-primary);font-weight:var(--font-weight-strong)}
.fpick-opt .ic{color:var(--text-muted);flex:none}
.fpick-opt .fpick-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fpick-empty{padding:8px 10px}
.fpick-new{display:flex;align-items:center;gap:7px;width:100%;padding:7px 10px;border:none;
  border-top:1px solid var(--border);border-radius:0 0 var(--radius-sm) var(--radius-sm);
  background:none;font:inherit;font-size:var(--font-size-label);color:var(--text-tertiary);
  text-align:left;cursor:pointer}
.fpick-new:hover{background:var(--surface-raised);color:var(--text-primary)}

/* Where the pick LANDS: a quiet line under the search box, not a boxed control. As a bordered,
   inset panel above the results it read as a step to complete before searching, when it is really
   a footnote to the search with a way to change it. No border, no fill. */
/* The result-count line. It replaced the pinned "Adding to" target: where a part lands is
   asked on the row's own Add button now, so nothing about the destination is standing here. */
.pickhead{display:flex;align-items:center;min-height:16px;padding:0 2px}
.pickhead .muted{margin-left:auto;text-align:right}

/* The MCP hint: "an agent could do this for you", shown where the work happens.
   Quiet on purpose — it advertises a feature rather than reporting the user's data, so it must
   never outrank the thing they came to do. One accent-soft line, not a coloured banner. */
.mcphint{display:flex;align-items:center;gap:10px;padding:8px 12px;
  background:var(--accent-soft);border:1px solid var(--accent-soft-line);
  border-radius:var(--radius-md);color:var(--text-secondary);font-size:var(--font-size-label)}
.mcphint svg{flex:none;color:var(--text-tertiary)}
.mcphint-text{flex:1;min-width:0}
/* Pushed to the end and de-emphasised: dismissing is always available, never the obvious move. */
.mcphint-x{flex:none;color:var(--text-tertiary);padding:4px}
.mcphint-x:hover{color:var(--text-primary)}


/* ---- #/mcp: connections + per-client setup ---------------------------------------------------
   The old page was three numbered steps ending in "you should now be connected", which is the one
   thing a guide cannot know. This one mints a credential and then shows, per client, the exact
   two facts it needs. Every code block is something the user moves into another program
   byte-for-byte, so Copy is a first-class control rather than decoration. */
.mcpconns,.mcpsetup{padding:var(--space-3);margin-bottom:var(--space-3)}
.mcpconns-head{display:flex;align-items:center;justify-content:space-between;gap:12px;
  margin-bottom:var(--space-2)}
.mcpconns-head h2,.mcpsetup h2{font-size:15px;margin:0}
.mcpsetup-lead{color:var(--text-secondary);font-size:13px;margin:0 0 var(--space-2)}

.mcpconn-list{display:flex;flex-direction:column;gap:6px}
.mcpconn{display:flex;align-items:center;gap:12px;padding:9px 11px;border:1px solid var(--border);
  border-radius:var(--radius-md);background:var(--surface-page)}
.mcpconn-main{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}
.mcpconn-label{font-weight:var(--font-weight-medium);font-size:13px}
.mcpconn-when{font-size:11px;color:var(--text-tertiary)}
/* Only a connection that has actually been USED reads as live. A minted token nobody connected
   is not a connected agent, and colouring it green would be the old page's lie in a new shape. */
.mcpconn-when.live{color:var(--success)}
.mcpconn-meta{display:flex;align-items:center;gap:10px;font-size:11px;color:var(--text-tertiary)}
.mcpconn-meta code{font-family:var(--font-mono);font-size:10.5px}
.mcpconn-ro{border:1px solid var(--border);border-radius:4px;padding:0 5px}

/* Shown once, and loud about it: the server keeps only a hash, so this really is the last time. */
.mcptoken{border:1px solid var(--accent-soft-line);background:var(--accent-soft);
  border-radius:var(--radius-md);padding:11px 12px;margin-bottom:var(--space-2)}
.mcptoken-title{display:flex;align-items:center;gap:7px;font-weight:var(--font-weight-strong);
  font-size:13px;margin-bottom:4px}
.mcptoken-why{margin:0 0 8px;font-size:12px;color:var(--text-secondary)}

.mcpurl{display:flex;flex-direction:column;gap:5px;margin-bottom:var(--space-3)}
.mcpurl-label{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--text-tertiary)}

.mcpclients{display:grid;grid-template-columns:repeat(auto-fill,minmax(330px,1fr));gap:12px}
.mcpclient{border:1px solid var(--border);border-radius:var(--radius-md);padding:11px 12px}
.mcpclient-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:5px}
.mcpclient-name{font-weight:var(--font-weight-strong);font-size:13px}
.mcpclient-badge{font-size:10px;letter-spacing:.05em;color:var(--text-tertiary);
  border:1px solid var(--border);border-radius:4px;padding:0 5px}
.mcpclient-note{margin:0 0 7px;font-size:11.5px;color:var(--text-tertiary)}
/* Sits after the block and reads as the next instruction, so it gets the body colour the notes
   above the block deliberately do not have. */
.mcpclient-then{margin:9px 0 0;font-size:12px;color:var(--text-secondary)}

.mcpcodewrap{display:flex;flex-direction:column}
/* The header is part of the block: one border around both, square where they meet. Free-floating
   above it, the Copy button sat a full card-width from the code and level with nothing. */
.mcpcodehead{display:flex;align-items:center;justify-content:space-between;gap:10px;
  padding:5px 6px 5px 11px;border:1px solid var(--border);border-bottom:none;
  border-radius:var(--radius-sm) var(--radius-sm) 0 0;background:var(--surface-raised)}
.mcpcodelang{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.04em;
  text-transform:uppercase;color:var(--text-tertiary)}
.mcpcodewrap .mcpcode{border-top-left-radius:0;border-top-right-radius:0}
/* --surface-tool is a near-black well in BOTH themes, and it has a partner token: everywhere else
   in this file that uses it pairs it with --text-on-tool (.bulkbar, the pop menu). This paired it
   with --text-secondary, which is dark in the light theme — so the client snippets, the thing the
   page exists to hand you, were dark-on-dark. The Copy button still worked, which is exactly why
   it survived a screenshot check: the block LOOKS deliberate until you try to read it. */
.mcpcode{margin:0;padding:10px 12px;border:1px solid var(--border);
  border-radius:var(--radius-sm);background:var(--surface-tool);overflow-x:auto;
  font-family:var(--font-mono);font-size:11px;line-height:1.55;white-space:pre;
  color:var(--text-on-tool)}
/* WRAPPING ONLY. These sit on the same near-black well as every other .mcpcode, so re-declaring
   colour here re-broke exactly what pairing --surface-tool with --text-on-tool had just fixed —
   and it landed on the worst possible element: the one-time bearer token, which the user gets ONE
   chance to read. Copy still worked, which is what hid it. The base rule owns the colour. */
.mcpcode.token,.mcpcode.url{white-space:pre-wrap;word-break:break-all}

.mcpsoon{display:flex;align-items:flex-start;gap:7px;margin:var(--space-3) 0 0;
  font-size:12px;color:var(--text-tertiary)}
.mcpsoon svg{flex:none;margin-top:2px}

.mcpcreate{padding:var(--space-3);margin-bottom:var(--space-3)}
.mcpcreate h3{margin:0 0 var(--space-2);font-size:14px}
.mcpfield{display:flex;flex-direction:column;gap:5px;margin-bottom:var(--space-2);font-size:12px;
  color:var(--text-secondary)}
.mcpcheck{display:flex;align-items:center;gap:8px;font-size:13px;margin-bottom:var(--space-2)}
.mcpcreate-acts{display:flex;justify-content:flex-end;gap:8px}

/* The OAuth consent screen. Centred and free of the app chrome on purpose: it opens in a window
   an MCP client popped, often with no other tab of ours, and the only decision on it is Allow. */
.consentwrap{min-height:70vh;display:grid;place-items:center;padding:var(--space-4)}
.consentcard{width:min(430px,92vw);border:1px solid var(--border);border-radius:var(--radius-lg);
  background:var(--surface-card);padding:26px}
.consent-icon{width:38px;height:38px;border-radius:10px;display:grid;place-items:center;
  background:var(--accent-soft);border:1px solid var(--accent-soft-line);color:var(--text-secondary);
  margin-bottom:14px}
.consentcard h1{margin:0 0 5px;font-size:18px}
.consent-who{margin:0 0 14px;color:var(--text-secondary);font-size:13px}
.consent-can{margin:0 0 18px;padding-left:18px;color:var(--text-secondary);font-size:13px}
.consent-can li{margin:5px 0}
.consent-go{width:100%}
.consent-sub{margin:12px 0 0;text-align:center;font-size:12px;color:var(--text-tertiary)}
/* The destination line on the consent screen. A remote host is the thing worth reading twice,
   so it is the only part of that card that carries a warning colour. */
.consent-dest{display:flex;align-items:flex-start;gap:7px;margin:0 0 16px;font-size:12.5px;
  color:var(--text-tertiary)}
.consent-dest svg{flex:none;margin-top:2px}
.consent-dest.remote{color:var(--warning);border:1px solid var(--warning-line,var(--border));
  background:var(--warning-soft,transparent);border-radius:var(--radius-sm);padding:8px 10px}

/* ---- #/mcp, OAuth-first ---------------------------------------------------------------------
   The URL is the page. It gets the weight a primary action gets, because copying it IS the action
   — everything below is that same URL restated for one client. */

.mcpinstall{align-self:flex-start;margin-bottom:8px}

/* The escape hatch, collapsed. A token is the wrong answer for a person at a browser and the only
   answer for a machine without one, so it is present and quiet. */
.mcpadv{padding:var(--space-3)}
.mcpadv-toggle{display:flex;align-items:center;gap:7px;background:none;border:0;padding:0;
  cursor:pointer;color:var(--text-secondary);font:inherit;font-size:13px}
.mcpadv-toggle:hover{color:var(--text-primary)}
.mcpadv-why{margin:12px 0;font-size:12.5px;color:var(--text-secondary);max-width:64ch}
/* Cards size to their own content. Without this the grid stretches every card in a row to the
   tallest, so a two-line "paste the URL" client sat in a card as tall as a nine-line JSON block. */
.mcpclients{align-items:start}
.mcpconns-none{margin:6px 0 0;font-size:12.5px;color:var(--text-tertiary)}
/* A long command scrolls rather than being clipped mid-URL with no way to see the rest. */
.mcpcode{max-width:100%}
/* A shell command WRAPS; only structured config scrolls. A one-line command clipped mid-URL reads
   as broken, and the part that gets cut is the part that matters. JSON keeps pre + scroll, where
   wrapping would break the indentation that makes it scannable. */
.mcpcode.bash{white-space:pre-wrap;word-break:break-all}

/* ---- #/mcp: three steps, one client at a time ------------------------------------------------
   The page used to show eight code blocks at once. Nobody uses eight clients, so seven of them
   were noise sitting on the one instruction that mattered. Tabs carry each client's own logo
   because that is what people scan for — the Cursor cube lands before the word does.

   Restrained on colour on purpose. DESIGN.md §3.5 reserves the agent indigo for the frame strip;
   spending it again on every numbered circle and active tab makes the page shout in a hue that is
   supposed to mean "this is the agent lane", not "this is a step". */
/* ONE COLUMN WIDTH for the whole lane.
   At 1440 every card ran the full width while the step prose stopped at 68ch, so a single column
   of content had three different right edges and the page read as ragged rather than composed.
   Capping the cards instead of unleashing the paragraph keeps the measure that makes prose
   readable and gives the URL row, the tabs, the code and the text one shared edge. */
/* 1020 is measured, not chosen by eye: the eight tabs need 919px of track and the card spends 66px
   on padding and borders, so anything under ~985 drops one tab onto a second row on its own, which
   reads as an accident rather than a wrap. */
.mcpsetup,.mcpconns,.mcpadv,.mcptoken{max-width:1020px}
.mcpsteps{list-style:none;margin:0;padding:0}
.mcpsteps li{display:flex;gap:14px;padding:0 0 20px;position:relative}
.mcpsteps li:last-child{padding-bottom:0}
/* The rail is what makes three cards read as one sequence. */
.mcpsteps li:not(:last-child):after{content:"";position:absolute;left:13px;top:30px;bottom:4px;
  width:1px;background:var(--border)}
/* A filled disc, not an outlined one. At 1px on a light surface the ring vanished and the numbers
   read as loose digits, which took the sequence — the whole point of the three steps — with it. */
.mcpstep-n{flex:none;width:26px;height:26px;border-radius:50%;display:grid;place-items:center;
  background:var(--surface-raised);border:1px solid var(--border);z-index:1;
  font:var(--font-weight-strong) 11.5px var(--font-sans);color:var(--text-primary)}
.mcpstep-b{flex:1;min-width:0}
.mcpstep-b h3{margin:4px 0 10px;font-size:13.5px}
.mcpstep-p{margin:0;font-size:12.5px;color:var(--text-secondary);max-width:68ch;line-height:1.6}

.mcphero-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.mcphero-url{flex:1;min-width:220px;font-family:var(--font-mono);font-size:13.5px;
  padding:10px 12px;border:1px solid var(--border);border-radius:var(--radius-md);
  background:var(--surface-raised);overflow-x:auto;white-space:nowrap}

.mcptabs{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px}
.mcptab{display:inline-flex;align-items:center;gap:7px;font:inherit;font-size:12px;
  padding:5px 11px 5px 8px;border-radius:99px;border:1px solid var(--border);background:none;
  color:var(--text-secondary);cursor:pointer}
.mcptab:hover{background:var(--surface-raised);color:var(--text-primary)}
/* The selected tab is a surface change, not a colour change: the logo beside it is already
   carrying the brand's own colour, and two colours fighting in one pill reads as a mistake. */
/* The selected tab inverts. A border tint alone was invisible next to seven neighbours carrying
   their own brand colour — the one thing that has to be unambiguous here is which client you are
   reading instructions for. */
/* The pair .btn.primary uses, which is the one combination guaranteed legible on both themes.
   This read `color: var(--surface-1)` — a token that does not exist — so the declaration was
   dropped and the label inherited text-primary onto a text-primary pill: the selected client's
   name rendered invisible. */
.mcptab.on{background:var(--accent);border-color:var(--accent);
  color:var(--text-on-accent);font-weight:var(--font-weight-strong)}
.mcptab.on:hover{background:var(--accent);color:var(--text-on-accent)}
.bmark{display:inline-flex;align-items:center;line-height:0}
.bmark svg{width:15px;height:15px}
.mcptabbody-wrap{min-height:1px}
