/* workspace.css — Workspace-only styling layered on top of the shared
   /style.css design tokens. Nothing here overrides Operations; it only adds
   classes used by the Workspace SPA (public/workspace/app.js). */

/* restyle phase 1a, 2026-08-07 — three sweeps through this file:
     · control chrome (panel2 fill / text ink / 1px line border / radius) is
       now stated ONCE, by the global :where(input, select, textarea) rule in
       /style.css. Rules here keep only what DIFFERS from it — a --panel fill,
       a tighter radius, a size. Blocks marked "chrome from global control
       rule" are the ones that were nothing but the duplicate.
     · corner radii name their job (--r-ctl / --r-soft / --r-card) so a later
       phase can move a whole shape class at once.
     · ok/warn/bad/accent tints are color-mix on the token instead of a frozen
       rgba triplet, so they follow the theme.
     · type sizes that were already ON the scale name their step (--fs-micro …
       --fs-title). Only exact matches moved — the half-pixel sizes (10.5,
       11.5, 12.5) are left alone rather than rounded onto the scale, because
       rounding them is a design decision, not a rename.
   The REPORT side (.rpt-* .pnl-* .spec-* .rk-* .vc-* and @media print) is
   untouched on purpose: its colours and sizes are paper-correct, not
   screen-correct, and must not follow a theme. The dense grids (.dev-c /
   .bom-c cells) are equally untouched — their inputs are DELIBERATELY
   chromeless and override the global on purpose. */

.ws-back { font-size: var(--fs-body-s); }

/* Fluid: use the whole window (owner 2026-07-21) — wide grids benefit; a
   min keeps narrow windows scrolling instead of crushing the tables. */
/* The shared main element already carries the app padding (22px 24px 80px in
   style.css) — the wrapper must not add its own, or every Workspace page sits
   lower and narrower than the same page in Operations. */
.ws-wrap { max-width: none; margin: 0 auto; padding: 0; }

.ws-crumbs { color: var(--dim); font-size: var(--fs-body-s); margin: 4px 0 18px; }
.ws-crumbs a { color: var(--accent); text-decoration: none; }
.ws-crumbs a:hover { text-decoration: underline; }
.ws-crumbs .sep { margin: 0 7px; color: var(--line); }

/* Project picker cards (mirror the Operations .mod-card feel). */
.ws-picker { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; }
.ws-proj { display: block; text-decoration: none; color: var(--text);
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-card); padding: 15px 16px; }
.ws-proj:hover { border-color: var(--accent); }
.ws-proj .code { font-family: var(--mono); font-size: var(--fs-small); color: var(--dim); }
.ws-proj .name { font-size: var(--fs-lead); margin: 4px 0; }
.ws-proj .meta { font-size: var(--fs-small); color: var(--dim); }
.ws-proj .count { float: right; font-size: var(--fs-small); color: var(--dim); }

/* Hierarchy tree. Indentation communicates Site → Venue → Area → Room. */
.ws-tree { margin-top: 6px; }
.ws-node { display: flex; align-items: center; gap: 8px;
  border: 1px solid var(--line); border-radius: var(--r-soft); background: var(--panel);
  padding: 8px 10px; margin: 5px 0; }
.ws-node.archived { opacity: .55; }
.ws-node .kind { font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .06em;
  color: var(--dim); border: 1px solid var(--line); border-radius: var(--r-ctl); padding: 1px 6px; }
.ws-node .nname { font-size: var(--fs-body); }
.ws-node .ncode { font-family: var(--mono); font-size: var(--fs-small); color: var(--dim); }
.ws-node .grow { flex: 1; }
.ws-node .rowbtns { display: flex; gap: 4px; opacity: 0; transition: opacity .1s; }
.ws-node:hover .rowbtns { opacity: 1; }
.ws-node .rowbtns button { font-size: var(--fs-small); padding: 2px 8px; }
.ws-node .rowbtns button:disabled { opacity: .3; cursor: default; border-color: var(--line); color: var(--dim); }

/* Tier colours (see the .ws-depth-* colour rules at the very end of this file —
   they must come after .ws-leveldef / .ws-item-row so they win the left border). */

.ws-children { margin-left: 22px; border-left: 1px dashed var(--line); padding-left: 12px; }

.ws-addbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 10px 0 12px; }
/* ONE grammar for every add row (owner 2026-08-25): pickers first, one
   primary ＋ button naming the noun, then the muted hint — and one PLACE:
   directly beneath the views bar (or the panel header where none), above
   the grid. The row is a CARD (his follow-up ruling): the same swept
   two-corner shape and nested fill the views bar wears, so the pair reads
   as siblings — the bar reads the table, the card feeds it. Fill and
   spacing only, no border, per the surface law. */
.ws-addbar { background: var(--surface-2); border-radius: var(--r-card); padding: 9px 14px; }
.vw-bar + .ws-addbar { margin-top: -6px; }
.ws-add-hint { font-size: var(--fs-small); }
/* chrome from global control rule (restyle phase 1a, 2026-08-07) */
.ws-addbar input, .ws-addbar select { padding: 6px 8px; font-size: var(--fs-body-s); }
/* A SELECT SIZES ITSELF TO ITS LONGEST OPTION, and nothing bounds what a person
   may call a zone: `clean()` trims a name and does not cap it. A pasted
   paragraph became a 2,691px dropdown that pushed the whole Device Schedule
   into horizontal scroll -- measured 2026-09-10 at a 1,425px viewport,
   overflowing by 1,320px.

   The failure to fix is the PAGE scrolling sideways, not the select being
   wide, so the bound is the bar rather than a number. A fixed cap was tried
   first and rejected: 260px clipped an ordinary 42-character device name that
   wants 281px. `min-width: 0` is the half that does the work -- without it a
   flex item refuses to shrink below its content and overflows anyway. */
.ws-addbar select { max-width: 100%; min-width: 0; }
.ws-addbar .primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.ws-hint { color: var(--dim); font-size: var(--fs-small); }

/* Per-project level names (depth labels). */
.ws-levels { margin: 12px 0 6px; padding: 10px 12px; border: 1px solid var(--line);
  border-radius: var(--r-soft); background: var(--panel); }
.ws-levels-h { font-size: var(--fs-small); color: var(--dim); margin-bottom: 8px; }
.ws-levels-h .muted { opacity: .8; }
.ws-levels-row { display: flex; flex-wrap: wrap; gap: 10px; }
.ws-level { display: flex; align-items: center; gap: 6px; }
.ws-level-n { font-family: var(--mono); font-size: var(--fs-mini); color: var(--dim);
  border: 1px solid var(--line); border-radius: var(--r-ctl); padding: 1px 6px; }
.ws-level-name { background: var(--panel2); color: var(--text); border: 1px solid var(--line);
  border-radius: var(--r-ctl); padding: 5px 8px; font-size: var(--fs-body-s); width: 150px; }
.ws-level-name:disabled { opacity: .6; }

.ws-readonly { color: var(--dim); font-size: var(--fs-small); border: 1px dashed var(--line);
  border-radius: var(--r-soft); padding: 8px 12px; margin: 10px 0; }

/* Future-slice placeholders — clearly disabled, signal direction only. */
.ws-future { margin-top: 30px; }
.ws-future h3 { color: var(--dim); font-size: var(--fs-small); text-transform: uppercase; letter-spacing: .06em; }
.ws-future .row { display: flex; gap: 10px; flex-wrap: wrap; }
.ws-future .chip { border: 1px dashed var(--line); border-radius: var(--r-soft); padding: 8px 12px;
  color: var(--dim); font-size: var(--fs-body-s); opacity: .7; cursor: not-allowed; }

/* One empty-state voice across the suite (owner 2026-08-08): same ink, type
   size, line-height and breathing room as Operations' .empty-state
   (style.css) — an empty region is a sentence, not a greyed-out shrug. NOT
   flex like its Operations twin: Workspace call sites are inline sentences
   (with <b> and .ws-empty-go mid-flow), and flex would fragment them. */
.ws-empty { color: var(--dim); font-size: var(--fs-body-s); line-height: 1.5;
  padding: 18px 4px; max-width: 62ch; }
/* The FILTERED-empty block (lib-filtered.js) — one shape in both apps; see
   the twin rule in style.css. */
.flt-empty { color: var(--dim); font-size: var(--fs-body-s); line-height: 1.5;
  padding: 18px 4px; display: flex; flex-direction: column; align-items: flex-start; gap: 10px; }
.flt-empty strong { color: var(--text-2); font-weight: 600; font-size: var(--fs-body); }
.flt-empty .flt-why { color: var(--dim); max-width: 58ch; }
.ws-err { color: var(--bad); }
/* The NIC panel's one-line scope heading: which level's ports these are. */
.nic-scope-h { font-size: var(--fs-mini); padding: 2px 0 4px; }

/* Panels (Level Definition, Items). */
.ws-panel { border: 1px solid var(--line); border-radius: var(--r-card); background: var(--panel);
  padding: 14px 16px; margin: 14px 0; }
.ws-panel-h { font-size: var(--fs-body-s); font-weight: 600; margin-bottom: 10px; }
.ws-panel-h .muted { color: var(--dim); font-weight: 400; }

/* Level definition rows — each stacks and indents to show the hierarchy, and
   carries the same tier colour as the matching zone level.
   left-stripe sweep, 2026-08-07: that colour used to be a 3px left border. It
   is now `--lvc` (see the ramp near the foot of this file, or an inline value
   from app.js) spent as a wash leading from the left edge, plus the level's
   colour tinting the whole border instead of one side of it. `background-color`
   + `background-image` rather than the `background` shorthand so the two are
   separate layers — the zone drag states restate the colour layer only. */
.ws-leveldef { display: flex; flex-direction: column; gap: 6px; margin: 5px 0;
  padding: 6px 8px; border-radius: var(--r-soft);
  border: 1px solid color-mix(in srgb, var(--lvc, var(--line)) 45%, var(--line));
  border-top-color: var(--edge-hi-soft);
  background-color: var(--panel2);
  background-image: linear-gradient(90deg, color-mix(in srgb, var(--lvc, transparent) 14%, transparent),
                                           color-mix(in srgb, var(--lvc, transparent) 4%, transparent) 55%,
                                           transparent); }
.ld-main { display: flex; align-items: center; gap: 6px; }
/* 2nd row indented so the card reads more easily. */
.ld-cfg { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: var(--fs-small); color: var(--dim); margin-left: 30px; }
.ld-cfg label { color: var(--dim); }
/* --panel, not the global's --panel2: these sit ON a --panel2 card. */
.ld-sel { background: var(--panel); padding: 3px 6px; font-size: var(--fs-small); }
.ld-chk { display: inline-flex; align-items: center; gap: 3px; }
.ld-sep, .ld-seplabel { opacity: .7; }
.ld-sepinput { width: 34px; text-align: center; }
.ld-preview { font-family: var(--mono); font-size: var(--fs-mini); color: var(--accent); margin-left: 4px;
  border: 1px dashed var(--line); border-radius: var(--r-ctl); padding: 2px 6px; }
.ld-group-label { color: var(--dim); font-weight: 600; min-width: 78px; }
.ld-attrs { row-gap: 8px; }
/* Area lives in a single combined box (checkbox + calc method). */
.ld-attr-box { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--line);
  border-radius: var(--r-ctl); padding: 3px 8px; background: var(--panel); }
/* Each catalog attribute is a toggle chip; highlighted when tracked. */
.ld-attr-chk { display: inline-flex; align-items: center; gap: 4px; border: 1px solid var(--line);
  border-radius: var(--r-ctl); padding: 3px 8px; background: var(--panel); color: var(--text); cursor: pointer; }
.ld-attr-chk:has(input:checked) { border-color: var(--accent); color: var(--text); }
.ld-sel:disabled { opacity: .45; }

/* Zone attribute value controls. */
/* chrome from global control rule (restyle phase 1a, 2026-08-07) */
.zn-attr { font-size: var(--fs-small); padding: 4px 6px; flex: none; }
.zn-attr[type="checkbox"] { border: none; background: none; padding: 0; }
.zn-attr-text { width: 88px; }
.zn-attr-sel { max-width: 132px; }
/* Wider than the other attribute pulldowns because its options carry a name
   AND an address ("AV-R01 · 10.1.0.106"), and a controller truncated to its
   first eight characters is not a controller you can pick between. */
.zn-ctl-sel { max-width: 186px; }
.zn-attr:focus { border-color: var(--accent); outline: none; }
.zn-attr-wrap { display: inline-flex; align-items: center; gap: 3px; font-size: var(--fs-mini); color: var(--dim);
  border: 1px solid var(--line); border-radius: var(--r-ctl); padding: 3px 6px; background: var(--panel2); white-space: nowrap; flex: none; }
.zn-ceiling { display: inline-flex; gap: 3px; align-items: center; flex: none; }
.ws-lvl-n { font-family: var(--mono); font-size: var(--fs-mini); color: var(--dim);
  border: 1px solid var(--line); border-radius: var(--r-ctl); padding: 1px 6px; }
/* The rank is TYPEABLE inside the L badge: same mono voice, two digits wide,
   no chrome of its own — the badge is the chrome. */
.ws-lvl-n .ld-rank { width: 2ch; background: transparent; border: 0; padding: 0;
  font: inherit; color: inherit; text-align: center; }
.ws-lvl-n .ld-rank:focus { outline: none; color: var(--text); }
/* --panel, not the global's --panel2: these sit ON a --panel2 card. */
.ld-f { background: var(--panel); padding: 5px 8px; font-size: var(--fs-body-s); }
.ld-f[data-f="name"] { min-width: 150px; font-weight: 600; }
.ld-btns { display: flex; gap: 4px; margin-left: auto; }
.ld-btns button { font-size: var(--fs-small); padding: 2px 8px; }
.ld-btns button:disabled { opacity: .3; cursor: default; }
.ld-btns button[data-lact="del"]:hover { border-color: var(--bad); color: var(--bad); }

.ws-examples { color: var(--dim); font-size: var(--fs-body-s); margin-bottom: 10px; display: flex;
  align-items: center; gap: 8px; flex-wrap: wrap; }
.ws-examples .ex { font-size: var(--fs-small); padding: 4px 10px; border-radius: var(--r-ctl); }
.ws-examples .ex:hover { border-color: var(--accent); color: var(--text); }
.ws-examples .ex.ws-danger { margin-left: auto; }
.ws-examples .ex.ws-danger:hover { border-color: var(--bad); color: var(--bad); }
/* chrome from global control rule (restyle phase 1a, 2026-08-07) */
.dt-tpl-select { font-size: var(--fs-small); padding: 3px 6px; }

.ws-addlevel, .ws-additem { margin-top: 10px; }
/* chrome from global control rule (restyle phase 1a, 2026-08-07) */
.ws-addlevel input, .ws-additem input, .ws-additem select { padding: 6px 8px; font-size: var(--fs-body-s); }

/* Zones — an aligned editable grid. The coded name lives in a fixed-width
   gutter and indents to show hierarchy, so every following column lines up. */
.ws-scroll { overflow-x: auto; }
/* THE HEADING IS LIFTED OUT OF THE SCROLLER (owner 2026-08-31: headings
   "should stick on-screen, remaining visible until the last row is about to
   scroll off-screen").
   A sticky heading pins to its nearest SCROLLING ancestor, and `overflow-x:auto`
   already makes every one of these boxes a scroll container in BOTH axes, so a
   heading inside one can never pin to the page however it is styled.
   `overflow-y: clip` was tried first, to leave the page as the scroller: it
   computes to `hidden`, the box stays a scroll container, and the heading is
   captured exactly as before. Capping the pane at the window height was tried
   second: with ~482px of chrome above a Workspace grid the page can still
   over-scroll and carry the pane and its heading off the top, and making the
   pane sticky does not help, because its containing block is exactly its own
   height and it has no room to move.
   So the heading is moved into a RAIL of its own, a sibling above the pane
   which is not a scroll container in the vertical axis and therefore pins to the
   page. The page goes on being the vertical scroller, nothing is capped, and no
   grid loses height. `wsFloatHeads` keeps the rail's horizontal offset equal to
   the pane's, so the columns stay lined up while the grid scrolls sideways.
   The rail carries the SAME CLASSES as its pane, so every `.sched-table
   .bom-c`-shaped rule in this file goes on matching a heading cell that now
   lives somewhere else. */
.ws-headrail.ws-scroll { position: sticky; top: var(--topbar-h, 57px); z-index: 5;
  overflow-x: hidden; overflow-y: hidden; max-height: none; background: var(--panel); }
.ws-headrail .bom-head, .ws-headrail .dev-head { margin-bottom: 0; }
/* left-stripe sweep, 2026-08-07 — same conversion as .ws-leveldef above; a zone
   row and its level definition must go on wearing the same mark. */
/* A SELECT AND AN INPUT DO NOT AGREE ON THEIR OWN HEIGHT. The shared rule in
   style.css gives every control the same fill, border and radius but no height,
   so a native select renders shorter than a text input beside it — 23px against
   28px on this row, which reads as one field sagging in a row of them. Pinned
   together here, the same way .nic-row already does it. Found by measuring the
   rendered heights of neighbouring controls, 2026-08-13. */
.ws-item-row :where(input:not([type=checkbox]):not([type=radio]), select) { height: 28px; }
.ws-item-row { display: flex; align-items: center; gap: 8px; margin: 4px 0;
  padding: 5px 8px; border-radius: var(--r-soft);
  border: 1px solid color-mix(in srgb, var(--lvc, var(--line)) 45%, var(--line));
  border-top-color: var(--edge-hi-soft);
  background-color: var(--panel);
  background-image: linear-gradient(90deg, color-mix(in srgb, var(--lvc, transparent) 14%, transparent),
                                           color-mix(in srgb, var(--lvc, transparent) 4%, transparent) 55%,
                                           transparent); }
.zn-drag { cursor: grab; color: var(--dim); font-size: var(--fs-body); user-select: none; width: 14px; text-align: center; }
.zn-drag:active { cursor: grabbing; }
.zn-drag-sp { width: 14px; }

/* ---- dragging a zone (owner 2026-08-04) ---------------------------------
   The move is spelled out while it is still a proposal. Three states, and
   they must be distinguishable at a glance:
     .zn-can-drop — a legal destination, lit the moment you pick a zone up
     .zn-drop-ok  — the one you are over
     .zn-drop-no  — a row you cannot nest under, and why
   Rows that are neither recede, so the legal landing places are what is left
   to look at. */
.zn-drag-active .ws-item-row { transition: opacity .12s, box-shadow .12s; }
.zn-drag-active .ws-item-row:not(.zn-can-drop):not(.zn-dragging):not(.zn-drop-no) { opacity: .38; }
.zn-drag-active .zn-drag { cursor: grabbing; }
/* Legal destination — a quiet standing invitation, not a shout. */
.ws-item-row.zn-can-drop { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 55%, transparent);
  background: color-mix(in srgb, var(--accent) 8%, transparent); }
/* The one under the pointer. */
.ws-item-row.zn-drop-ok { box-shadow: inset 0 0 0 2px var(--accent);
  background: color-mix(in srgb, var(--accent) 20%, transparent); opacity: 1; }
/* A row that cannot take it. Red, but flat — it is a "no", not an error. */
.ws-item-row.zn-drop-no { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--bad) 60%, transparent);
  background: color-mix(in srgb, var(--bad) 10%, transparent); opacity: 1; cursor: not-allowed; }
/* The zone being carried, and the subtree coming with it. */
.ws-item-row.zn-dragging { opacity: .45; box-shadow: inset 0 0 0 1px var(--accent2); }
/* left-stripe sweep, 2026-08-07: the subtree coming along was marked by a 2px
   amber inset bar. Same amber, same "it is coming with", now a wash leading
   from the left edge — and it replaces the row's LEVEL wash for the duration of
   the drag, which is correct: while a move is being proposed, what matters is
   what is moving, not what level it is.
   THE FIVE DRAG STATES STILL READ APART, measured in the running app after the
   change, not assumed:
     .zn-can-drop  1px accent ring, faint accent fill, opacity 1
     .zn-drop-ok   2px accent ring, strong accent fill
     .zn-drop-no   1px red ring, red fill, not-allowed cursor
     .zn-dragging  amber RING over the row's own level wash, opacity .45
     .zn-kin       amber WASH, no ring, opacity .38
   dragging and kin are the closest pair and were already distinguished by
   ring-vs-bar; they are now distinguished by ring-vs-wash, which is a larger
   difference than the one it replaces, not a smaller one.
   THE `opacity: .5` BELOW HAS NEVER RENDERED, and is left exactly as found.
   `.zn-drag-active .ws-item-row:not(.zn-can-drop):not(.zn-dragging):not(.zn-drop-no)`
   is (0,5,0) against this rule's (0,2,0), so a kin row has always dimmed to
   .38 with it. Pre-existing, unrelated to stripes, and not silently repaired
   inside a paint change — flagged, not touched. */
.ws-item-row.zn-kin { opacity: .5;
  background-image: linear-gradient(90deg, color-mix(in srgb, var(--accent2) 20%, transparent),
                                           color-mix(in srgb, var(--accent2) 6%, transparent) 55%,
                                           transparent); }

/* The caption: what the drop will actually do, in words. */
.zn-dragcap { display: flex; align-items: center; gap: 8px; min-height: 30px;
  margin: 0 0 8px; padding: 6px 12px; border-radius: var(--r-soft); font-size: var(--fs-body-s);
  background: var(--panel2); color: var(--dim); border: 1px solid var(--line); }
.zn-dragcap[hidden] { display: none; }
.zn-dragcap b { color: var(--text); font-weight: 600; }
.zn-dragcap[data-state="ok"] { border-color: var(--accent); color: var(--text);
  background: color-mix(in srgb, var(--accent) 14%, transparent); }
.zn-dragcap[data-state="no"] { border-color: color-mix(in srgb, var(--bad) 60%, transparent);
  color: var(--bad); background: color-mix(in srgb, var(--bad) 10%, transparent); }

/* "Top level" as a target you can aim at, rather than whatever empty pixels
   happen to sit below the last row. Only present during a drag. */
.zn-rootdrop { display: none; margin: 8px 2px 2px; padding: 10px 12px;
  border: 1px dashed var(--line); border-radius: var(--r-soft); color: var(--dim);
  font-size: var(--fs-body-s); text-align: center; }
.zn-drag-active .zn-rootdrop { display: block; }
.zn-rootdrop.zn-drop-ok { border-style: solid; border-color: var(--accent); color: var(--text);
  background: color-mix(in srgb, var(--accent) 18%, transparent); }

@media (prefers-reduced-motion: reduce) {
  .zn-drag-active .ws-item-row { transition: none; }
}
.zn-collapse { flex: none; width: 20px; padding: 0; background: transparent; border: 1px solid transparent;
  color: var(--dim); font-size: var(--fs-mini); border-radius: 5px; }
.zn-collapse:hover { border-color: var(--line); color: var(--text); }
.zn-collapse-sp { flex: none; width: 20px; }
.zn-pd { display: inline-flex; gap: 4px; align-items: center; flex: none; }
.zn-gutter { flex: none; min-width: 96px; }
.zn-coded { display: inline-block; font-family: var(--mono); font-size: var(--fs-small); color: var(--text);
  border: 1px solid var(--line); border-radius: var(--r-ctl); padding: 3px 7px; white-space: nowrap; }
.ld-dot { width: 11px; height: 11px; border-radius: 3px; display: inline-block; flex: none; }
/* Coded-name colour matches its level's colour (inline when a level is
   assigned; these depth rules are the fallback) — LTC brand order
   (owner 2026-07-23): Yellow, Red, Blue lead, then Green, then the rest. */
/* Named through the shared --ramp-* ladder so Workspace depths and Operations
   phase tiers draw from one palette (owner 2026-08-07) — same colours as the
   brand order above, just aliased. */
.ws-depth-0 .zn-coded { color: var(--ramp-3); border-color: var(--ramp-3); }
.ws-depth-1 .zn-coded { color: var(--ramp-5); border-color: var(--ramp-5); }
.ws-depth-2 .zn-coded { color: var(--ramp-0); border-color: var(--ramp-0); }
.ws-depth-3 .zn-coded { color: var(--ramp-4); border-color: var(--ramp-4); }
.ws-depth-4 .zn-coded { color: var(--ramp-1); border-color: var(--ramp-1); }
.ws-depth-5 .zn-coded { color: var(--ramp-2); border-color: var(--ramp-2); }
/* Compact level select — reads like a label until hovered/focused. */
/* WIDE ENOUGH FOR THE NAME THE PROJECT USES. 92px was measured against the
   level names this app ships with (Site, Land, Building, Room); a project that
   calls a level "Segment" got SEGMEN and a cut-off arrow (owner 2026-09-07).
   Levels are named by whoever sets the project up, so the width follows the
   longest name rather than a number somebody guessed, with a floor so short
   names still line up and a ceiling so one long name cannot push the rest of
   the row off the screen. */
.zn-lvl-sel { flex: none; width: auto; min-width: 92px; max-width: 200px;
  background: transparent; border: 1px solid transparent;
  color: var(--dim); font-size: var(--fs-mini); text-transform: uppercase; letter-spacing: .04em;
  border-radius: var(--r-ctl); padding: 3px 4px; }
.zn-lvl-sel:hover, .zn-lvl-sel:focus { border-color: var(--line); color: var(--text); outline: none; }
/* chrome from global control rule (restyle phase 1a, 2026-08-07) */
.zn-f { padding: 5px 7px; font-size: var(--fs-body-s); }
.zn-f:focus { border-color: var(--accent); outline: none; }
.zn-name-in { flex: none; width: 210px; }
.zn-code-in { flex: none; width: 90px; }
.zn-size, .zn-size-ro { flex: none; display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; width: 120px; }
.zn-size-in { width: 64px; }
.zn-size-ro { color: var(--dim); font-family: var(--mono); font-size: var(--fs-small); }
.zn-size .unit, .zn-size-ro .unit { color: var(--dim); font-size: var(--fs-mini); }
/* Units engine adornment: the small, light, gray unit shown beside a value
   ($, sq ft, W, V, lbs…) in read-only cells and as an input suffix. */
.u-tag { color: var(--dim); font-size: var(--fs-mini); font-weight: 400; margin-left: 3px; white-space: nowrap; }
/* An input paired with its unit suffix. */
.u-field { display: inline-flex; align-items: baseline; gap: 3px; }
.u-field .u-tag { margin-left: 0; }
.ws-item-row .grow { flex: 1; min-width: 8px; }
.ws-item-row .rowbtns { display: flex; gap: 4px; }
.ws-item-row .rowbtns button { font-size: var(--fs-small); padding: 2px 8px; }
.ws-item-row .rowbtns button[data-act="del"]:hover { border-color: var(--bad); color: var(--bad); }

/* Attribute option-list editors (Settings tab). */
.ao-head { display: flex; gap: 8px; color: var(--dim); font-size: var(--fs-mini); text-transform: uppercase; letter-spacing: .04em; margin: 4px 0; }
.ao-head .ao-hcell { flex: 1; }
.ao-head .ao-hdel { flex: none; width: 28px; }
.ao-row { display: flex; gap: 8px; align-items: center; margin: 4px 0; }
/* chrome from global control rule (restyle phase 1a, 2026-08-07) */
.ao-f { flex: 1; padding: 6px 8px; font-size: var(--fs-body-s); }
.ao-f:focus { border-color: var(--accent); outline: none; }
.ao-del { flex: none; width: 28px; font-size: var(--fs-body-s); padding: 4px 0; }
.ao-del:hover { border-color: var(--bad); color: var(--bad); }
.ao-add { margin-top: 8px; font-size: var(--fs-body-s); }
/* One panel, several catalog sub-sections. */
.ao-cat { border-top: 1px solid var(--line); padding: 12px 0 4px; margin-top: 10px; }
.ao-cat:first-of-type { border-top: none; margin-top: 4px; }
.ao-cat-h { font-weight: 600; font-size: var(--fs-body-s); margin-bottom: 8px; }
.ao-cat .ao-row { max-width: 720px; }

/* Power Density hierarchy: Code → room types → W/sq ft. */
.pd-code { border: 1px solid var(--line); border-radius: var(--r-soft); padding: 10px 12px; margin: 8px 0; background: var(--panel2); }
.pd-code-h { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
/* --panel, not the global's --panel2: these sit ON a --panel2 card. */
.pd-code-in { background: var(--panel);
  padding: 6px 8px; font-size: var(--fs-body-s); font-family: var(--mono); width: 160px; }
.pd-code-del { flex: none; width: 28px; }
.pd-code-del:hover { border-color: var(--bad); color: var(--bad); }
.pd-rooms { margin-left: 16px; border-left: 1px dashed var(--line); padding-left: 12px; }
.pd-rhead { display: flex; gap: 8px; color: var(--dim); font-size: var(--fs-mini); text-transform: uppercase; letter-spacing: .04em; margin-bottom: 4px; }
.pd-rhead span:nth-child(1) { width: 200px; } .pd-rhead span:nth-child(2) { width: 140px; }
.pd-room { display: flex; gap: 8px; align-items: center; margin: 4px 0; }
.pd-room-in { background: var(--panel); padding: 6px 8px; font-size: var(--fs-body-s); }
.pd-room-in[data-f="area_type"] { width: 200px; } .pd-room-in[data-f="w_sqft"] { width: 140px; }
.pd-room-in:focus { border-color: var(--accent); outline: none; }
.pd-room-del { flex: none; width: 28px; }
.pd-room-del:hover { border-color: var(--bad); color: var(--bad); }
.pd-room-add, .pd-code-add { margin-top: 6px; font-size: var(--fs-body-s); }

/* Units (folded into Setup) + misc */
.ws-units { display: flex; gap: 34px; flex-wrap: wrap; align-items: center; }
.ws-set-row { display: flex; align-items: center; gap: 10px; }
.ws-set-row > label { color: var(--dim); font-size: var(--fs-body-s); }
/* Settings dropdowns (electrical service) — compact, matches the panel. */
/* chrome from global control rule (restyle phase 1a, 2026-08-07) */
.ws-set-select { padding: 5px 8px; font-size: var(--fs-body-s); }
.ws-set-select:disabled { opacity: .6; }
/* Force a settings row onto its own line within the wrapping .ws-units flow. */
.ws-set-break { flex-basis: 100%; }
/* Parked on a literal in phase 1a because 10px was neither the control radius
   nor the card radius; joined to --r-ctl now that the control radius IS the
   pill this chip was always trying to be. (restyle phase 1b, 2026-08-07) */
.ws-ro-tag { font-size: var(--fs-mini); color: var(--dim); border: 1px solid var(--line); border-radius: var(--r-ctl); padding: 2px 8px; text-transform: uppercase; letter-spacing: .05em; }
.tabs button.ws-tab-dev { color: var(--dim); opacity: .5; cursor: not-allowed; }

/* Device Types (Setup panel). */
/* THE ONE TRACK DEFINITION for Device Categories. The heading row and every
   data row are the same grid, so a label can never again sit over the wrong
   column -- there is nowhere to write a width twice. Category takes the slack
   and has a real floor, because the names are long ("FIXTURE - LIGHTING
   EFFECT") and were being truncated (owner 2026-08-28). */
/* ONE grid for the whole table, not one per row. Two content-sized tracks
   (the QC boxes, the row buttons) can only resolve to the same width if every
   row is measured together, so the heading and the rows are `display: contents`
   and their cells are all items of THIS grid. Separate grids per row gave the
   heading its own answer and put every label over the wrong column. */
.dt-table { display: grid;
  grid-template-columns: 92px minmax(300px, 1fr) 176px max-content max-content;
  gap: 6px 8px; align-items: center; }
.dt-grid { display: contents; }
.dt-head > span { color: var(--dim); font-size: var(--fs-mini); text-transform: uppercase;
  letter-spacing: .04em; align-self: end; }
/* Every control in the row fills its own track, and an input and a select must
   be the SAME pill: a native select is intrinsically taller than a padded
   input, which read as two different shapes side by side (his third note the
   same day). Height is stated once for both; the select keeps room for its
   own arrow. */
.dt-row .dt-f { height: 32px; box-sizing: border-box; width: 100%; min-width: 0; }
.dt-row select.dt-f { padding-right: 24px; }
/* chrome from global control rule (restyle phase 1a, 2026-08-07) */
.dt-f { padding: 6px 8px; font-size: var(--fs-body-s); }
.dt-letter { text-align: center; font-family: var(--mono); font-weight: 600; }
.dt-f:focus { border-color: var(--accent); outline: none; }
.dt-btns { display: flex; gap: 4px; }
.dt-btns button { font-size: var(--fs-small); padding: 2px 8px; }
.dt-btns button:disabled { opacity: .3; cursor: default; }
.dt-del:hover { border-color: var(--bad); color: var(--bad); }

/* Device List table (horizontally scrollable, quick inline edit). */
.dev-table { overflow-x: auto; padding-bottom: 6px; }
.dev-row { display: flex; align-items: stretch; width: max-content; }
/* Hover fill: see the GRID VOICE block at the foot of this file. Removed from
   here rather than overridden there — as a `background` shorthand it would
   keep resetting background-color under the new lift and erase the fills the
   lift is meant to stack on. */
.dev-head { border-bottom: 1px solid var(--line); margin-bottom: 2px; }
.dev-head .dev-c { color: var(--dim); font-size: var(--fs-mini); text-transform: uppercase; letter-spacing: .04em;
  display: flex; align-items: center; padding: 6px; }
/* Layout skeleton only. The cell/button chrome (padding, borders, focus paint,
   gutter width) is owned by the Spreadsheet-tight grids block below (~line 543,
   owner 2026-07-21); overlapping declarations were removed from here rather
   than left shadowed (2026-08-08 audit). */
.dev-actions { flex: none; display: flex; gap: 3px; align-items: center; }
.dev-actions button.dev-del:hover { border-color: var(--bad); color: var(--bad); }
.dev-c { flex: none; width: 134px; box-sizing: border-box; }
/* Fallback only. Device Type / Category are resizable like every other column,
   so app.js writes an inline width on each one from the user's stored widths
   (LEAD_COLS in renderDevices); these values are its starting defaults and must
   match. A width fixed HERE alone is what made them the only two columns on the
   page that could not be dragged. */
.dev-c.dev-code { width: 92px; } .dev-c.dev-type { width: 122px; }
.dev-c.dev-chk { width: 66px; justify-content: center; }
.dev-cell, .dev-a { width: 100%; background: var(--panel2); color: var(--text); font-size: var(--fs-small); box-sizing: border-box; }
.dev-code .dev-cell { font-family: var(--mono); }
.dev-cell:focus, .dev-a:focus { outline: none; }
.dev-chk .dev-a { width: auto; }
/* Code validation: duplicate codes are red; a code that doesn't start with its
   Type's letter is orange. Applied live client-side; !important so the flag
   survives :focus while typing. */
.dev-cell.dev-code-dup { border-color: var(--bad) !important; background: color-mix(in srgb, var(--bad) 18%, transparent); }
.dev-cell.dev-code-badtype { border-color: var(--warn) !important; background: color-mix(in srgb, var(--warn) 18%, transparent); }

/* Column-group header row (labeled, collapsible). */
.dev-grouprow .dev-actions, .dev-grouprow .dev-code, .dev-grouprow .dev-type { }
/* Group header spans exactly its columns: box-border width, no margin, and a
   right divider drawn as an inset shadow so it never adds width. */
.dev-grouphdr { flex: none; box-sizing: border-box; display: flex; align-items: center; gap: 4px;
  padding: 4px 8px; font-size: var(--fs-small); font-weight: 600; color: var(--text); overflow: hidden;
  background: var(--panel2); border-bottom: 1px solid var(--line); box-shadow: inset -1px 0 0 var(--line); }
.dev-grpbtn { flex: none; width: 18px; padding: 0; background: transparent; border: 1px solid transparent;
  color: var(--dim); font-size: var(--fs-mini); border-radius: 4px; }
.dev-grpbtn:hover { border-color: var(--line); color: var(--text); }
.dev-grouphdr.dev-grp-collapsed { width: 76px; }
.dev-grp-collapsed .dev-grp-lbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dim); font-weight: 500; font-size: var(--fs-mini); }
.dev-grp-collapsed-col { flex: none; width: 76px; box-sizing: border-box; }

/* Device List filter bar. */
.dev-filters { display: flex; align-items: center; gap: 8px; margin: 8px 0; flex-wrap: wrap; }
/* A label and the control it names are one thing. Without this the filter row
   wraps between "Sort" and its pull-down, leaving a caption stranded at the end
   of one line and its control at the start of the next. */
.dev-fgrp { display: inline-flex; align-items: center; gap: 6px; }
/* chrome from global control rule (restyle phase 1a, 2026-08-07) — the filter
   bar sits ABOVE the grid, so unlike the cells inside it these keep chrome. */
.dev-filters input, .dev-filters select { padding: 6px 8px; font-size: var(--fs-body-s); }
.dev-filters #dev-q { min-width: 220px; }
.dev-filters input:focus, .dev-filters select:focus { border-color: var(--accent); outline: none; }

/* Tier colours. Row colours follow the LTC brand order and are the FALLBACK —
   a level with an explicit colour has it applied inline by app.js, and that
   wins.
   Aliased to the shared --ramp-* ladder (owner 2026-08-07) — colours unchanged.

   ON THE POSITION OF THESE RULES (left-stripe sweep, 2026-08-07). They used to
   have to sit LAST in the file, and the old comment said so: they set the
   `border-left-color` LONGHAND, while .ws-leveldef / .ws-item-row set the
   `border` SHORTHAND, and between two declarations of equal specificity the
   later one wins — so being last was the whole reason the tier colour survived
   at all.

   That constraint is GONE. These rules now set a custom property, `--lvc`, and
   the three carriers read it. Source order no longer decides anything: an
   inline `--lvc` from app.js beats a class's by specificity, not by position,
   and no shorthand anywhere can overwrite a custom property.

   THEY STAY HERE ANYWAY. The contract test pins .ws-depth-0 and .ws-depth-5 and
   the six-colour ramp is one of the most-read things in this file; moving a
   block whose only remaining reason to move is tidiness buys nothing and costs
   every future reader their memory of where it lives. What changed is the
   REASON, and the reason is what a comment is for — this block is no longer
   fragile to reordering, so do not restore the old warning if the rules do get
   moved someday. */
.ws-depth-0 { --lvc: var(--ramp-3); }
.ws-depth-1 { --lvc: var(--ramp-5); }
.ws-depth-2 { --lvc: var(--ramp-0); }
.ws-depth-3 { --lvc: var(--ramp-4); }
.ws-depth-4 { --lvc: var(--ramp-1); }
.ws-depth-5 { --lvc: var(--ramp-2); }
/* A row with no level: no wash, and a plain neutral border. --lvc is restated
   as transparent rather than left unset because the carriers' fallback only
   applies when the property is MISSING, and the border is restated outright
   because `color-mix(transparent 45%, var(--line))` would have quietly served a
   55%-strength line instead of the real one. */
.ws-noleveledge { --lvc: transparent; border-color: var(--line);
  border-top-color: var(--edge-hi-soft); }

/* Bill of Materials tab (horizontally scrollable; a band per device). */
.bom-table { overflow-x: auto; padding-bottom: 6px; }
.bom-row { display: flex; align-items: stretch; width: max-content; }
/* Hover fill: see the GRID VOICE block at the foot of this file (same reason
   as .dev-row above — the shorthand was erasing .bom-primary-row's mark). */
.bom-head { border-bottom: 1px solid var(--line); margin-bottom: 2px; }
.bom-head .bom-c { color: var(--dim); font-size: var(--fs-mini); text-transform: uppercase; letter-spacing: .04em;
  display: flex; align-items: center; padding: 6px; }
.bom-actions { flex: none; width: 62px; display: flex; gap: 3px; align-items: center; padding: 3px 4px; }
.bom-actions button { font-size: var(--fs-small); padding: 2px 6px; }
.bom-actions button.bom-del:hover { border-color: var(--bad); color: var(--bad); }
.bom-c { flex: none; padding: 2px 3px; box-sizing: border-box; }
.bom-f { width: 100%; background: var(--panel2); color: var(--text); border: 1px solid var(--line);
  border-radius: 5px; padding: 5px 6px; font-size: var(--fs-small); box-sizing: border-box; }
.bom-f:focus { border-color: var(--accent); outline: none; }
.bom-f.bom-num { text-align: right; font-family: var(--mono); }
.bom-c.bom-ro { display: flex; align-items: center; font-family: var(--mono); font-size: var(--fs-small);
  color: var(--dim); padding: 5px 6px; white-space: nowrap; }
/* device band: code + category + count + subtotal + add */
.bom-band { display: flex; align-items: center; gap: 10px; width: max-content; min-width: 100%;
  background: var(--panel2); border-top: 1px solid var(--line); border-radius: 6px;
  padding: 6px 8px; margin-top: 10px; font-size: var(--fs-body-s); position: sticky; left: 0; }
.bom-band .bom-devcode { font-family: var(--mono); font-weight: 700; }
.bom-band .bom-count { font-size: var(--fs-small); }
.bom-band .bom-devtotal { font-family: var(--mono); font-size: var(--fs-small); margin-left: auto; }
.bom-band .bom-add { font-size: var(--fs-small); padding: 2px 9px; border-radius: var(--r-ctl); }
.bom-band .bom-add:hover { border-color: var(--accent); color: var(--text); }
.bom-total { font-size: var(--fs-body-s); margin-bottom: 8px; }
.bom-total strong { font-family: var(--mono); }

/* BOM Summary display: one row per device type (BOM × Qty scheduled), then
   the footer walk — subtotal, contingency, inflation, tax, total. */
.bom-sum-row .bom-c { display: flex; align-items: center; padding: 5px 6px;
  font-size: var(--fs-small); white-space: nowrap; }
.bom-sum-row .bom-sum-n { font-family: var(--mono); justify-content: flex-end; }
.bom-sum-flag { font-family: var(--sans); font-size: var(--fs-mini); color: var(--warn, var(--bad));
  border: 1px solid currentColor; border-radius: var(--r-ctl); padding: 0 7px; margin-left: 6px; }
.bom-sum-foot { margin-top: 14px; border-top: 2px solid var(--line); width: max-content;
  min-width: 100%; position: sticky; left: 0; }
.bom-sum-frow { display: flex; align-items: center; gap: 10px; padding: 5px 6px;
  border-bottom: 1px solid var(--line-soft); font-size: var(--fs-small); }
.bom-sum-flabel { flex: none; width: 130px; font-weight: 600; }
.bom-sum-fctl { flex: none; width: 420px; display: flex; align-items: center; gap: 6px; }
.bom-sum-fctl .bs-f { width: auto; }
/* Same coat as .bom-f, its OWN class: the shared BOM cell wiring must never
   catch these (they commit a whole object, not a line PATCH). */
.bs-f { background: var(--panel2); color: var(--text); border: 1px solid var(--line);
  border-radius: 5px; padding: 4px 6px; font-size: var(--fs-small); box-sizing: border-box;
  font-family: var(--mono); text-align: right; }
.bs-f:focus { border-color: var(--accent); outline: none; }
select.bs-f, .bs-f[data-bs="tax_note"] { font-family: var(--sans); text-align: left; }
.bom-sum-famt { flex: none; width: 130px; font-family: var(--mono); text-align: right;
  justify-content: flex-end; display: flex; }
.bom-sum-frange { flex: none; width: 210px; font-family: var(--mono); text-align: right;
  justify-content: flex-end; display: flex; }
.bom-sum-sub .bom-sum-famt, .bom-sum-total .bom-sum-famt { font-weight: 700; }
.bom-sum-total { border-top: 1px solid var(--line); border-bottom: none; font-size: var(--fs-body-s); }

/* Device List: sort/group controls, resizable columns, group bands, exports. */
.dev-head .dev-c { position: relative; }
.dev-grip { position: absolute; top: 0; right: -3px; width: 7px; height: 100%; cursor: col-resize; }
.dev-grip:hover { background: var(--accent); opacity: .5; border-radius: 2px; }
.dev-band { display: flex; align-items: center; gap: 10px; width: max-content; min-width: 100%;
  background: var(--panel2); border-top: 1px solid var(--line); border-radius: 6px;
  padding: 5px 8px; margin-top: 8px; font-size: var(--fs-body-s); position: sticky; left: 0; }
.dev-band .muted { font-size: var(--fs-small); }
.dev-filters select { max-width: 220px; }
#dev-sortdir { padding: 4px 8px; font-size: var(--fs-mini); }
/* export links (CSV/XLSX) in a panel header */
/* export button pair — same look as LTC Operations (button.small.ghost) */
.ws-export { float: right; display: inline-flex; gap: 6px; }
/* BOM primary-device line: quiet emphasis */
/* Was a frozen 7% of brand blue #3777bc, which is the LIGHT theme's accent —
   on dark it no longer matched the accent ink on the row beside it. Mixed from
   --accent, so the wash follows whichever blue the theme is using.
   (restyle phase 1a, 2026-08-07) */
.bom-primary-row { background: color-mix(in srgb, var(--accent) 7%, transparent); }
/* NOT ACCENT INK (owner, 2026-08-17: "why would this text be blue? it's
   generated, but I don't think it is referencing"). He was right, and the
   collision is close enough to be unfair: --accent is #649cd8 and the
   borrowed-cell convention's --calc is #5b9bd5, so a label meaning "this is
   the auto line" was wearing the colour that means "this value is followed
   from somewhere else". Worse, the cells BESIDE it genuinely are windows onto
   the Device List and were made plain in v0.155.0 — leaving the one cell that
   references nothing as the only blue one on the row.
   The row keeps its 7% wash above, which is what says the line is automatic;
   the label only has to be readable and firm. */
.bom-c.bom-primary { color: var(--text-2); font-family: inherit; font-weight: 600; }

/* Quiet cells (owner 2026-08-24): a cell whose Kind probably has nothing to
   say there dims instead of hiding — still editable, and only while EMPTY (a
   stored value is never dimmed). Focus or hover wakes the whole cell, so the
   moment somebody decides the cell does apply, it stops arguing. */
.dev-c.dev-quiet > * { opacity: .38; }
.dev-c.dev-quiet:focus-within > *, .dev-c.dev-quiet:hover > * { opacity: 1; }

/* Device List: computed BOM rollup cells, collapsible group bands. */
.dev-c.dev-ro { display: flex; align-items: center; font-family: var(--mono); font-size: var(--fs-small);
  color: var(--dim); padding: 5px 6px; white-space: nowrap; overflow: hidden; }
.dev-bandbtn { background: transparent; border: none; color: var(--dim); cursor: pointer;
  padding: 0 4px; font-size: var(--fs-small); }
.dev-bandbtn:hover { color: var(--text); border: none; }
/* BOM: collapsible category groups inside a device band. */
.bom-cathdr { display: flex; align-items: center; gap: 8px; width: max-content; min-width: 100%;
  padding: 3px 8px 3px 70px; font-size: var(--fs-small); color: var(--dim); position: sticky; left: 0;
  border-top: 1px dashed var(--line); }
.bom-cathdr .bom-catname { font-weight: 600; color: var(--text); }
.bom-cathdr .bom-catsub { font-family: var(--mono); margin-left: 10px; }
.bom-catbtn { background: transparent; border: none; color: var(--dim); cursor: pointer;
  padding: 0 2px; font-size: var(--fs-small); }
.bom-catbtn:hover { color: var(--text); border: none; }

/* BOM: one bordered panel per Device Type, encompassing its parts. */
.bom-devpanel { border: 1px solid var(--line); border-radius: var(--r-soft); margin-top: 12px;
  padding-bottom: 4px; width: max-content; min-width: 100%; box-sizing: border-box; }
.bom-devpanel .bom-band { border-top: none; margin-top: 0; border-radius: 7px 7px 0 0; }
/* Inside a device panel the band hugs its content (sticky-left), so the
   subtotal and + line stay visible instead of riding the far scroll edge. */
.bom-devpanel .bom-band { min-width: 0; width: max-content; padding-right: 14px; }

/* ---- Spreadsheet-tight grids (Device List, BOM, Device Schedule) ----------
   Owner 2026-07-21: tighter rows, continuous grid lines instead of per-input
   borders, quick to tab and arrow around. Inputs are borderless/transparent;
   focus paints the cell. Validation flags become inset outlines. */
.dev-table { border-top: 1px solid var(--line); }
.dev-row:not(.dev-grouprow):not(.dev-head) { border-bottom: 1px solid var(--line-soft); }
.dev-head .dev-c { padding: 4px 6px; }
.dev-c { padding: 0; border-right: 1px solid var(--line-soft); display: flex; align-items: center; }
.dev-c.dev-chk { justify-content: center; }
.dev-cell, .dev-a:not([type=checkbox]) { border: none; background: transparent; border-radius: 0; padding: 4px 6px; }
.dev-cell:focus, .dev-a:focus { background: var(--panel2); box-shadow: inset 0 0 0 1px var(--accent); outline: none; }
.dev-cell.dev-code-dup { box-shadow: inset 0 0 0 1px var(--bad) !important; }
.dev-cell.dev-code-badtype { box-shadow: inset 0 0 0 1px var(--warn) !important; }
.bom-table { border-top: 1px solid var(--line); }
.bom-row:not(.bom-head) { border-bottom: 1px solid var(--line-soft); }
.bom-head .bom-c { padding: 4px 6px; }
.bom-c { padding: 0; border-right: 1px solid var(--line-soft); display: flex; align-items: center; }
.bom-c.bom-chk { justify-content: center; }
.bom-f, .net-f { border: none; background: transparent; border-radius: 0; padding: 4px 6px; }
.bom-f:focus, .net-f:focus { background: var(--panel2); box-shadow: inset 0 0 0 1px var(--accent); outline: none; }
.bom-c.bom-ro { padding: 4px 6px; }
.dev-actions, .bom-actions { width: 56px; padding: 1px 3px; border-right: 1px solid var(--line-soft); }
.dev-actions button, .bom-actions button { padding: 1px 5px; font-size: var(--fs-mini); border: none; background: transparent; color: var(--dim); }
.dev-actions button:hover, .bom-actions button:hover { color: var(--text); border: none; }
/* Row VERBS hide until the row is under the pointer or holds the keyboard;
   the selection checkbox and the contents chevron sit OUTSIDE .row-tools
   because they carry state a person must be able to see at a glance
   (owner 2026-08-24: the resting grid should show data, not buttons).
   focus-within keeps every button reachable by keyboard alone. */
.bom-actions .row-tools, .dev-actions .row-tools { display: inline-flex; gap: 3px; align-items: center; opacity: 0; transition: opacity .12s; }
.bom-row:hover .row-tools, .bom-row:focus-within .row-tools,
.dev-row:hover .row-tools, .dev-row:focus-within .row-tools { opacity: 1; }
/* Racks & Enclosures carries one control more than any other table — chevron,
   duplicate, save-as-template, delete — and 56px is sized for two buttons, so
   the last one sat on top of the name in the next column. Scoped to .cn-table
   so every other table keeps its narrower gutter; the header rows share the
   class and therefore stay aligned with the data rows. The four controls need
   ~97px uncompressed on macOS; the rest is slack, because emoji glyph widths
   differ by platform and a too-tight column silently squeezes them back into
   the name.
   FIVE since 2026-08-17 — revision history joined them, because this is where
   a rack's contents are overridden. +22px keeps the same slack. */
.cn-table .bom-actions { width: 130px; }
/* The Device List hit the SAME bug from the other direction: 56px was right for
   its two controls until revision history added a third (v0.66.0), and nobody
   widened the gutter, so the trash can sat under the Device Type beside it.
   Measured at 75px for the three uncompressed on macOS; 84px leaves the same
   proportion of slack as the rack table above, for the same reason — emoji
   glyph widths differ by platform, and a too-tight column silently pushes the
   last control back under its neighbour rather than complaining.
   The header and group rows carry .dev-actions too, so they stay aligned. */
.dev-actions { width: 84px; }
/* The BOM lines grid took the same third control on 2026-08-17 and needs the
   same gutter as the Device List for the same reason. Scoped to .bom-lines so
   the other .bom-table grids (WAN, the address register, VLANs) keep theirs. */
.bom-lines .bom-actions { width: 84px; }

/* Device Schedule: level-hierarchy bands (indent + tier colour by depth).
   left-stripe sweep, 2026-08-07 — the third carrier of the .ws-depth-N ramp,
   converted with .ws-leveldef and .ws-item-row. All three had to move together
   or the same level would read three different ways across three tabs. */
.sched-band { display: flex; align-items: center; gap: 8px; width: max-content; min-width: 0;
  padding: 3px 12px 3px 6px; margin-top: 6px; font-size: var(--fs-body-s);
  background-color: var(--panel2);
  background-image: linear-gradient(90deg, color-mix(in srgb, var(--lvc, transparent) 14%, transparent),
                                           color-mix(in srgb, var(--lvc, transparent) 4%, transparent) 55%,
                                           transparent);
  border: 1px solid color-mix(in srgb, var(--lvc, var(--line)) 45%, var(--line));
  border-top-color: var(--edge-hi-soft);
  border-radius: 6px; position: sticky; left: 0; }
.sched-band .bom-count { font-size: var(--fs-small); }
.sched-band .bom-devtotal { font-family: var(--mono); font-size: var(--fs-small); margin-left: 8px; }
/* The band's own checkbox: everything under it, at any depth. Sized to the
   band the way the row checkbox is sized to the row. */
.sched-band .sc-band-pick { width: 14px; height: 14px; margin: 0; flex: none;
  cursor: pointer; accent-color: var(--accent); }
.sched-bandbtn { background: transparent; border: none; color: var(--dim); cursor: pointer; padding: 0 2px; font-size: var(--fs-small); }
.sched-bandbtn:hover { color: var(--text); border: none; }
.sched-lvl { font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .05em; color: var(--dim);
  border: 1px solid var(--line); border-radius: var(--r-ctl); padding: 1px 6px; }

/* Grips need a positioned header cell to anchor to (the Device List already
   has this via .dev-head .dev-c) — without it the BOM/Schedule grips rendered
   off-target and couldn't be grabbed. */
.bom-head .bom-c { position: relative; }
.dev-grip { z-index: 2; }

/* Network tab: VLAN rows, reservations, NIC panels, and the address register.
   NIC panels appear under the row that owns them, on every grid that can hold
   an address (Device List template, Device Schedule, Racks & Enclosures). */
.nic-btn { background: var(--panel2); color: var(--text); border: 1px solid var(--line);
  border-radius: var(--r-ctl); padding: 2px 7px; font-size: var(--fs-small); cursor: pointer; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Dead fallback dropped — see .net-bar-used. (restyle phase 1b, 2026-08-07) */
.nic-btn:hover { border-color: var(--accent); }
.nic-btn-tpl { opacity: .72; font-style: italic; }
/* left-stripe sweep, 2026-08-07: not on the owner's census, found by the final
   grep, same pattern and so the same conversion. The 3px accent edge said "this
   panel belongs to the row above it"; the accent now says it as a leading wash
   and an evenly tinted border, and the lopsided `0 8px 8px 0` radius that
   existed to sit beside the bar goes with it. */
.nic-panel { border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  border-top-color: var(--edge-hi-soft); border-radius: var(--r-soft);
  background-color: var(--panel2);
  background-image: linear-gradient(90deg, color-mix(in srgb, var(--accent) 12%, transparent),
                                           color-mix(in srgb, var(--accent) 4%, transparent) 55%,
                                           transparent);
  margin: 2px 0 6px 34px; padding: 6px 8px; }
.nic-row { display: grid; gap: 5px; align-items: center; margin-bottom: 3px;
  grid-template-columns: 1.1fr 1.2fr .8fr 1.1fr 1.1fr 1.1fr 1.2fr 1fr .9fr .7fr 1.3fr 26px; }
.nic-row-tpl { grid-template-columns: 1.4fr 1.6fr 3fr 26px; }
/* --panel (the panel shell is --panel2) and a tighter 5px corner than the
   global control radius — the rest comes from the global rule. */
/* ONE TABLE STYLE PER CARD (owner 2026-08-20: "Why are there two different
   visual styles for tables on the same page. pill-shapes and rounded
   rectangle... with 2 different background colors").

   A controller card draws four tables — applications, NICs, control
   input/output, and control scope. The first two were shaped here; the other
   two carried no shape of their own and so fell through to the generic control
   style, which is a PILL (--r-ctl is 999px). Four tables, two shapes, on one
   screen.

   Converging on this one rather than inventing a third: it is already the
   majority, it is what the Device Schedule's NIC editor uses, and the height
   line below is a fix the owner asked for on this very card. The pill stays
   the shape of a BUTTON, which is what --r-ctl is for; a cell in a table is
   not a control you press. */
.nic-row input, .nic-row select,
.ctl-fnrow :where(input:not([type=checkbox]), select),
.ctl-add select,
.ctl-via { background: var(--panel);
  border-radius: 5px; padding: 3px 6px; font-size: var(--fs-small); min-width: 0; }
.nic-row input, .nic-row select { width: 100%; }
/* AND ONE HEIGHT. Equal padding and font do not produce equal heights: a native
   <select> adds its own metrics, so VLAN and Mode stood 1.5px taller than every
   box beside them on all four grids that draw this editor. Found by measuring
   the row after the owner pointed at the same mismatch on the controller card
   (2026-08-13). Stated as a height, so it cannot drift again. */
.nic-row :where(input:not([type=checkbox]), select),
.ctl-fnrow :where(input:not([type=checkbox]), select),
.ctl-add select,
.ctl-via { height: 22px; line-height: 1.2; }
.nic-row.nic-head { font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .05em; color: var(--dim); }
/* The address as four octets, written as one address rather than four boxes:
   no borders, the VLAN-dictated octets gray like any other calculated value,
   what the user types in normal text. Trouble tints the WHOLE cell, matching
   the Device List's duplicate/mismatch convention. */
.nic-ip { display: flex; align-items: center; min-width: 0; width: 100%;
  border-radius: 4px; padding: 0 2px; }
.nic-ip input.nic-oct { width: 100%; min-width: 0; text-align: center;
  background: transparent; border: none; border-radius: 3px; box-shadow: none;
  color: var(--text); font-size: var(--fs-small); padding: 4px 0;
  font-variant-numeric: tabular-nums; }
.nic-ip input.nic-oct:focus { outline: none; background: var(--panel2);
  box-shadow: inset 0 0 0 1px var(--accent); }
.nic-ip input.nic-oct::placeholder { color: var(--dim); }
/* the octets the VLAN establishes — calculated, so gray and not typeable */
.nic-ip input.nic-oct-fixed { color: var(--dim); cursor: default; }
.nic-dot { color: var(--dim); flex: none; font-size: var(--fs-mini); padding: 0 1px; }
.nic-ip-warn { background: color-mix(in srgb, var(--warn) 18%, transparent); box-shadow: inset 0 0 0 1px var(--warn); }
.nic-ip-dup { background: color-mix(in srgb, var(--bad) 18%, transparent); box-shadow: inset 0 0 0 1px var(--bad); }
/* a value that isn't in the right format (a MAC that isn't a MAC) — same
   orange as an address that can't work; !important so it survives :focus */
input.net-bad-fmt { background: color-mix(in srgb, var(--warn) 18%, transparent) !important;
  box-shadow: inset 0 0 0 1px var(--warn) !important; }
.nic-row-inherit input, .nic-row-inherit select { opacity: .8; }
.nic-del, .res-del { background: none; border: 0; color: var(--dim); cursor: pointer; font-size: var(--fs-small); }
.nic-del:hover, .res-del:hover { color: #e66; }
.nic-foot { display: flex; align-items: center; gap: 10px; justify-content: space-between;
  margin-top: 4px; font-size: var(--fs-small); }
.nic-empty { font-size: var(--fs-small); padding: 2px 0; }

.vlan-row .bom-c { align-items: center; }
/* ONE HEIGHT ACROSS THE VLAN ROW, stated so it cannot drift (the nic-row
   lesson): equal padding and font do not produce equal heights, and the
   color picker stood 3px taller than the fields beside it. */
.vlan-row .net-f, .vlan-res-btn, .vlan-row .cpick-btn { height: 27px; box-sizing: border-box;
  padding-top: 0; padding-bottom: 0; }
/* the reserved-ranges toggle has its own column, after DHCP */
.vlan-res-c { justify-content: center; }
.vlan-res-btn { background: var(--panel2); color: var(--text); border: 1px solid var(--line);
  /* 5px, not the pill: it sits in a row of 5px table fields (uniformity
     review 2026-08-25 — "the clearest Workspace mismatch"). */
  border-radius: 5px; padding: 4px 8px; font-size: var(--fs-small); cursor: pointer;
  font-variant-numeric: tabular-nums; }
.vlan-res-btn:hover { border-color: var(--accent); }
/* The reservations panel is indented — the indent is what says "this
   expansion belongs to the VLAN row above it" (owner 2026-08-16, and the
   ruling covers every attached expansion). Its size is the Number column's
   width, set inline by the wiring from the rendered row, so the panel begins
   where VLAN Name begins even after a column drag. */
/* delete sits at the FAR RIGHT of the row, past every column */
.bom-row .bom-c.vlan-actions { width: 46px; flex: none; justify-content: center;
  border-right: none; border-left: 1px solid var(--line-soft); }
.vlan-actions button { background: none; border: none; color: var(--dim);
  font-size: var(--fs-small); padding: 2px 5px; cursor: pointer; }
.vlan-actions button:hover { color: var(--bad); }
.res-row { display: grid; grid-template-columns: 1.6fr 1.1fr 1.1fr 1.1fr .6fr 26px;
  gap: 5px; align-items: center; margin-bottom: 3px; }
.res-row input, .res-row select { background: var(--panel);
  border-radius: 5px; padding: 3px 6px; font-size: var(--fs-small); width: 100%; min-width: 0; }
.res-row.res-head { font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .05em; color: var(--dim); }
.res-row.res-bad input { border-color: #c55; }
.res-count { font-size: var(--fs-small); text-align: right; font-variant-numeric: tabular-nums; }

/* VLAN field color code — the swatch mirrors the patch cable / keystone. */
.vlan-swatch { display: inline-block; width: 11px; height: 11px; border-radius: 3px;
  border: 1px solid rgba(128, 128, 128, .55); vertical-align: -1px; margin-right: 6px; flex: none; }
.vlan-swatch-none { background: repeating-linear-gradient(45deg, transparent, transparent 2px, var(--line) 2px, var(--line) 4px); }
/* Network inputs get their own class — `.bom-f` carries BOM save behaviour. */
/* KEEPS its chrome on purpose (restyle phase 1a, 2026-08-07): `.bom-f, .net-f`
   above strips the border and fill for the dense grids, and this rule is what
   puts them back for the Network tab. Same specificity, later wins — so
   deleting a declaration here does not fall through to the global control
   rule, it falls through to `background: transparent; border: none`. */
.net-f { width: 100%; background: var(--panel2); color: var(--text); border: 1px solid var(--line);
  border-radius: 5px; padding: 5px 6px; font-size: var(--fs-small); box-sizing: border-box; }
.net-f:focus { border-color: var(--accent); outline: none; }
.vlan-color-c { display: flex; align-items: center; }
/* color picker: swatch + name button, menu of swatch + name rows */
.cpick { position: relative; display: block; width: 100%; min-width: 0; }
.cpick-btn { display: flex; align-items: center; gap: 6px; width: 100%; min-width: 0;
  /* The same chrome as .net-f beside it: the picker is a field in a row of
     fields, and a borderless cell in a bordered row read as a gap
     (uniformity review 2026-08-25). */
  background: var(--panel2); color: var(--text); border: 1px solid var(--line); border-radius: 5px;
  padding: 5px 6px; font-size: var(--fs-small); cursor: pointer; text-align: left; }
.cpick-btn:hover:not(:disabled) { border-color: var(--accent); }
.cpick-btn:disabled { cursor: default; opacity: .6; }
.cpick-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cpick-caret { color: var(--dim); font-size: var(--fs-mini); flex: none; }
/* FIXED, not absolute: the grids scroll (overflow-x: auto), which would clip
   an absolutely-positioned menu. Placed from the button's rect on open. */
.cpick-menu { position: fixed; z-index: 200; min-width: 160px;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-soft);
  padding: 4px; display: flex; flex-direction: column;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .45); max-height: 280px; overflow-y: auto; }
.cpick-menu[hidden] { display: none; }
.cpick-opt { display: flex; align-items: center; gap: 8px; background: none; border: none;
  color: var(--text); font-size: var(--fs-small); padding: 5px 8px; border-radius: 5px;
  cursor: pointer; text-align: left; white-space: nowrap; }
.cpick-opt:hover { background: var(--panel2); }
.cpick-opt.cpick-on { font-weight: 700; }
.nic-vlan { display: flex; align-items: center; min-width: 0; }
.nic-vlan select { min-width: 0; }

/* Was a hand-picked amber two hex digits away from the app's override amber:
   close enough that nobody could see the difference, far enough that they never
   actually matched. Same meaning ("look at this — it is not wrong"), so the
   same token. (restyle phase 1b, 2026-08-07) */
.net-warn { color: var(--ovr); }
.net-bad { color: #e06a6a; font-weight: 600; }
.net-findings ul { margin: 4px 0 0; padding-left: 20px; font-size: var(--fs-body-s); }
.net-findings li { margin: 3px 0; }
/* Sized for dozens of VLANs: compact cards, every line fixed, name and CIDR
   clipping rather than wrapping so no card is taller than its neighbour. */
.net-utils { display: grid; grid-template-columns: repeat(auto-fill, minmax(255px, 1fr)); gap: 10px; }
/* left-stripe sweep, 2026-08-07: app.js used to hand this card its VLAN colour
   as `border-color` plus an `inset 3px 0 0` bar. It now hands over the raw hex
   in --vlc and the card decides.
   THE COLOUR CODE IS THE POINT OF THIS CARD. A VLAN's field colour is what the
   patch cables, keystones and labels are; a wall of these cards is scanned BY
   colour before anything is read. So this is the strongest wash in the app —
   18% leading, and the full border at 55% rather than the usual 30–45% — and
   it is a 135° corner-to-corner wash rather than a 90° row wash, because a
   card is a card, not a row. The floor was 16%/50%; it reads better one step
   up on a --panel2 field. Do not quieten it without checking a full grid of
   ten VLANs side by side. The left padding stays at 13px: it was never the
   bar's space, it is the card's.
   The border mixes toward --line, not toward transparent, for two reasons: it
   stays OPAQUE, which is how it held its own against the old raw-hex border,
   and a VLAN with no colour set falls back to exactly plain --line rather than
   to a half-strength ghost of it. */
.net-util { border-radius: var(--r-soft);
  border: 1px solid color-mix(in srgb, var(--vlc, var(--line)) 65%, var(--line));
  border-top-color: var(--edge-hi-soft);
  background-color: var(--panel2);
  background-image: linear-gradient(135deg, color-mix(in srgb, var(--vlc, transparent) 18%, transparent),
                                            color-mix(in srgb, var(--vlc, transparent) 5%, transparent));
  padding: 8px 10px 8px 13px; font-size: var(--fs-small);
  /* A BUTTON since 2026-08-13 — the card opens the VLAN's address map, and the
     only way in must be reachable from a keyboard. These four lines undo what
     a button brings with it; everything above is the card it already was. */
  font-family: inherit; color: inherit; text-align: left; cursor: pointer;
  display: block; width: 100%; }
.net-util:hover { border-color: color-mix(in srgb, var(--vlc, var(--accent)) 85%, var(--accent)); }
.net-util:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* THE COLOUR CODE IS THE POINT OF THIS CARD (see above), and on the light theme
   the shared top-edge token resolves to a neutral line — which would leave three
   coloured sides and one grey one. This card is the exception: its top edge is
   its own colour, so the ring stays whole AND stays readable as a VLAN colour. */
:root[data-theme="light"] .net-util {
  border-top-color: color-mix(in srgb, var(--vlc, var(--line)) 65%, var(--line)); }
.net-util-h { display: flex; justify-content: space-between; align-items: baseline; gap: 8px;
  margin-bottom: 5px; }
.net-util-h strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.net-util-h .muted, .net-util-h .net-warn { flex: none; font-size: var(--fs-mini); }
.net-util-l { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.5; }
.net-bar { display: flex; height: 7px; border-radius: var(--r-ctl); overflow: hidden;
  background: var(--line); margin-bottom: 5px; }
.net-bar span { display: block; height: 100%; }
/* Both chips ALWAYS render, on or off, so a wall of cards keeps every line at
   the same height and reads as columns rather than shifting about. */
.net-tags { display: flex; gap: 5px; margin-bottom: 5px; }
.net-tag { font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .05em;
  border: 1px solid var(--line); border-radius: var(--r-ctl); padding: 1px 8px;
  color: var(--dim); opacity: .35; }
.net-tag-on { opacity: 1; color: var(--text); border-color: var(--accent); }
/* The reserved slice of an address bar is the same "deliberate, not broken"
   amber as .net-warn — unified off the same near-miss literal. The fallback
   argument on --accent went with it: --accent has been declared on :root since
   the theme blocks existed, so the var() could only ever resolve to --accent,
   and the dead second argument read as a real alternative colour that a reader
   had to check. (restyle phase 1b, 2026-08-07) */
.net-bar-used { background: var(--accent); }
.net-bar-res { background: var(--ovr); }
/* Address Register: a real grid — cells clip instead of bleeding into their
   neighbour, headings sort, and the grip on each heading resizes. */
/* Group by / Sort by, above the register (owner 2026-08-13). */
.net-regbar { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.net-regsel { display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fs-mini); text-transform: uppercase; letter-spacing: .06em; color: var(--dim); }
.net-regsel select { text-transform: none; letter-spacing: 0; font-size: var(--fs-small); }
/* A band inside the register. It has to sit at the left edge and stay there,
   because the register scrolls sideways and a heading that slides away names
   nothing. */
.net-reg-band { position: sticky; left: 0; }
/* Rows sit UNDER their heading, and the indent is on the first cell's CONTENT
   rather than on the row: indenting the row would push every cell right and
   the columns would no longer line up with the headings they belong to
   (owner 2026-08-13). */
.net-reg-grouped .net-reg-row .bom-c[data-nck="owner"] { padding-left: 20px; }
.net-reg-grouped .net-reg-band { padding-left: 6px; }
/* A band folds shut. The chevron is the Racks & Enclosures band button, so it
   looks and behaves the way every other band in this app does. */
.net-regfold { margin-right: 2px; }
.net-reg-row .bom-c { font-size: var(--fs-small); padding: 2px 6px; }
.net-reg-row .bom-c, .net-reg-row .bom-c.bom-ro {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.net-reg-row .bom-c input, .net-reg-row .bom-c select { min-width: 0; max-width: 100%; }
.net-vlan-c { display: flex; align-items: center; }
/* the heading itself must NOT clip — the resize grip sits 3px proud of the
   cell edge; only the label text ellipses */
.net-hdr { position: relative; }
/* The register's Multicast column group: the fold button in both states, and
   the shut group's stub cell (a dot when the row holds multicast data). */
.net-mcbtn { background: transparent; border: none; color: var(--dim); font: inherit;
  font-size: var(--fs-mini); text-transform: uppercase; letter-spacing: .04em;
  cursor: pointer; padding: 0 2px; }
.net-mcbtn:hover { color: var(--text); }
.net-mcstub { justify-content: center; color: var(--accent); }
.net-hdr .net-sortbtn, .net-hdr .vlan-sortbtn { background: none; border: 0; padding: 0; cursor: pointer;
  color: inherit; font: inherit; text-transform: inherit; letter-spacing: inherit;
  display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.net-hdr .net-sortbtn:hover, .net-hdr .vlan-sortbtn:hover { color: var(--text); }
.net-hdr-on .net-sortbtn, .net-hdr-on .vlan-sortbtn { color: var(--text); font-weight: 700; }
/* its own class, not .dev-grip — the Device List binds a resize handler to
   that one and would double-bind these */
.net-grip { position: absolute; top: 0; right: -3px; width: 7px; height: 100%;
  cursor: col-resize; z-index: 2; }
.net-grip:hover { background: var(--accent); opacity: .5; border-radius: 2px; }
/* an address that cannot work reads orange; a duplicate reads red */
/* a gentle note — worth saying, not worth alarming about */
.net-note { color: var(--dim); }
.net-note::marker { color: var(--dim); }

/* Reports tab: card grid of report generators. */
.report-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; }
.report-group-h { font-weight: 700; font-size: var(--fs-small); text-transform: uppercase;
  letter-spacing: .06em; color: var(--dim); margin: 18px 0 10px; }
.report-group-h:first-of-type { margin-top: 4px; }
.report-card { border: 1px solid var(--line); border-radius: var(--r-card); background: var(--panel2);
  padding: 14px 16px; display: flex; flex-direction: column; gap: 8px; }
.report-card .report-title { font-size: var(--fs-body); font-weight: 600; }
.report-card .report-desc { font-size: var(--fs-small); flex: 1; }
.report-card .report-btn { align-self: flex-start; }
.report-card .report-btn:disabled { opacity: .5; cursor: not-allowed; }
/* A report's own option (max circuits per page, VLANs per page) sits in the
   report window's toolbar beside paper size and orientation — every card is
   just title, description, button (owner 2026-08-05). */
.rpt-winopt-l { display: inline-flex; align-items: center; gap: 6px; margin-left: 8px;
  font-size: 12px; color: var(--dim); white-space: nowrap; }
.rpt-winopt { background: var(--panel2); color: var(--text); border: 1px solid var(--line);
  border-radius: 6px; padding: 3px 6px; font-size: 12px; }
/* The number box, and only the number box. Site Conditions brought checkboxes
   into this toolbar and a 62px checkbox is a 62px hit area with a tick lost
   somewhere inside it. */
input.rpt-winopt[type="number"] { width: 62px; }
input.rpt-winopt[type="checkbox"] { padding: 0; }
/* A quiet lead-in for the run of include/exclude ticks, so they read as a set
   rather than as five unrelated switches. */
.rpt-winopt-g { font-size: 11px; color: var(--dim); text-transform: uppercase;
  letter-spacing: .06em; margin-left: 14px; }
/* Site Conditions puts eight controls up here. They wrap onto a second line
   rather than running off the right-hand edge of the window. */
.rpt-toolbar { display: flex; flex-wrap: wrap; align-items: center; row-gap: 6px; }
.rpt-toolbar .pr-paper { flex-wrap: wrap; row-gap: 6px; }

/* Report Header & Footer editor (a Setup section, and folded above the report
   cards). THE EDITOR IS A MAP OF THE SHEET (owner 2026-08-25): each control
   sits where its slot prints — header box with left / center / right columns,
   the separator choice under it, a dashed spacer standing in for the body,
   then the footer's separator and the footer box. */
/* Configuration on the left, a live example on the right (owner 2026-08-25).
   The example is the REAL furniture from print-doc.js on a miniature sheet,
   scaled with zoom so its type keeps the printed proportions. */
.hf-split { display: flex; gap: 30px; align-items: flex-start; flex-wrap: wrap; }
.hf-split > .hf-map { flex: 1 1 560px; max-width: 760px; }
.hf-example { flex: 1 1 420px; min-width: 360px; max-width: 620px;
  display: flex; flex-direction: column; gap: 8px; }
.hf-example > .hf-group-h { text-align: center; }
.hf-ex-page { zoom: 0.58; width: 733px; padding: 24px 42px; border-radius: 4px;
  box-shadow: 0 1px 6px rgba(0, 0, 0, .35); }
.hf-ex-body { display: flex; flex-direction: column; gap: 14px; padding: 22px 0 26px; }
.hf-ex-body > div { height: 11px; background: #ececec; border-radius: 3px; }
.hf-ex-body > div:nth-child(2) { width: 82%; }
.hf-ex-body > div:nth-child(4) { width: 64%; }
.hf-map { display: flex; flex-direction: column; gap: 8px; max-width: 1100px; }
/* minmax(0, 1fr), not 1fr: a 1fr track cannot shrink below its content's
   min width, and a label + select + custom input row would push the columns
   over each other instead of wrapping. */
/* Three IDENTICAL tracks (owner): minmax(0, 1fr) so no column can grow on
   its content and break the equality — long rows wrap inside their box. */
.hf-sheetbox { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px 14px; }
/* Each alignment group is its own box (owner: "group the left aligned things
   in a box… centered… right aligned"), so the sheet row itself draws no
   chrome — boxes inside a box would be a card in a card. */
.hf-col { display: flex; flex-direction: column; gap: 6px; min-width: 0;
  border: 1px solid var(--line); border-radius: 6px; background: var(--panel2);
  padding: 8px 12px; }
.hf-col .ws-set-row { flex-wrap: wrap; row-gap: 4px; }
.hf-col .ws-set-row > label { white-space: nowrap; }
/* One label width per column, so the selects sit in a straight rank. */
.hf-col.hf-l .ws-set-row > label { min-width: 96px; }
/* A control may never overhang its box: it shrinks to the row (the browser
   ellipsizes a select's text itself), and the long ones wrap below their
   label instead of poking past the border. */
.hf-col .ws-set-row > select, .hf-col .ws-set-row > input { max-width: 100%; min-width: 0; }
.hf-col.hf-c { align-items: center; text-align: center; }
.hf-col.hf-r { align-items: flex-end; text-align: right; }
/* The region names and the two separator choices sit centred: they belong to
   the whole width of the sheet, not to a column of it. */
.hf-map > .hf-group-h { text-align: center; }
.hf-map > .ws-set-row { justify-content: center; }
.hf-body { border: 1px dashed var(--line); border-radius: 6px; text-align: center;
  font-size: 12px; padding: 10px 14px; }
.hf-group-h { font-weight: 700; font-size: var(--fs-small); text-transform: uppercase;
  letter-spacing: .06em; color: var(--dim); margin: 2px 0 0; }
.hf-txt { flex: 0 1 200px; min-width: 120px; }
.hf-fixed { font-weight: 600; }
.hf-note { font-size: 12px; max-width: 340px; line-height: 1.45; }
.hf-fold > summary { cursor: pointer; font-weight: 600; font-size: var(--fs-body);
  padding: 12px 16px; }
.hf-fold[open] > summary { padding-bottom: 4px; }
.hf-fold > .hf-map { padding: 4px 16px 14px; }

/* Reports render as a light document page (matches what prints). */
.rpt-page { background: #fff; color: #111; border-radius: 10px; padding: 26px 30px; max-width: 1100px; }
.rpt-page .rpt-head { display: flex; justify-content: space-between; align-items: baseline;
  border-bottom: 2px solid #111; padding-bottom: 8px; margin-bottom: 14px; gap: 20px; }
.rpt-page h1 { font-size: 18px; margin: 0; color: #111; }
.rpt-page .proj { text-align: right; font-size: 12px; }
.rpt-page h2 { font-size: 14px; margin: 18px 0 6px; color: #111; }
.rpt-page table { border-collapse: collapse; width: 100%; margin-bottom: 10px; }
.rpt-page th, .rpt-page td { border: 1px solid #999; padding: 3px 7px; text-align: left;
  vertical-align: top; font-size: 12px; color: #111; background: transparent; }
.rpt-page th { background: #eee; font-size: 10px; text-transform: uppercase; letter-spacing: .04em; }
.rpt-page td.num, .rpt-page th.num { text-align: right; font-variant-numeric: tabular-nums; }
.rpt-page tr.total td { font-weight: 700; border-top: 2px solid #111; }
.rpt-page .muted, .rpt-page .rpt-muted { color: #555; }
.rpt-page .panel { break-inside: avoid; }

/* Repeating title block: every report is wrapped in a one-column table whose
   THEAD holds the header. A table-header-group is the only construct browsers
   reliably repeat at the top of each printed sheet, so the logo, report title,
   project identity, and generated stamp appear on every page. The wrapper's
   own cells must shed the report table styling below (borders, padding). */
.rpt-sheet { width: 100%; border-collapse: collapse; margin: 0; }
.rpt-sheet > thead { display: table-header-group; }
.rpt-sheet > tbody { display: table-row-group; }
.rpt-page .rpt-sheet > thead > tr > td,
.rpt-page .rpt-sheet > tbody > tr > td {
  border: 0; padding: 0; background: transparent; vertical-align: top; font-size: inherit; }
/* the repeated header needs air beneath it on the sheets that follow */
@media print { .rpt-sheet > thead > tr > td .rpt-head { margin-bottom: 12px; } }

/* MEANING-BEARING FILLS MUST SURVIVE THE PRINTER (owner 2026-08-15: the
   Device Type box printed as gray text with no box, and the section pills
   vanished). Browsers strip background colors on paper unless the page says
   `print-color-adjust: exact` — the "print backgrounds" checkbox is off by
   default and nobody should have to find it. On these sheets a background is
   not decoration: the identity box is white-on-black, a pill IS its
   background, a VLAN chip is only its color. Unconditional rather than inside
   @media print: the property has no effect on screen, and this way its
   presence can be verified without emulating a printer. */
.rpt-page, .rpt-page *, .doc-page, .doc-page * {
  -webkit-print-color-adjust: exact; print-color-adjust: exact; }

@media print {
  /* Page furniture must never print: the shared footer and its astronaut are
     absolutely positioned and land on top of the report body, and the fixed
     overlays (save status, toasts, dev hint) would stamp onto every sheet. */
  #topbar, .rpt-toolbar, #app-footer, .foot-astro,
  #save-status, #ws-notice, #dev-hint, .dev-mode-toggle { display: none !important; }
  body { background: #fff; }
  main, .ws-wrap { padding: 0; max-width: none; }
  .rpt-page { border-radius: 0; padding: 0; max-width: none; }
  /* The mount scrolls sideways on screen (overflow-x: auto) so a Tabloid
     sheet does not widen the page. In print that clip hides the sheets from
     WebKit's document width, and WebKit derives the page height from that
     width: the sheets spilled a sliver page each (2026-09-19, style.css
     .doc-page has the measurement). Operations' .pr-stage already did this. */
  #rpt-mount, .rpt-page { overflow: visible !important; }   /* the mount's overflow-x is inline */
}

/* Panel Schedules cards (pnl-*): one swept card per page of a panel, on
   the SHARED paginator since 2026-08-15 — the report's own page-break CSS
   and per-panel page numbers are gone; chunking at max-circuits-per-page
   is data, fitting is the engine's. Same clipped-heading / corner-
   CONTINUED dress as the rest of the card family. */
.rpt-page h2.pnl-conth { position: absolute; width: 1px; height: 1px; margin: 0;
  padding: 0; overflow: hidden; }
.rpt-page h2.pnl-conth.doc-cont { position: static; width: auto; height: auto;
  overflow: visible; margin: 0 0 6px; padding: 0; display: flex;
  justify-content: space-between; align-items: flex-start; }
.rpt-page h2.pnl-conth .pnl-conth-mark { display: none; }
/* color EXPLICIT — the band dress paints white type, and an inherited color
   is how a corner mark prints white on white (the Controller lesson). */
.rpt-page h2.pnl-conth.doc-cont .pnl-conth-mark { display: block; font-size: 10px;
  font-weight: 800; color: #111; text-transform: uppercase; letter-spacing: .09em;
  padding: 6px 12px 0 0; }
.rpt-page h2.pnl-conth.doc-cont .doc-cont-mark { display: none; }
.rpt-page h2.pnl-conth.doc-cont .vc-num-id { margin: 6px 0 0 10px; width: 150px;
  box-sizing: border-box; text-align: center; font-size: 20px; line-height: 1.2;
  font-weight: 800; font-style: normal; letter-spacing: 0; padding: 6px 10px;
  border-radius: 2px 12px 2px 12px; display: flex; align-items: center; justify-content: center; }
.rpt-page .pnl-band { display: flex; gap: 14px; align-items: stretch;
  margin: 10px 10px 6px 10px; }
/* The large black label, upper left (owner 2026-08-15) — the panel's code,
   one short token, so it earns display size. */
.rpt-page .pnl-idbox { background: #000; color: #fff; border-radius: 2px 12px 2px 12px;
  width: 150px; flex: none; box-sizing: border-box; padding: 6px 10px;
  display: flex; align-items: center; justify-content: center; text-align: center;
  font-size: 20px; line-height: 1.2; font-weight: 800; }
.rpt-page .pnl-bt { display: flex; flex-direction: column; justify-content: flex-start;
  gap: 4px; min-width: 0; flex: 1; }
/* FOUR fixed columns, every line (owner's three-line layout, 2026-08-15) —
   auto-fill fit only three beside the black box, so the four-fact lines
   silently wrapped to six and doubled the band. Fixed columns keep the
   lines to their line and align the pills down the band. */
.rpt-page .pnl-brow { display: grid;
  grid-template-columns: repeat(4, 1fr); gap: 2px 16px; }
.rpt-page .pnl-brow .pill-label { margin: 1px 0 0; display: inline-block;
  white-space: nowrap; }
.rpt-page .pnl-val { font-size: 11px; line-height: 1.25; }
.rpt-page .pnl-tbl tr.pnl-total td { border-top: 1.5px solid #111; border-bottom: 0;
  font-weight: 700; }
.rpt-page .pnl-tbl td:first-child { font-weight: 700; }
/* Light interior verticals (owner 2026-08-15), never on the outer edges,
   and the same tightened padding as the Load Schedule so the lines and
   the headings' columns agree. */
.rpt-page .pnl-tbl th + th, .rpt-page .pnl-tbl td + td {
  border-left: 1px solid #e4e4e4; padding-left: 4px; }
.rpt-page .pnl-tbl th, .rpt-page .pnl-tbl td { padding-right: 3px; }
/* 42 circuits on ONE Letter portrait page (owner 2026-08-15): the row
   earns panel-schedule type — 10px on a 14.5px rhythm — and the band
   compresses in step. Doubled class (.rpt-mini.pnl-tbl) on purpose: the
   family's .rpt-mini td rule sits later in this file and would win the
   order tie at equal specificity, leaving rows at 11px/18.6px (measured). */
.rpt-page .rpt-mini.pnl-tbl td { font-size: 10px; line-height: 1.1;
  padding: 0.5px 3px 0.5px 0; }
.rpt-page .rpt-mini.pnl-tbl td + td { padding-left: 4px; }
.rpt-page .pnl-band { margin-bottom: 4px; }
.rpt-page .pnl-bt { gap: 2px; }
.rpt-page .pnl-brow { gap: 1px 16px; }
.rpt-page .pnl-val { font-size: 10.5px; line-height: 1.2; }
.pnl-warn { color: #b00; font-weight: 600; }

/* ---- Connectivity map (conn-*): the workshop's computed layout ----------
   Columns of house cards, one black band per VLAN between them, links as
   allocated lanes with labels in the gutters, copper in the corridor
   below. The stage draws at natural size and scales to fit ONE page. */
.rpt-page .conn-fit { overflow: hidden; margin: 0 auto; }
.rpt-page .conn-stage { position: relative; transform-origin: 0 0; }
.rpt-page .conn-card { position: absolute; border: 2px solid #111;
  border-radius: 3px 12px 3px 12px; background: #fff; overflow: hidden; }
.rpt-page .conn-card .hd { background: #000; color: #fff; font-weight: 800; font-size: 11px;
  padding: 2px 9px; border-radius: 0 9px 0 0; display: flex; justify-content: space-between; gap: 8px; }
.rpt-page .conn-card .hd small { font-weight: 600; opacity: .75; }
.rpt-page .conn-card .bd { padding: 3px 9px 4px; font-size: 9.5px; line-height: 1.45; }
.rpt-page .conn-recv { border-width: 1.5px; border-color: #444; }
.rpt-page .conn-recv .hd { background: #f1f1f1; color: #111; border-bottom: 1px solid #ddd; }
/* Two-line receiver header: name over location left, IP over univ · start
   addr right (owner 2026-08-16). */
.rpt-page .conn-hd2 { align-items: flex-start; }
.rpt-page .conn-hd2 .hd-l, .rpt-page .conn-hd2 .hd-r { display: flex; flex-direction: column; min-width: 0; }
.rpt-page .conn-hd2 .hd-r { text-align: right; flex: none; }
.rpt-page .conn-hd2 span { font-weight: 600; font-size: 9px; color: #555; }
/* On the black sender header the location line reads in the header's own
   dress: lighter white, pill included. */
.rpt-page .conn-src .conn-hd2 span { color: #ccc; }
.rpt-page .conn-src .conn-hd2 .pill { background: #555; }
.rpt-page .conn-zdrive { border-style: dashed; }
.rpt-page .conn-zdrive .hd { background: #fff; color: #111; border-bottom: 1px dashed #ccc; }
.rpt-page .conn-vchip { display: inline-flex; align-items: center; gap: 4px; border: 1px solid #ccc;
  border-radius: 9px; padding: 0 6px 1px 4px; font-size: 8.5px; font-weight: 700;
  white-space: nowrap; background: #fff; }
.rpt-page .conn-vchip i { width: 7px; height: 7px; border-radius: 2px; display: inline-block; }
/* A plain rounded GRAY rectangle on purpose (owner 2026-08-16): the band
   is infrastructure, not an LTC identity block — no swept corners, and not
   black. It stacks ABOVE the line layer, so a protocol line that does not
   interface with this VLAN visibly passes BEHIND it. */
.rpt-page .conn-spine { position: absolute; background: #6e6e6e; border-radius: 10px; z-index: 2; }
.rpt-page .conn-card { z-index: 3; }
.rpt-page .conn-stage svg { z-index: 1; }
.rpt-page .conn-spine .lbl { position: absolute; left: 50%; top: 50%;
  transform: translate(-50%,-50%) rotate(-90deg); color: #fff; font-weight: 800;
  font-size: 9.5px; letter-spacing: .12em; white-space: nowrap; }
/* Attachment dots sit ON TOP of everything (owner 2026-08-16) — they are
   the "a link meets its VLAN here" mark and must never be occluded. */
.rpt-page .conn-att { position: absolute; width: 8px; height: 8px; border-radius: 50%;
  border: 1.5px solid #fff; z-index: 5; }
.rpt-page .conn-lk { z-index: 4; }
/* The key: a quiet bordered panel, two readable rows — protocols, then
   grammar (owner 2026-08-16: the one-line version was unreadable). */
.rpt-page .conn-legend { position: absolute; display: flex; flex-direction: column; gap: 4px;
  font-size: 10px; color: #222; z-index: 4; border: 1.5px solid #ccc; border-radius: 10px;
  padding: 5px 12px 7px; background: #fff; }
.rpt-page .conn-legend .conn-key-h { font-size: 8.5px; font-weight: 800; letter-spacing: .1em;
  text-transform: uppercase; color: #555; }
.rpt-page .conn-legend .conn-key-row { display: flex; gap: 18px; align-items: center; flex-wrap: wrap; }
.rpt-page .conn-legend svg, .rpt-page .conn-legend i { flex: none; }
.rpt-page .conn-legend .kdot { width: 8px; height: 8px; border-radius: 50%; background: #888;
  border: 1.5px solid #fff; box-shadow: 0 0 0 1px #999; display: inline-block; }
.rpt-page .conn-attn { position: absolute; color: #fff; font-size: 8px; font-weight: 800;
  border-radius: 8px; padding: 0 5px; border: 1.5px solid #fff; }
.rpt-page .conn-stage svg { position: absolute; inset: 0; pointer-events: none; }
.rpt-page .conn-lk { position: absolute; font-size: 9px; font-weight: 700; background: #fff;
  border: 1px solid #ddd; border-radius: 8px; padding: 0 6px 1px; white-space: nowrap; }
.rpt-page .conn-dim { color: #777; }
.rpt-page .conn-note { color: #888; font-size: 9px; }
.rpt-page .vc-card > .pnl-warnline { color: #b00; font-weight: 600; font-size: 11px;
  margin: 2px 16px 1px; }
/* ---- Rack Elevations ------------------------------------------------------
   A drawn rack: RU 1 at the BOTTOM, so the bay is a positioned box and each
   item is offset from `bottom` by its slot. Heights come from app.js in px per
   RU, set inline on the ruler, the bay and the spec column together.

   ALIGNMENT: the three columns must share one interior height or every item
   drifts from its gridline. The bay has a 2px rail on each side, so its border
   box is 4px taller than its content; the ruler and spec column carry matching
   TRANSPARENT borders so all three interiors start and end on the same pixel.
   Without this the blocks sat ~4px low and the bottom item ran off the rack. */
/* ONE source for the rack's width. The bay is content-box plus a 2px rail each
   side, so the label bar above it must be 4px WIDER to line up with the rack's
   outer edges — sized to the bay's content box it sat inboard of both rails.
   The gutter is one source too: the ruler, its header, and the caster row must
   agree or the drawing shears at the rack's corners. Datum rules overhang the
   gutter to the left by design (they are 162px, anchored right); the gutter
   can only shrink until their labels hit the paper's edge, so the bay's width
   comes mostly out of the spec column's slack (owner 2026-08-05). */
/* ---- Bill of materials cards (owner 2026-08-15) ---------------------------
   Each device type in a swept-corner card, the Device Specification's grammar
   at a smaller size. The border is also the FIX for this report's silent
   overpacking: with no border, the heading's top margin collapsed through
   the panel and stood ~18px between cards on the page, where outerH — which
   reads only a block's own margins — never counted it, so the paginator
   packed one card too many and the page clipped. A border blocks margin
   collapse, so what is measured is what renders. */
.bomr-card { border: 2px solid #111; border-radius: 3px 18px 3px 18px;
  padding: 5px 5px 6px; margin: 0 0 12px; background: #fff; }
/* The card's real heading: clipped on normal sheets (the band says it
   better), visible only when the paginator reprints it "(continued)" on a
   card split across pages — the VLAN card's pattern. */
.rpt-page .bomr-conth { position: absolute; width: 1px; height: 1px; margin: 0;
  overflow: hidden; }
/* A continuation REPEATS THE DEVICE TYPE BLOCK (owner 2026-08-15: "it's safe
   to repeat the device type block... but NOT the image or qty"), and puts
   the word CONTINUED in the border's upper right corner. The reprinted h2
   keeps its two spans: the code dresses as the black swept box on the left,
   the mark sits at the corner; the engine's own " (continued)" suffix is
   hidden here because the corner already says it. */
.rpt-page .bomr-conth.doc-cont { position: static; width: auto; height: auto;
  overflow: visible; display: flex; justify-content: space-between;
  align-items: flex-start; margin: 0 0 5px; }
.rpt-page .bomr-conth.doc-cont .bomr-conth-code { background: #000; color: #fff;
  border-radius: 2px 10px 2px 10px; padding: 5px 14px; font-size: 18px; font-weight: 800; }
.rpt-page .bomr-conth.doc-cont .bomr-conth-mark { font-size: 9px; font-weight: 800;
  text-transform: uppercase; letter-spacing: .09em; padding-top: 2px; }
.rpt-page .bomr-conth.doc-cont .doc-cont-mark { display: none; }
/* The identity row inside every category table: invisible wherever the
   category heading itself is on the page, and surfaced by splitTable as
   ".doc-group-cont" when ROWS spill mid-table — the deepest break, where
   neither the card's h2 nor the category h3 travels with them. */
.rpt-page .bomr-grp { display: none; }
.rpt-page .bomr-grp.doc-group-cont { display: table-row; }
.rpt-page .bomr-grp.doc-group-cont td { font-weight: 800; font-size: 9.5px;
  text-transform: uppercase; letter-spacing: .07em; border: 0; padding: 2px 0 1px; }
/* min-height = the three-pill band as measured (owner 2026-08-15: "keep the
   height of the Device Type and Image and Qty blocks the same" across cards)
   — a card missing its Classification pill (unset kind) must not shrink its
   boxes below its neighbors'. A long description may still grow a band; the
   floor is what is uniform. */
.bomr-band { display: flex; gap: 10px; align-items: stretch; margin: 0 0 4px; min-height: 90px; }
/* Just the code, large (owner 2026-08-15) — no caption, no category line. */
.bomr-typebox { background: #000; color: #fff; border-radius: 2px 10px 2px 10px;
  padding: 4px 14px; min-width: 92px; text-align: center; display: flex;
  align-items: center; justify-content: center; }
.bomr-typebox .code { font-size: 32px; font-weight: 800; line-height: 1.05; }
/* Top-aligned, not centered (owner 2026-08-15): when Classification is
   absent the remaining pills move UP into the top slot rather than floating
   in the box's middle. */
.bomr-band-t { flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: flex-start; }
.bomr-band-t .pill-label { margin: 2px 0 0; align-self: flex-start; }
.bomr-val { font-size: 11px; line-height: 1.25; }
/* The photograph, left of the quantity box at the same height (owner
   2026-08-15): both stretch to the band, whose height the pills column sets.
   A FIXED width, not aspect-ratio — aspect-ratio on a stretched flex item
   feeds width back into height and the band inflated to 245px. */
.bomr-photo { position: relative; border: 2px solid #111; border-radius: 2px 10px 2px 10px;
  overflow: hidden; background: #fff; width: 150px; flex: none; align-self: stretch; }
/* Absolute, so a tall photograph fills the frame without SIZING it: as a flow
   child its resolved height (150px wide at a portrait aspect) became the
   band's height, and every box in the band stretched to 240px with it. */
.bomr-photo img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
.bomr-photo img { width: 100%; height: 100%; object-fit: contain; display: block; }
/* Caption at the top, figure centered in the space BELOW it (owner
   2026-08-15: they overlapped when the figure centered on the whole box) —
   and the bottom of the box is deliberately spare, where a coming modifier
   line ("Linear Feet") will sit. */
.bomr-qtybox { border: 2px solid #111; border-radius: 2px 10px 2px 10px;
  min-width: 96px; padding: 4px 10px; text-align: center; display: flex; flex-direction: column; }
.bomr-qtybox .spec-qtylabel { font-size: 7px; }
.bomr-qtybox .qty { flex: 1; display: flex; align-items: center;
  justify-content: center; font-size: 16px; font-weight: 800; font-variant-numeric: tabular-nums; }
/* Each category is a heading over its own slightly-inset table (owner
   2026-08-15), ruled horizontally only and tighter than the house table —
   the card supplies the outer boundary, so the cells do not need cages.
   Heading and table are DIRECT card children (no wrapper), so the card's own
   splitter owns every break and rows never print outside a border. */
.bomr-cath { margin: 2px 0 1px; font-size: 9.5px; font-weight: 800;
  text-transform: uppercase; letter-spacing: .07em; }
.bomr-card .bomr-tbl { margin: 0 0 6px 12px; width: calc(100% - 12px); }
/* Classification and Device Category share the top line of the band. */
.bomr-band-row { display: flex; gap: 24px; }
/* Column widths (owner 2026-08-15): part numbers get room, the qty column
   gives it up. Widths on the th govern the column. */
.rpt-page .bomr-tbl th.bomr-th-prod { width: 24%; }
.rpt-page .bomr-tbl th.bomr-th-qty { width: 72px; }
/* The priced card's own bottom line: subtotal right-aligned over a rule,
   the figure bold, the placed extension trailing in gray. */
.bomr-subtotal { display: flex; justify-content: flex-end; align-items: baseline; gap: 10px;
  margin: 0 0 2px 12px; padding-top: 3px; border-top: 1px solid #999; font-size: 10px; }
.bomr-subtotal-l { text-transform: uppercase; font-size: 8.5px; letter-spacing: .07em;
  color: #666; font-weight: 700; }
.bomr-subtotal strong { font-size: 12px; font-variant-numeric: tabular-nums; }
.bomr-subtotal-x { color: #555; font-variant-numeric: tabular-nums; }
/* The disclaimer closes every BOM document: the full LTC logo beside the
   words, the whole of it in a swept-corner border like the cards above it.
   Requested text (owner 2026-08-15), so it is the exception to paper never
   growing words nobody asked for. Atomic: a disclaimer split across pages
   would read as two half-promises. */
/* A LARGER-than-usual gap above (owner 2026-08-15): the disclaimer is not
   another device card, and the extra air says so before the border does. The
   margin is the block's own, so outerH counts every pixel of it. */
.bomr-disclaimer { display: flex; gap: 14px; align-items: center; margin: 34px 0 0;
  border: 2px solid #111; border-radius: 2px 14px 2px 14px; padding: 8px 12px; }
.bomr-disc-logo { width: 150px; flex: none; }
.bomr-disclaimer p { margin: 0; font-size: 8.5px; color: #444; line-height: 1.45; }
.rpt-page .bomr-tbl th, .rpt-page .bomr-tbl td { border-left: 0; border-right: 0;
  padding: 1.5px 7px; font-size: 10px; }
/* Gray, not black (owner 2026-08-15): the column headers are wayfinding, the
   rows are the content. */
.rpt-page .bomr-tbl th { background: transparent; border-top: 0; font-size: 8.5px; color: #666; }

/* ---- IP Address Assignments (owner 2026-08-15) ----------------------------
   Each VLAN or Zone group in a swept card; clean tables (no gray, no
   vertical lines); IP left-aligned; the MAC column wide enough for a full
   address. Host and Switch/Port gave their room to Device ID, the device's
   description, and Device Type. */
.rpt-page .ipr-card { border: 2px solid #111; border-radius: 3px 18px 3px 18px;
  padding: 5px 5px 6px; margin: 0 0 12px; background: #fff; }
.rpt-page .ipr-card h2 { margin: 2px 4px 4px; font-size: 13px; }
.rpt-page .ipr-tbl { margin-bottom: 2px; }
.rpt-page .ipr-tbl th { background: transparent; border-top: 0; border-left: 0; border-right: 0;
  color: #666; font-size: 8.5px; }
.rpt-page .ipr-tbl td { border-left: 0; border-right: 0; padding: 2.5px 7px; font-size: 10px; }
.rpt-page .ipr-tbl .ipr-ip { text-align: left; font-variant-numeric: tabular-nums; white-space: nowrap; }
.rpt-page .ipr-tbl th.ipr-mac { width: 112px; }
.rpt-page .ipr-tbl td.ipr-mac { white-space: nowrap; font-variant-numeric: tabular-nums; }

/* THE SWEPT FRAME (owner 2026-08-15, carrying the Device Specification's
   style over): the sheet's own border draws the boundary, which is what let
   the header and footer hairlines come off (rk-doc above). Scoped to the
   paginated page so the Racks tab's on-screen panels keep their look. */
.doc-page .rk-sheet { border: 2px solid #111; border-radius: 3px 22px 3px 22px;
  padding: 6px; background: #fff; }
/* THE FRAME FILLS THE SHEET (owner 2026-08-15: "as large as it can be...
   approaches the header and the footer... isn't shrunken down if a rack is
   short"), the Device Specification's own mechanism: each elevation is alone
   on its page (data-doc-break-before), so the box takes the whole body, the
   band and drawing stack from the top, and the spec block and breakdown are
   pushed to the floor — the slack a short rack leaves collects BETWEEN the
   drawing and the specs, not below the border. In the paginator's measuring
   stage there is no definite parent height, so 100% resolves to the natural
   height it has always measured. */
.doc-page.rk-doc .doc-body > .rk-sheet { height: 100%; box-sizing: border-box;
  display: flex; flex-direction: column;
  /* ROOM UNDER THE LAST ROW. The breakdown is parked on the floor of the
     frame, 6px above the border. Safari's print pass draws the frame a few
     pixels shorter than the column it laid out inside it, and the row that
     pays is the last one: "Rack total" was cut in half on paper (owner
     2026-09-19), whole on screen and in Chrome. 16px of floor absorbs that
     and costs nothing anybody can see. */
  padding-bottom: 16px; }
/* The drawing floats CENTERED in whatever a short rack leaves (owner
   2026-08-15): auto margins both sides of the face split the slack equally,
   which also parks the spec block and breakdown on the floor. Only the face
   carries auto margins — an auto margin anywhere else would claim an equal
   share of the slack and pull the centering off. */
.doc-page.rk-doc .doc-body > .rk-sheet > .rk-face { margin-top: auto; margin-bottom: auto; }
/* 4px to MATCH the band's 4px bottom margin above the face — the auto
   margins split what is left after fixed margins, so the fixed margins on
   each side must agree or the centering skews by their difference (it sat
   9px low under .rk-spec's default 13px). */
.doc-page.rk-doc .doc-body > .rk-sheet > .rk-spec { margin-top: 4px; }
/* The identity band: the rack's code in a black swept box, the way the spec
   sheet boxes its device type; name and location read beside it. */
.rk-band { display: flex; gap: 12px; align-items: stretch; margin-bottom: 4px; }
.rk-idbox { background: #000; color: #fff; border-radius: 2px 12px 2px 12px;
  padding: 4px 14px; min-width: 84px; text-align: center; display: flex; flex-direction: column; }
.rk-idbox span { font-size: 7.5px; text-transform: uppercase; letter-spacing: .12em; color: #bbb; }
.rk-idbox .code { flex: 1; display: flex; align-items: center; justify-content: center;
  font-size: 21px; font-weight: 800; line-height: 1.05; }
.rk-band-t { display: flex; flex-direction: column; justify-content: center; min-width: 0; }
/* Which page this is — FRONT, REAR, N/A or LOOSE — opposite the identity box
   (owner 2026-08-15), in the bordered swept shape of the Device
   Specification's quantity box. The band stretches its children, so the two
   boxes bracket the band at the same height. */
.rk-facebox { margin-left: auto; border: 2px solid #111; border-radius: 2px 12px 2px 12px;
  min-width: 84px; padding: 4px 12px; box-sizing: border-box; display: flex;
  align-items: center; justify-content: center;
  font-size: 14px; font-weight: 800; letter-spacing: .08em; }
/* The LOOSE page's schedule sits in the drawing's place: full width, and the
   rk-face class it shares brings the centering auto margins with it. */
.rk-loose { display: flex; }
.rk-loosetbl { width: 100%; }
.rk-face { --rk-bay-w: 296px; --rk-gut-w: 112px; --rk-rail: 2px; margin-bottom: 3px; }
.rk-h2 { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; }
.rk-body { display: flex; align-items: flex-start; }
.rk-body-h { display: flex; align-items: flex-end; margin-bottom: 2px; }
/* The gutters take an equal share of whatever is left, so the BAY lands on the
   centre line of the page whatever the paper (owner 2026-08-05). Their content
   hugs the rack — ruler right-aligned, specs left-aligned — so the drawing
   still reads as one object rather than three scattered columns. */
/* The header gutters must resolve to the SAME width as the body gutters below,
   or the label bar sits off the rack. `flex-basis: 0` measures the content box,
   so padding on a border-box item is inside the basis and padding on a
   content-box item is outside it — the two rows then disagree by exactly that
   padding. Neither header cell carries horizontal padding for that reason;
   their content is inset with margin and text-indent instead. */
.rk-h-ruler { flex: none; width: var(--rk-gut-w); display: flex; justify-content: flex-end;
  gap: 4px; box-sizing: content-box; font-size: 8px; color: #555; }
.rk-h-in { width: 26px; text-align: right; }
.rk-h-u { width: 14px; text-align: right; margin-right: 4px; }
.rk-h-bay { flex: none; width: calc(var(--rk-bay-w) + var(--rk-rail) * 2); }
.rk-h-specs { flex: 1 1 0; min-width: 0; box-sizing: content-box; display: flex; align-items: flex-end;
  font-size: 7px; letter-spacing: .05em; color: #555; text-transform: uppercase;
  border-bottom: 1px solid #bbb; }
.rk-cap { position: relative; display: block; border: 1px solid #111; background: #ddd;
  text-align: center; font-weight: 700; font-size: 10px; padding: 2px 0; letter-spacing: .04em; }
.rk-fan { position: absolute; left: 5px; top: 50%; transform: translateY(-50%);
  width: 12px; height: 12px; color: #111; }

.rk-ruler { position: relative; flex: none; width: var(--rk-gut-w); box-sizing: content-box;
  border-top: 2px solid transparent; border-bottom: 2px solid transparent; }
.rk-r { position: relative; display: flex; align-items: center; justify-content: flex-end;
  padding-right: 4px; box-sizing: border-box; }
/* The inch figures sit ON the line between slots, so they are pulled half a
   line out of their row; the RU number is centred in the slot it names. */
/* The white ground is not decoration: the datum lines run the full width of
   the gutter and would otherwise strike through these figures. */
.rk-in { position: absolute; right: 20px; font-size: 7px; color: #666;
  font-variant-numeric: tabular-nums; white-space: nowrap;
  background: #fff; padding: 0 2px; z-index: 1; }
.rk-in-t { top: 0; transform: translateY(-50%); }
.rk-in-b { bottom: 0; transform: translateY(50%); }
.rk-u { font-size: 8px; color: #333; font-variant-numeric: tabular-nums; min-width: 14px;
  text-align: right; background: #fff; position: relative; z-index: 1; }

/* Reference heights, drawn across the gutter to the rack: the label sits above
   its own line at the far end, the way a drafted note reads back to what it
   marks. Behind the ruler figures, which carry a white ground to stay legible
   where a line passes through them. */
.rk-datum { position: absolute; right: 0; border-bottom: 1px solid #777; }
/* Label ABOVE the line, measurement BELOW it — centred on the line they
   collided with each other. Stacked, the gutter only has to be as wide as the
   longest label. */
.rk-datum-l { position: absolute; left: 0; right: 0; bottom: 1px; font-size: 7.5px;
  color: #333; white-space: nowrap; text-align: center; }
.rk-datum-h { position: absolute; left: 50%; top: 2px; transform: translateX(-50%);
  background: #fff; padding: 0 2px; font-size: 7px; color: #555;
  font-variant-numeric: tabular-nums; white-space: nowrap; z-index: 1; }

/* The bay. Side rails are its own borders; the gridlines are real elements
   because a browser may drop background images when it prints. It CLIPS: an
   item recorded outside the rack used to be drawn over the rack's own label,
   which broke the sheet instead of reporting the mistake. */
/* A drawn elevation is at a SCALE, so the bay is a fixed width rather than
   whatever the paper leaves over — halved (owner 2026-08-05), which hands the
   width back to the spec column beside it. */
.rk-bay { position: relative; flex: none; width: var(--rk-bay-w);
  border: var(--rk-rail) solid #111; background: #fff; overflow: hidden; box-sizing: content-box; }
.rk-gl { border-top: 1px solid #ccc; box-sizing: border-box; }
.rk-gl:first-child { border-top: 0; }
.rk-it { position: absolute; left: 2px; right: 2px; box-sizing: border-box;
  border: 1px solid #333; background: #f1f1f1; display: flex; align-items: center;
  justify-content: center; overflow: hidden; line-height: 1.1; }
/* One rack unit is ~15px, so a 1RU block has room for exactly one line — it
   ellipsises. Anything 2RU or taller can afford to wrap instead of truncate. */
/* line-height must clear the DESCENDERS: text-overflow needs overflow:hidden,
   and that clips at the line box — at 1.1 the tail of a "g" was cut off. */
/* The side padding must clear the LONGER of the two rails — a 13-character
   Device ID like 611-North-079 runs ~63px in the 7.5px mono, and at 56px it
   collided with a long centred label. Symmetric, so the label stays centred. */
.rk-it-l { font-size: 9px; line-height: 1.3; font-weight: 600; text-align: center;
  padding: 0 64px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.rk-it-l.rk-it-wrap { white-space: normal; overflow-wrap: anywhere; }
/* Device Type to the right rail, Device ID to the left — the label stays bold
   and centred on the block regardless of how long either code is. */
.rk-it-d { position: absolute; right: 4px; font-size: 7.5px; color: #555; font-family: var(--mono); }
.rk-it-id { position: absolute; left: 4px; font-size: 7.5px; color: #555; font-family: var(--mono); }

/* Per-item specs, on the item's own line beside the rack. */
.rk-specs { position: relative; flex: 1 1 0; min-width: 0; box-sizing: content-box; overflow: hidden;
  border-top: 2px solid transparent; border-bottom: 2px solid transparent; }
/* A schedule beside the rack: one cell per figure, ruled top and bottom in
   line with the item's own block and divided down the columns, so the numbers
   stack instead of running together. Units live in the headings — that is what
   buys the width the notes need. */
.rk-sp { position: absolute; left: 0; right: 0; box-sizing: border-box;
  display: flex; align-items: stretch; font-size: 7.5px; color: #333;
  font-variant-numeric: tabular-nums; overflow: hidden; white-space: nowrap;
  border-top: 1px solid #e2e2e2; border-bottom: 1px solid #e2e2e2; }
.rk-sp-c { display: flex; align-items: center; padding: 0 4px; box-sizing: border-box;
  overflow: hidden; text-overflow: ellipsis; }
.rk-sp-grid { position: absolute; inset: 0; display: flex; }
.rk-sp-grid .rk-sp-c { border-right: 1px solid #ececec; }
.rk-sp-grid .rk-sp-c:last-child { border-right: 0; }
/* The product identity takes every pixel the fixed figures leave. */
.rk-sp-p { flex: 1; min-width: 0; font-weight: 600; }
.rk-sp-v { flex: none; width: 46px; justify-content: flex-end; }
.rk-sp-v:nth-of-type(4) { width: 56px; }
/* Anything 2RU or taller can wrap a long identity instead of truncating. */
.rk-sp-tall .rk-sp-p { white-space: normal; line-height: 1.15; }
.rk-h-specs .rk-sp-c { border-right: 1px solid #ececec; align-items: flex-end; padding-bottom: 1px; }
.rk-h-specs .rk-sp-c:last-child { border-right: 0; }

/* A slot claimed twice, or an item hanging off the end of the rack. Drawn AND
   named under the elevation — an overlap that only overlaps is a drawing that
   lies about what fits. */
.rk-it.rk-clash, .rk-it.rk-over { border-color: #b00; border-width: 1.5px; background: #fdeaea; }
/* Casters, drawn under the rack when it is mounted Rolling. */
.rk-under { display: flex; }
.rk-under-l { flex: none; width: var(--rk-gut-w); }
.rk-under-bay { flex: none; width: calc(var(--rk-bay-w) + var(--rk-rail) * 2);
  display: flex; justify-content: space-between; padding: 0 16px; box-sizing: border-box; }
.rk-caster { display: block; width: 9px; height: 9px; border: 1.5px solid #111;
  border-radius: 50%; background: #fff; margin-top: -1px; }
.rk-note { font-size: 10px; color: #b00; margin: 5px 0 0; }
/* What the rack IS — product identity, and how it is built and hung. Sits
   between the drawing and the load table, because it describes the box the
   drawing shows rather than what has been put in it. */
/* Key over hairline, the Device Specification's treatment, so the two reports
   read as one family. Tight on purpose: this block plus the load table is the
   height budget that decides whether a 48RU rack fits one sheet. */
.rk-spec { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0 16px; margin: 13px 0 0; }
.rk-kv { display: flex; justify-content: space-between; align-items: baseline; gap: 8px;
  border-bottom: 1px solid #ddd; padding: 0; font-size: 8.5px; line-height: 1.25; }
.rk-kv span { color: #666; text-transform: uppercase; letter-spacing: .05em; font-size: 7px;
  white-space: nowrap; }
.rk-kv strong { text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Free text: spans the row and may wrap. */
.rk-kv-wide { grid-column: 1 / -1; }
.rk-kv-wide strong { white-space: normal; overflow: visible; text-align: left; flex: 1; }
.rk-sheet .rk-tot { margin-top: 5px; margin-bottom: 0; }
/* Horizontal rules only (owner 2026-08-15): the row line and the column gaps
   separate plenty; a full grid of cell borders read as a cage. */
.rk-sheet .rk-tot th, .rk-sheet .rk-tot td { font-size: 8px; padding: 0 6px; line-height: 1.1;
  border-left: 0; border-right: 0; }
.rk-sheet .rk-h2 { margin: 0; font-size: 12.5px; }
/* The face, in the rack's own label bar — left corner, after the fan mark
   when there is one. */
.rk-cap-face { position: absolute; left: 6px; top: 50%; transform: translateY(-50%);
  font-size: 8px; font-weight: 700; letter-spacing: .1em; color: #333; }
.rk-fan + .rk-cap-face { left: 21px; }
/* The rack's location: its own line under the code and name, tight so the
   48RU page budget still holds. */
.rk-h2-loc { font-size: 10px; line-height: 1.2; color: #444; margin: 0 0 3px; }
.rk-sheet .rk-note { font-size: 8.5px; margin: 2px 0 0; line-height: 1.3; }

/* Bill of Materials: the BOM Category band inside a device's table. It is a
   ROW, not a heading, so the paginator can reprint it "(continued)" when a
   long category runs over a page — .doc-group-cont is what it adds. */
.rpt-page tr.rpt-grp td { background: #e4e4e4; font-weight: 700; font-size: 10px;
  text-transform: uppercase; letter-spacing: .05em; }
.rpt-page tr.rpt-grp.doc-group-cont td { font-weight: 600; font-style: italic; background: #efefef; }
.rpt-page .rpt-sub { margin: -4px 0 12px; font-size: 11px; color: #444; }
/* "Not priced" is a STATE, not a number. Set in the sheet's own ink (reports
   are theme-independent paper) and never right-aligned like an amount, so a
   column of costs cannot be skimmed as if this row held one.
   docs/WRITING-STANDARD.md §5. */
.rpt-page .rpt-notpriced { font-style: italic; color: #7a4800; font-size: 10px;
  text-transform: uppercase; letter-spacing: .04em; white-space: nowrap; }
.rpt-page .rpt-warn { border: 1.5px solid #b06a00; color: #7a4800; background: #fff5e6;
  padding: 6px 10px; margin: 0 0 12px; font-size: 11.5px; border-radius: 4px; }

/* Network reports: VLAN swatch, key/value config table, severity marks. */
.rpt-page .rpt-swatch { display: inline-block; width: 12px; height: 12px; border-radius: 3px;
  border: 1px solid #999; margin-right: 7px; vertical-align: -1px; }
.rpt-page h1.rpt-sec { font-size: 15px; margin: 18px 0 8px; border-bottom: 2px solid #111; padding-bottom: 4px; }
.rpt-page h1.rpt-break { break-before: page; }
.rpt-page h3 { font-size: 12px; margin: 10px 0 4px; color: #111; }
.rpt-page td.ip-warn, .rpt-page .ip-warn { color: #b06a00; font-weight: 700; }
.rpt-page td.ip-dup { color: #b00; font-weight: 700; }
/* VLAN Schedule: one card per VLAN. How many share a sheet is the report's
   own option; the break is `data-doc-break-before` on the card that starts a
   sheet — the report window paginates through LTCDoc, which places top-level
   blocks and IGNORES page-break-* CSS. (A .vc-brk page-break-after class and
   a .vc-sheets wrapper used to do this; the wrapper made the whole report one
   unsplittable block and every sheet boundary cut a card in half.)
   Echoes the Device Specification sheet (rounded frame, capsule labels, black
   identity box); the identity box wears the VLAN's field color as its edge. */
/* Every table inside a vc card is horizontal rules only, no gray (owner
   2026-08-15, Controller Schedule ruling; the VLAN ranges table already
   followed it) — one law for the family. And the LAST row of a table or a
   kv column sheds its underline (the positional rule every framed report
   follows): the next group's own top rule marks the boundary, and two lines
   a few pixels apart read as a mistake. */
.rpt-page .vc-card th { background: transparent; border-top: 0; border-left: 0; border-right: 0; color: #666; }
.rpt-page .vc-card td { border-left: 0; border-right: 0; }
.rpt-page .vc-card tbody tr:last-child td { border-bottom: 0; }
.rpt-page .vc-card .vc-col .vc-kv:last-child { border-bottom: 0; }

/* THE SWEPT CUT (owner 2026-08-15: "apply our shapes to the borders and
   blocks") — the same corner law as the spec sheet, rack sheets and BOM
   cards: rounded upper-right and lower-left, squared the other two, scaled
   to each box. */
.vc-card { border: 2px solid #111; border-radius: 3px 18px 3px 18px; padding: 0 0 12px;
  margin-bottom: 18px; break-inside: avoid; overflow: hidden; }
/* two-up: trim the furniture so a pair actually fits the sheet. The marker
   class rides each card (there is no wrapper for it to ride). */
.vc-card.vc-2up { margin-bottom: 12px; }
.vc-2up .vc-band { padding: 7px; gap: 12px; }
.vc-2up .vc-num, .vc-2up .vc-chip { width: 68px; height: 48px; }
.vc-2up .vc-num .n { font-size: 21px; }
.vc-2up .vc-name { font-size: 15px; }
.vc-2up .vc-body { padding: 2px 14px 0; }
.vc-2up .vc-stat b { font-size: 17px; }
.vc-2up .vc-res { padding: 6px 14px 0; }
/* THE CONTINUATION HEADING. Present on every card so the paginator will split
   it, hidden on the sheet where the band below already says the same thing, and
   shown only when print-doc reprints it as "… (continued)" at the head of a
   spilled card. Clipped rather than `display:none`, because a heading that is
   not laid out is not a heading the paginator can measure. */
.rpt-page .vc-conth { position: absolute; width: 1px; height: 1px; margin: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.rpt-page .vc-conth.doc-cont { position: static; width: auto; height: auto;
  overflow: visible; clip-path: none; white-space: normal;
  font-size: 13px; font-weight: 800; text-transform: uppercase; letter-spacing: .06em;
  padding: 9px 16px; border-bottom: 2px solid #111; background: #f4f4f4; }
/* No rule under the band and no gray wash (owner 2026-08-15, two rulings):
   the frame and the black number box mark identity on their own. */
/* Side gaps MATCH the top gap (owner 2026-08-15) — the number block and the
   color block sit as far from the frame's sides as from its top. */
.vc-band { display: flex; align-items: center; gap: 16px; padding: 10px; }
/* identity box and color chip share one footprint so the band reads as a pair */
.vc-num, .vc-chip { width: 82px; height: 60px; box-sizing: border-box; flex: none; }
.vc-num { background: #111; color: #fff; border: 3px solid #111; border-radius: 2px 10px 2px 10px;
  padding: 3px 0 0; text-align: center; }
.vc-num span { font-size: 8px; text-transform: uppercase; letter-spacing: .14em; color: #bbb; }
.vc-num .n { font-size: 26px; font-weight: 800; line-height: 1.1; font-variant-numeric: tabular-nums; }
.vc-title { flex: 1; min-width: 0; }
.vc-name { font-size: 17px; font-weight: 800; }
.vc-abbr { font-size: 11px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: #555; }
/* label and name sit to the LEFT of the chip, which is flush right */
/* COLOR CODE wears a pill and sits at the TOP of the band (owner
   2026-08-15) while the chip stays centered beside it. */
.vc-color { display: flex; align-items: center; gap: 10px; margin-left: auto; align-self: stretch; }
.vc-color-t { display: flex; flex-direction: column; align-items: flex-end; gap: 2px;
  align-self: flex-start; padding-top: 2px; text-align: right; }
.vc-color-t .pill-label { margin: 0; }
.vc-color-n { font-size: 14px; font-weight: 700; }
.vc-chip { border-radius: 2px 10px 2px 10px; border: 1px solid #888; display: block; }
.vc-chip[data-none] { background: repeating-linear-gradient(45deg, #fff, #fff 4px, #e4e4e4 4px, #e4e4e4 8px); }

.vc-body { display: grid; grid-template-columns: 1fr; gap: 0; padding: 4px 16px 0; }
.vc-grp { border-top: 1px solid #ddd; padding: 2px 0 6px; break-inside: avoid; }
.vc-grp.vc-nolabel { border-top: 0; padding-top: 4px; }
/* NETWORK holds every address fact; two columns filled top-to-bottom */
.vc-net { display: grid; grid-auto-flow: column; grid-template-rows: repeat(4, auto);
  column-gap: 22px; }
.vc-kv { display: flex; justify-content: space-between; gap: 14px;
  border-bottom: 1px solid #eee; padding: 2.5px 0; font-size: 11.5px; }
.vc-kv > span { color: #555; text-transform: uppercase; font-size: 9px;
  letter-spacing: .05em; padding-top: 2px; white-space: nowrap; }
.vc-kv strong { text-align: right; }
.vc-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; padding: 4px 0 2px; }
.vc-stat { border: 1px solid #ccc; border-radius: 2px 8px 2px 8px; padding: 6px 6px 5px; text-align: center; }
.vc-stat b { display: block; font-size: 20px; font-weight: 800; font-variant-numeric: tabular-nums; }
.vc-stat span { display: block; font-size: 8px; text-transform: uppercase; letter-spacing: .07em;
  color: #555; line-height: 1.3; margin-top: 2px; }
.vc-flags { display: flex; gap: 10px; padding: 3px 0; }
.vc-flag { display: flex; align-items: center; gap: 6px; border: 1px solid #ccc;
  border-radius: var(--r-ctl); padding: 3px 12px; font-size: 10.5px; color: #777; }
.vc-flag b { font-size: 11px; }
.vc-flag em { font-style: normal; font-weight: 700; }
.vc-flag-on { border-color: #111; color: #111; }
.vc-res { padding: 8px 16px 0; break-inside: avoid; }
/* The ranges table is a DIRECT card child since 2026-08-15 (continuation
   shells); it keeps the inset the old nested div gave it. No gray anywhere
   and horizontal rules only (owner, same day) — the card is the box. */
.vc-card > .vc-restbl { margin: 4px 16px 0; width: calc(100% - 32px); }
.rpt-page .vc-restbl th { background: transparent; border-top: 0; border-left: 0; border-right: 0; color: #666; }
.rpt-page .vc-restbl td { border-left: 0; border-right: 0; }
.rpt-page .vc-resnone { margin: 3px 16px 0; font-size: 11px; }
.rpt-page .vc-foot { break-before: avoid; }

/* ---- Controller Schedule (owner 2026-08-13) -------------------------------
   One sheet per controller, in the VLAN Schedule's grammar. `.vc-net` is sized
   for that card's eight address rows; these groups have four, so they use a
   plain two-column pair grid instead of inheriting a shape built for something
   else. */
/* The band is the card's HEADING (an h2), which is what lets print-doc.js
   continue a long controller onto a second sheet. It carries h2 defaults it did
   not have as a div, so they are cleared here. */
.rpt-page h2.vc-band { margin: 0; font-size: inherit; font-weight: inherit; }
/* Groups are direct children of the card now, so the body padding that used to
   come from .vc-body lives on them. */
.rpt-page .vc-card > .vc-grp { margin: 0 16px; }
/* Controller sheets: no rule above NETWORK, CONTROL INPUT/OUTPUT or NOTES
   (owner 2026-08-15: "too many lines") — the pills mark the sections. Only
   the first full-width group after the paired columns keeps its rule. The
   selector reaches controller sheets alone: a VLAN card nests its groups in
   .vc-body, never as direct children. */
.rpt-page .vc-card > .vc-grp { border-top: 0; }
.rpt-page .vc-card > .vc-duo + .vc-grp { border-top: 1px solid #ddd; }
.rpt-page .vc-card > .vc-grp:first-of-type { border-top: 0; padding-top: 6px; }
/* The heading carries the DEVICE ID AND NOTHING ELSE, because the paginator
   flattens it to text when it reprints it: with the description and the product
   line inside, a continued page read as one run-on string. What belongs on the
   first page only sits in .vc-subband beneath it, which is an ordinary child. */
/* Larger (owner 2026-08-15) — the sheet's one identifier earns its size. */
.rpt-page h2.vc-band .vc-num-id { background: #111; color: #fff; border-radius: 2px 12px 2px 12px;
  padding: 7px 16px; font-size: 22px; font-weight: 800; letter-spacing: .01em; }
/* The controller band (owner 2026-08-15): the Device ID in a black swept
   box at the photograph's height, Device Type / Description / Location
   stacked beside it under pill headings, the h2 clipped until a
   continuation needs it. The right margin clears the absolute photo. */
/* padding: 0 matters — the band padding this h2 inherits would paint the
   clipped black ID span as a small black stub above the card (owner caught
   it); a 1px box with 10px padding still shows 21px of its children. */
.rpt-page h2.ctlr-conth { position: absolute; width: 1px; height: 1px; margin: 0;
  padding: 0; overflow: hidden; }
.rpt-page h2.ctlr-conth.doc-cont { position: static; width: auto; height: auto;
  overflow: visible; margin: 0 0 6px; padding: 0; display: flex;
  justify-content: space-between; align-items: flex-start; }
/* On a continuation: the ID in its black box on the left, CONTINUED in the
   border's upper right corner (the BOM's dress); the engine's own
   " (continued)" suffix is hidden because the corner already says it. */
.rpt-page h2.ctlr-conth .ctlr-conth-mark { display: none; }
/* color stated EXPLICITLY: the heading's band dress paints white type for
   its black box, and the mark inherited it — CONTINUED was printing white
   on the white page (owner caught the invisible corner). */
.rpt-page h2.ctlr-conth.doc-cont .ctlr-conth-mark { display: block; font-size: 10px;
  font-weight: 800; color: #111; text-transform: uppercase; letter-spacing: .09em;
  padding: 6px 12px 0 0; }
.rpt-page h2.ctlr-conth.doc-cont .doc-cont-mark { display: none; }
/* The continued block matches the band's block exactly (owner 2026-08-15):
   same 150px, same prose type, same cut — it carries the Description now,
   like the first page. */
.rpt-page h2.ctlr-conth.doc-cont .vc-num-id { margin: 6px 0 0 10px; width: 150px;
  box-sizing: border-box; text-align: center; font-size: 12.5px; line-height: 1.35;
  font-weight: 700; font-style: normal; letter-spacing: 0; padding: 6px 10px;
  border-radius: 2px 12px 2px 12px; display: flex; align-items: center; justify-content: center; }
/* The section identity row: invisible wherever its pill is on the page,
   surfaced by splitTable "(continued)" when rows spill mid-table. */
.rpt-page .ctlr-grp { display: none; }
.rpt-page .ctlr-grp.doc-group-cont { display: table-row; }
.rpt-page .ctlr-grp.doc-group-cont td { font-weight: 800; font-size: 9px;
  text-transform: uppercase; letter-spacing: .07em; border: 0; padding: 2px 0 1px; }
.rpt-page .ctlr-band { display: flex; gap: 14px; align-items: stretch;
  margin: 10px 10px 6px 10px; }
/* All the band's boxes ride the pills column's height (owner 2026-08-15:
   "like we have on other reports") — stretch, no fixed heights. */
/* ONE width on every sheet (owner 2026-08-15) — the photograph's 150px, so
   the band's bookends match. The block holds the DESCRIPTION now (swapped
   with the ID, owner same day), so the type is prose-sized and wraps. */
.rpt-page .ctlr-idbox { background: #000; color: #fff; border-radius: 2px 12px 2px 12px;
  width: 150px; flex: none; box-sizing: border-box; padding: 6px 10px;
  display: flex; align-items: center; justify-content: center; text-align: center;
  font-size: 12.5px; line-height: 1.35; font-weight: 700; }
.rpt-page .ctlr-bt { display: flex; flex-direction: column; justify-content: flex-start;
  min-width: 0; flex: 1; }
.rpt-page .ctlr-bt .pill-label { margin: 1px 0 0; align-self: flex-start; }
/* Description takes the row (owner 2026-08-15: "leaving more room for a
   longer description"); Role sits at the right end of it. */
.rpt-page .ctlr-brow { display: flex; gap: 24px; }
.rpt-page .ctlr-brow > div:first-child { flex: 1; min-width: 0; }
.rpt-page .ctlr-brow > div:last-child { flex: none; min-width: 90px; }
.rpt-page .ctlr-brow .pill-label { margin: 1px 0 0; display: inline-block; }
.rpt-page .ctlr-val { font-size: 11px; line-height: 1.25; }
/* Flat sections (owner 2026-08-15): pill, lead line, table as direct card
   children, margined here since no group div carries them anymore. Protocol
   tables sit 12px further in; the Driven by / Drives line leads tight under
   its pill. */
.rpt-page .ctlr-rule { border-top: 1px solid #ddd; margin: 6px 16px 0; height: 0; }
.rpt-page .vc-card > .ctlr-pill { margin: 5px 16px 2px; }
.rpt-page .vc-card > .rpt-mini { margin: 2px 16px 5px; width: calc(100% - 32px); }
.rpt-page .vc-card > .ctlr-proto-tbl { margin-left: 28px; width: calc(100% - 44px); }
.rpt-page .vc-card > .ctlr-drive { margin: 2px 16px 1px; }
.rpt-page .vc-card > .ctlr-child { margin-left: 16px; margin-right: 16px; }
.rpt-page .vc-card > p.ctlr-child { margin-top: 1px; margin-bottom: 4px; }

/* The device photograph, upper right corner (owner 2026-08-15): absolute so
   it costs the flow nothing, uniform 10px inset, framed always. It is an
   ordinary child of the card, so a split card carries it on the FIRST shell
   only — the heading repeats, the picture does not. */
.rpt-page .vc-card { position: relative; }
.rpt-page .ctlr-photo { position: relative; margin-left: auto; width: 150px; flex: none;
  align-self: stretch; border: 2px solid #111; border-radius: 2px 10px 2px 10px;
  background: #fff; overflow: hidden; }
.rpt-page .ctlr-photo img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
.rpt-page .vc-band.doc-cont { display: block; font-size: 12px; font-weight: 700;
  padding: 8px 16px; color: #fff; }

/* ---- Load Schedule cards (lds-*, own prefix per the namespace rule) -------
   One card per ZONE (owner 2026-08-15): the zone's label in the black swept
   block, the hierarchy it exists within and its level-tracked attributes
   beside it under pills, then the placed-device table. Same clipped-heading /
   corner-CONTINUED dress as the Controller Schedule. */
.rpt-page h2.lds-conth { position: absolute; width: 1px; height: 1px; margin: 0;
  padding: 0; overflow: hidden; }
.rpt-page h2.lds-conth.doc-cont { position: static; width: auto; height: auto;
  overflow: visible; margin: 0 0 6px; padding: 0; display: flex;
  justify-content: space-between; align-items: flex-start; }
.rpt-page h2.lds-conth .lds-conth-mark { display: none; }
/* color EXPLICIT — the band dress paints white type, and an inherited color
   is how a corner mark prints white on white (the Controller lesson). */
.rpt-page h2.lds-conth.doc-cont .lds-conth-mark { display: block; font-size: 10px;
  font-weight: 800; color: #111; text-transform: uppercase; letter-spacing: .09em;
  padding: 6px 12px 0 0; }
.rpt-page h2.lds-conth.doc-cont .doc-cont-mark { display: none; }
.rpt-page h2.lds-conth.doc-cont .vc-num-id { margin: 6px 0 0 10px; width: 150px;
  box-sizing: border-box; text-align: center; font-size: 12.5px; line-height: 1.35;
  font-weight: 700; font-style: normal; letter-spacing: 0; padding: 6px 10px;
  border-radius: 2px 12px 2px 12px; display: flex; align-items: center; justify-content: center; }
.rpt-page .lds-band { display: flex; gap: 14px; align-items: stretch;
  margin: 10px 10px 6px 10px; }
/* The block PAIRS with the stacked fields beside it (owner 2026-08-15):
   codes stacked one per line, the box stretching to the band's height. */
.rpt-page .lds-idbox { background: #000; color: #fff; border-radius: 2px 12px 2px 12px;
  width: 150px; flex: none; box-sizing: border-box; padding: 6px 10px;
  display: flex; align-items: center; justify-content: center; text-align: center;
  font-size: 12.5px; line-height: 1.45; font-weight: 700; }
/* Top-aligned, not centered: the pills' top rides the black block's top
   (owner 2026-08-15). */
.rpt-page .lds-bt { display: flex; flex-direction: column; justify-content: flex-start;
  gap: 4px; min-width: 0; flex: 1; }
/* Organized, not ragged (owner 2026-08-15): both rows share one column
   grid, so the breadcrumb's pills and the attribute pills line up in tidy
   columns down the band. Long composed values (Power Density, Ceiling
   Details) take a double cell rather than squeezing. */
.rpt-page .lds-brow { display: grid;
  grid-template-columns: repeat(auto-fill, minmax(128px, 1fr)); gap: 2px 16px; }
.rpt-page .lds-brow > .lds-wide { grid-column: span 2; }
/* A pill never wraps into a gray blob — it is a label, not a paragraph. */
.rpt-page .lds-brow .pill-label { margin: 1px 0 0; display: inline-block;
  white-space: nowrap; }
.rpt-page .lds-val { font-size: 11px; line-height: 1.25; }
/* The zone's closing line: count and the two wattage sums, ruled off from
   the rows above it. */
.rpt-page .lds-tbl tr.lds-total td { border-top: 1.5px solid #111; border-bottom: 0;
  font-weight: 700; }
/* The card's closing fact when the zone tracks Power Density: the actual
   W per area, beside what the band's allowance promised. */
.rpt-page .vc-card > .lds-density { margin: 2px 16px 5px; font-size: 10.5px; }
/* The heading over the Totals card's roll-up table. Sized between the band and
   the table rather than as a third weight of its own. */
/* The line that closes a capped findings list: what is drawn, what is not, and
   the way to see more. Quiet, because it is not itself a finding. */
.qc-more { display: flex; align-items: center; gap: 10px; padding: 6px 0 2px; }
.qc-more .muted { font-size: 11px; }

.rpt-page .lds-tot-h { margin: 8px 16px 3px; font-size: 11px; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; }
/* The row's identifier reads first (owner 2026-08-15). */
.rpt-page .lds-tbl td:first-child { font-weight: 700; }
/* ONE column contract across every card (owner 2026-08-15: the zones must
   align when stacked): fixed layout reads the colgroup and nothing else,
   so identical widths on every card and every page. Headings wrap to two
   lines; values hold together through non-breaking spaces (99,999 W) and
   Description is the column that wraps. */
.rpt-page .lds-tbl { table-layout: fixed; }
.rpt-page .lds-tbl th { white-space: normal; }
/* Never on two rows (owner 2026-08-15): the identifier, the load type and
   the circuit each hold one line; their columns are sized for it. */
.rpt-page .lds-tbl td:first-child, .rpt-page .lds-tbl td:nth-child(7),
.rpt-page .lds-tbl td:nth-child(11) { white-space: nowrap; }
/* Light interior verticals (owner 2026-08-15, reversing the no-verticals
   opening call): between columns only, never on the outer edges. */
.rpt-page .lds-tbl th + th, .rpt-page .lds-tbl td + td {
  border-left: 1px solid #e4e4e4; padding-left: 4px; }
/* Sixteen columns share the sheet: tighter cell padding than the family
   default, or the table's minimum width pushes past its right margin and
   the border loses its gap (owner caught the flush right edge). */
.rpt-page .lds-tbl th, .rpt-page .lds-tbl td { padding-right: 3px; }
.rpt-page .vc-subband { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
  padding: 7px 16px 2px; border-bottom: 1px solid #ddd; }


.rpt-page .vc-pairs { display: grid; grid-template-columns: 1fr 1fr; column-gap: 22px; }
/* GENERAL BESIDE ACCESS & STARTUP, the pairing the card uses (owner
   2026-08-13). Both are short FIXED sets of key/value rows, so the wrapper's
   height cannot grow with the data — which is what makes it safe to be one
   atomic child of a card the paginator splits between children. The groups that
   carry tables stay full width and stay separate children, because each of
   those can outgrow a page and has to be free to break alone. */
/* The side margin the groups carry as direct children of the card has to move
   to the wrapper, or the right-hand group runs off the sheet. */
.rpt-page .vc-card > .vc-duo { margin: 0 16px; }
.rpt-page .vc-duo { display: grid; grid-template-columns: 1fr 1fr; column-gap: 22px;
  break-inside: avoid; }
.rpt-page .vc-duo > div > .vc-grp { margin: 0; }
.rpt-page .vc-duo > div > .vc-grp:first-of-type { border-top: 0; padding-top: 6px; }
/* An empty group is ONE line: the label, and the answer beside it. */
.rpt-page .vc-grp-empty { display: flex; align-items: baseline; gap: 10px; }
.rpt-page .vc-grp-empty .rpt-none { margin: 0; }
.rpt-page .vc-grp-empty .pill-label { margin-bottom: 0; }
/* A repeating list reads as a small table: the eye compares down a column. */
.rpt-page .rpt-mini { width: 100%; border-collapse: collapse; margin: 4px 0 2px; }
.rpt-page .rpt-mini th { font-size: 8.5px; text-transform: uppercase; letter-spacing: .05em;
  color: #555; text-align: left; border-bottom: 1px solid #ccc; padding: 2px 6px 2px 0; font-weight: 600; }
/* Headings stay LEFT even over numeric columns (owner 2026-08-15: "looks
   weird when they change") — the values right-align, the labels do not. */
.rpt-page .rpt-mini td { font-size: 11px; border-bottom: 1px solid #eee; padding: 2.5px 6px 2.5px 0;
  vertical-align: top; }
.rpt-page .rpt-mini td.num { font-variant-numeric: tabular-nums; }
/* Said plainly, so a blank group cannot be read as a missing one. */
.rpt-page .rpt-none { font-size: 10.5px; color: #777; margin: 3px 0 2px; font-style: italic; }
/* One field per line, for a group living in half a sheet. */
.rpt-page .vc-col { display: grid; grid-template-columns: 1fr; }
.rpt-page .vc-notes { font-size: 11.5px; margin: 3px 0 2px; white-space: pre-wrap; }

/* ---- Site Conditions report (owner 2026-08-14) -----------------------------
   The photographs from one event, laid out on the page. Same grammar as the
   Network and Controller sheets: a ruled band over a framed body, key over
   hairline for the detail, nothing decorated that is not carrying meaning.

   WIDTHS AND HEIGHTS ARE INLINE, not here. Both are computed from the measured
   printable box (see siteRptHtml), because they change with the paper and the
   orientation and the number across the page. A rule in this file would be a
   second opinion about the same number. */
/* The house dress (owner 2026-08-15): swept corners, no gray wash, no
   divider under the event heading — the frame is the boundary. */
.rpt-page .scr-sum { margin: 0 0 14px; border: 2px solid #111; border-radius: 3px 18px 3px 18px;
  overflow: hidden; }
.rpt-page .scr-sum h2 { margin: 0; font-size: 15px; font-weight: 800;
  padding: 8px 14px 2px; }
.rpt-page .scr-sum-b { display: grid; grid-template-columns: repeat(3, 1fr);
  gap: 0 20px; padding: 6px 14px 4px; }
.rpt-page .scr-evnote { font-size: 11px; margin: 0; padding: 2px 14px 8px;
  white-space: pre-wrap; }

/* One zone. The h2 is a DIRECT CHILD, which is what lets a long zone continue
   onto the next sheet under a reprint of its own name. */
.rpt-page .scr-grp { margin: 0 0 10px; }
.rpt-page .scr-grp > h2 { margin: 0 0 6px; font-size: 12px; font-weight: 800;
  text-transform: uppercase; letter-spacing: .06em; color: #111;
  border-bottom: 1px solid #111; padding-bottom: 3px; }
.rpt-page .scr-orphan { font-size: 10px; color: #b00; margin: 0 0 4px; }

/* `wrap` is for the continuation heading, not for the figures: the columns are
   measured to fit and never wrap on their own. When the paginator makes that
   heading visible it takes a full line above the pictures instead of standing
   up as a column beside them. */
.rpt-page .scr-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-start;
  margin: 0 0 10px; }
.rpt-page .scr-row > .scr-conth.doc-cont { flex: 0 0 100%; }
.rpt-page .scr-fig { margin: 0 0 10px; break-inside: avoid; }
/* The frame around the picture is the picture's edge: a photograph of a dark
   ceiling void has no edge of its own and bleeds into the paper. */
.rpt-page .scr-shot { position: relative; border: 1px solid #111; background: #f0f0f0;
  border-radius: 2px 10px 2px 10px; overflow: hidden;
  display: flex; align-items: center; justify-content: center; overflow: hidden; }
.rpt-page .scr-shot img { width: 100%; height: 100%; object-fit: contain; display: block; }
.rpt-page .scr-nofile { font-size: 10px; color: #777; text-align: center;
  padding: 0 10px; font-style: italic; }
/* The reference number, on the picture rather than beside it: this is how one
   of these gets named in an email six months later. */
.rpt-page .scr-n { position: absolute; left: 0; top: 0; background: #111; color: #fff;
  font-size: 10px; font-weight: 700; line-height: 1; padding: 3px 6px;
  font-variant-numeric: tabular-nums; }
.rpt-page .scr-cap { margin: 3px 0 0; }
.rpt-page .scr-line { font-size: 10px; color: #333; line-height: 1.35; }
.rpt-page .scr-sep { color: #aaa; padding: 0 5px; }
.rpt-page .scr-vid { font-weight: 700; }
/* An absence that is a fact about the record, in the same clothes everywhere. */
.rpt-page .scr-none { color: #777; font-style: italic; }
.rpt-page .scr-note { font-size: 11px; margin: 3px 0 0; white-space: pre-wrap;
  line-height: 1.35; }
.rpt-page .scr-meta { margin: 3px 0 0; }
.rpt-page .scr-kv { display: flex; justify-content: space-between; align-items: baseline;
  gap: 8px; border-bottom: 1px solid #e4e4e4; font-size: 10px; line-height: 1.3; }
.rpt-page .scr-kv > span:first-child { color: #666; text-transform: uppercase;
  letter-spacing: .05em; font-size: 8px; white-space: nowrap; }
.rpt-page .scr-kv > span:last-child { text-align: right; }
/* The screen's weather line and its warnings, printed. Same words, same
   distinctions: a reading that is not there says which absence it is. */
.rpt-page .sc-weather { font-size: 10px; color: #333; margin: 3px 0 0; line-height: 1.3; }
.rpt-page .sc-weather.muted { color: #777; font-style: italic; }
.rpt-page .sc-warn { color: #b00; }
/* THE CONTINUATION HEADING, as on a VLAN card: present so the paginator will
   divide a tall figure, clipped on the sheet where the number on the picture
   already says the same thing, and shown only where it is reprinted. */
.rpt-page .scr-conth { position: absolute; width: 1px; height: 1px; margin: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.rpt-page .scr-conth.doc-cont { position: static; width: auto; height: auto;
  overflow: visible; clip-path: none; white-space: normal; margin: 0 0 4px;
  font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .06em; }

/* Project logo container (Setup) + report title-block logos. */
.logo-box { display: inline-flex; align-items: center; justify-content: center; min-width: 54px; height: 34px;
  border: 1px dashed var(--line); border-radius: 6px; padding: 2px 8px; background: var(--panel2); }
.logo-box img { max-height: 28px; max-width: 120px; }
.ws-meta { background: var(--panel2); color: var(--text); border: 1px solid var(--line);
  border-radius: var(--r-ctl); padding: 5px 8px; font-size: 13px; width: 180px; }
.rpt-page .rpt-title { display: flex; align-items: center; gap: 14px; }
.rpt-page .rpt-logo { max-height: 44px; max-width: 160px; }
.rpt-page .rpt-logo-client { display: block; margin-left: auto; margin-bottom: 4px; }
.rpt-page .rpt-phase { font-size: 12px; color: #333; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; }

/* Device Specification sheets — one per device, title-block styling cues:
   rounded frame, capsule labels, black fixture-type box, CONFIDENTIAL pills. */
.rpt-page.rpt-bare { background: transparent; padding: 0; }
/* The paginator owns page breaks now (data-doc-break-before puts one fixture on
   each sheet), so this no longer forces its own — two mechanisms competing for
   the same break is how a blank page appears between every sheet. The bottom
   margin goes with it: the page element supplies the spacing. */
/* Padding was 22/22/14 and the band below added another 12 on top of it, so
   the sheet opened with 34px of nothing (owner 2026-08-14: "There's a lot of
   dead space around the perimeter"). The frame is the edge of the document;
   it does not need a margin to prove it. The reclaimed height also pays for
   the two termination cells added to the scope block. */
/* THE SWEPT CUT, on paper (owner 2026-08-15): rounded upper-right and
   lower-left, squared the other two — the corner treatment the whole app
   wears (--r-card), which itself CAME from LTC's printed material and the
   title block. The frame, the two identity boxes and the photo take it, at
   radii scaled to each box; nothing else on the sheet changes shape. */
.spec-sheet { position: relative; background: var(--spec-paper); color: var(--spec-ink);
  border: var(--spec-frame-rule) solid var(--spec-ink);
  /* STATED, NOT INHERITED. Every block height on this sheet is its type size
     times this number, and the PDF backend has to compute them; inheriting it
     from the app's body rule left the two renderers agreeing only by accident. */
  line-height: var(--spec-lead);
  border-radius: var(--spec-sheet-sweep); padding: var(--spec-sheet-pad); break-inside: avoid; }
/* No rule under the LAST row of a column (owner 2026-08-15: "remove the line
   beneath WARRANTY"). A row's underline separates it from the row below; under
   the final row there is nothing below, so it read as a stray mark hanging
   over the scope block's own heavy rule. Written as last-child rather than
   naming Warranty, because the principle is the position, not the field —
   whatever row ends the column sheds its line. */
.spec-cols > div > .spec-kv:last-child { border-bottom: 0; }

/* NO HAIRLINES AGAINST A FRAMED BODY (owner 2026-08-15: "With a big border
   around the info, I don't think we need the divider line below the header and
   below the footer"). The 1pt rules under the header and over the footer exist
   to separate furniture from an OPEN body — a table of BOM rows runs straight
   into them. A body that is one framed sheet draws its own boundary, so the
   rules doubled it. Scoped by :has() to pages whose body is a spec sheet, so
   the BOM, schedules and every open-table report keep the rule the owner asked
   for in the first place. Keyed on the spec-doc PAGE CLASS, never on the
   page's content: the furniture probe holds no content, so a :has(.spec-sheet)
   version measured the header 26px taller than any real page drew it and
   reported every sheet "taller than one page" while all of them fit. The
   class rides the probe and the sheets alike (openReport opts.docClass). */
.doc-page.spec-doc .doc-head, .doc-page.rk-doc .doc-head, .doc-page.bom-doc .doc-head,
.doc-page.net-doc .doc-head, .doc-page.ipr-doc .doc-head, .doc-page.scr-doc .doc-head { border-bottom: 0; padding-bottom: 2px; margin-bottom: 4px; }
.doc-page.spec-doc .doc-foot, .doc-page.rk-doc .doc-foot, .doc-page.bom-doc .doc-foot,
.doc-page.net-doc .doc-foot, .doc-page.ipr-doc .doc-foot, .doc-page.scr-doc .doc-foot { border-top: 0; padding-top: 2px; margin-top: 4px; }
/* "Separator lines: On" (Report Header & Footer, 2026-08-25) beats the family
   suppression above: a person who chose rules gets them on every page, framed
   body or not. Same three-class specificity as the suppressors — :has() takes
   its argument's — so being LATER in this sheet is what decides it. Since the
   choice arrived the resting sheet draws no rule anywhere (default off), so
   the suppression now only matters to a project that turned rules on. */
.doc-page .doc-head:has(.doc-h-rule) { border-bottom: 1pt solid #999; padding-bottom: 6px; }
.doc-page .doc-foot:has(.doc-f-rule) { border-top: 1pt solid #999; padding-top: 5px; }

/* THE FRAME FILLS THE SHEET (owner 2026-08-14: "the bottom edge of the rounded
   rectangle should reach as close to the bottom margin as the top edge does").
   One specification is one page — `data-doc-break-before` guarantees it is the
   only thing in this body — so the box takes the whole body rather than
   stopping wherever the content happens to end and leaving a third of the page
   outside the border.

   Scoped to a spec sheet that is a DIRECT child of the body, so no other
   report's blocks are stretched. `border-box`, or the padding and the 2px
   border would push it past the bottom. The paginator is unaffected: in its
   measuring stage there is no definite parent height, so 100% resolves to the
   natural height it has always measured. */
.doc-page.spec-doc .doc-body > .spec-sheet { height: 100%; box-sizing: border-box;
  display: flex; flex-direction: column; }
/* The scope block sits ON the bottom edge, and any slack collects ABOVE its
   rule rather than below it (owner 2026-08-14). `margin-top: auto` is the
   whole mechanism: the band and the columns stack from the top, the scope
   grid is pushed to the floor, and the divider it carries travels with it. */
.doc-body > .spec-sheet > .spec-scope { margin-top: auto; }
/* …and the COLUMNS swallow that slack instead of leaving it as a dead band
   above the divider (owner 2026-08-15: the photograph belongs "close to the
   bottom of the main area, near the divider"). flex:1 stretches the grid to
   meet the scope block; its columns stretch with it, and the photo's own
   margin-top:auto walks it down to the floor the grid now reaches. Without
   this the photo pinned to the bottom of a SHORT columns block, 40-80px shy
   of the rule it was asked to sit against. */
.doc-page.spec-doc .doc-body > .spec-sheet > .spec-cols { flex: 1; }
/* THE NUMBERS COME FROM SPEC_GEOM (lib-specsheet.js), because the OEM binder's
   PDF backend has to draw this exact pill and may not keep its own copy of the
   metrics. The pill is one shape across the whole suite — the spec sheet's and
   every other report's have always been identical — so one definition serves
   both, and a change reaches paper and screen together. Only the top margin
   differs on the sheet; see below. */
.pill-label { display: inline-block; background: var(--spec-pill-ground); color: var(--spec-on-dark);
  border-radius: var(--r-ctl);
  font-size: var(--spec-pill); font-weight: 700; text-transform: uppercase;
  letter-spacing: var(--spec-pill-track);
  padding: var(--spec-pill-pad); margin: 8px 0 var(--spec-pill-gap-below); }
/* Tighter on the spec sheet only. Measured (2026-08-15): with Connections and
   Certifications both in the left column, the fullest sheet ran 20px past the
   page, and six pills' worth of 8px top margins is exactly that slack. */
.spec-sheet .pill-label { margin-top: var(--spec-pill-gap-above); }
/* No margin of its own: it is the first thing on the sheet, and the padding
   above already spaces it off the frame. */
/* min-height matches the Bill of Materials band (owner 2026-08-17: "make the
   sizes of the Device Type and Quantity Blocks on the Device Specification
   report match those of the Bill of Materials report"). Once the two boxes
   shrink to BOM scale they no longer set the band's height, so the floor does
   — otherwise a device with a short Manufacturer column would draw a shallower
   band than the same device's BOM card. */
.spec-band { display: flex; gap: var(--spec-band-gap); align-items: stretch; margin-top: 0;
  min-height: var(--spec-band-min-h); }
/* Caption at the top, category the same distance off the bottom, and the code
   centered in whatever is left (owner 2026-08-14). Equal top and bottom
   padding is what makes the two captions symmetrical; the code takes the rest.
   Same construction as the quantity box across the band, so the two black-and-
   white identity blocks are built the one way.
   AT BOM SCALE since 2026-08-17: .bomr-typebox's 92px, 4px/14px padding, 32px
   code and 2px/10px sweep, so the same device's two documents carry the same
   block. A FIXED width, not a min-width: the spec box also carries a category
   line, and as a min-width a long category ("Ceiling Recessed") would widen
   the box past the BOM's and undo the match — pinned, it wraps instead. */
.spec-typebox { background: var(--spec-box); color: var(--spec-on-dark);
  border-radius: var(--spec-box-sweep); padding: var(--spec-typebox-pad);
  width: var(--spec-type-box-w); flex: none; text-align: center; display: flex; flex-direction: column; }
.spec-typebox .code { flex: 1; display: flex; align-items: center; justify-content: center; }
/* Caption and category shrink with the box: at their old sizes neither fits
   the narrower box on one line, and "DEVICE TYPE" wrapping mid-phrase is
   worse than smaller type. */
.spec-typebox span { font-size: var(--spec-caption); text-transform: uppercase;
  letter-spacing: var(--spec-caption-track); color: var(--spec-caption-ink); }
.spec-typebox .code { font-size: var(--spec-code); font-weight: 800; line-height: var(--spec-code-lead); }
.spec-typebox .cat { font-size: var(--spec-cat); text-transform: uppercase;
  letter-spacing: var(--spec-cat-track); color: var(--spec-cat-ink); line-height: var(--spec-cat-lead); }
/* TOP-ALIGNED WITH THE DEVICE TYPE BOX, rows spread evenly down its height
   (owner 2026-08-14). The band stretches every child to the tall black box's
   height, so the column is a flex stack with space-between: Manufacturer's
   pill starts where the box starts, Product Number's row ends where it ends,
   and the separations between the three rows fall out equal instead of being
   hand-tuned margins. */
/* The gap went from 9px to none on 2026-08-17. The Manufacturer column sets
   the band's height now that the boxes no longer do, and its three rows plus
   9px of gaps stood 13px taller than the Bill of Materials band — which would
   have left the two documents' boxes the same width and different heights,
   half a match. Closed, the band measures 95px against the BOM's 94. The rows
   keep their separation from the pills' own margins. */
.spec-mfr { flex: 1; min-width: 0; position: relative; display: flex; flex-direction: column;
  justify-content: space-between; gap: 0; }
/* Block, not inline-block: an inline-block pill sits ON A TEXT LINE, and the
   line's half-leading held every pill ~5px below its row — which is exactly
   the misalignment the owner pointed at, and it also swelled the three rows
   until they overfilled the column and space-between had nothing left to
   distribute. As a block the pill's top IS the row's top. */
.spec-mfr .pill-label { display: block; width: fit-content; margin: 0 0 var(--spec-pill-gap-in-band); }
.spec-mfr-row { display: flex; gap: var(--spec-mfr-row-gap); }
/* Compressed with the boxes (owner 2026-08-17: "This will mean you'll have to
   compress the lines of text on the Dev Spec report... that's ok"). Three
   pill-and-value rows have to live inside a 90px band now instead of the
   150px-wide black box's height. The WEIGHTS are untouched: Manufacturer,
   Product Line and Variation stay bold by his instruction — only their size
   moves. */
.spec-big { font-size: var(--spec-big); font-weight: 700; }
.spec-mid { font-size: var(--spec-mid); font-weight: 600; }
.spec-mono { font-family: var(--mono); font-size: var(--spec-mono); font-weight: 600; }
/* Only INSIDE the band. .spec-mid is also the scope block's value type at the
   foot of the sheet, which no ruling touched and which has all the room it
   ever had. */
.spec-mfr .spec-mid { font-size: var(--spec-mfr-mid); }
/* CUSTOM is a red pill on the Manufacturer row (owner 2026-08-15), sitting at
   the opposite end of the line from the MANUFACTURER pill — pill metrics
   matching .pill-label, red ground, white type. The head row is flex with
   space-between so the two pills bracket the line; when the device is not
   custom the row holds one pill and nothing else changes. */
.spec-mfr-head { display: flex; justify-content: space-between; align-items: center;
  gap: var(--spec-mfr-head-gap); }
/* The section pill's metrics in the custom flag's red: same shape, different
   ground, one set of numbers. */
.spec-custom { display: inline-block; background: var(--spec-custom-ground); color: var(--spec-on-dark);
  border-radius: var(--r-ctl);
  font-size: var(--spec-pill); font-weight: 700; text-transform: uppercase;
  letter-spacing: var(--spec-pill-track); padding: var(--spec-pill-pad); }
/* SAME HEIGHT AS THE DEVICE TYPE BOX (owner 2026-08-14). The band already
   stretches its children; `align-self: flex-start` was opting this one out, so
   it sat short beside a tall black box and the row read as unfinished. With
   the stretch restored the content is centered in the box rather than pinned
   to the top of it. */
/* padding-top is MEASURED, not derived: the Device Type box pads 8px and its
   caption sits on a taller line box, which put this one 7px high when the two
   paddings matched on paper. 13px lands the two captions on one line. */
.spec-qtybox { position: relative; border: var(--spec-frame-rule) solid var(--spec-ink);
  border-radius: var(--spec-box-sweep);
  /* At BOM scale since 2026-08-17: .bomr-qtybox's 96px and 2px/10px sweep.
     Fixed rather than a floor, for the same reason as the Device Type box —
     "Quantity used" is what sets its natural width and it must not push past
     the BOM's. The old 150px was the Device Type box's width (owner
     2026-08-15: "use that width"); the two still mirror each other, both
     having moved together. */
  width: var(--spec-qty-box-w); flex: none;
  /* The old +5px over the black box's padding was a MEASURED correction for
     two captions of different sizes on different line boxes. Both are 7px
     now, so the line boxes match (10.15px, measured 2026-08-17) and the
     correction is gone: 2px of padding plus the 2px border is the black box's
     4px, and the captions land on one line. */
  padding: var(--spec-qtybox-pad); text-align: center; display: flex; flex-direction: column; }
/* The Device Type box's caption, in black on white. Its 2px border plus 6px of
   padding puts it at the same 8px from the top of the band as that box's own
   8px padding, so the two captions sit on one line across the sheet. */
.spec-qtylabel { font-size: var(--spec-caption); font-weight: 700; text-transform: uppercase;
  letter-spacing: var(--spec-caption-track); color: var(--spec-qty-caption); }
/* CENTERED IN THE BOX, not in what the caption left over. As a flex child it
   was centered within the space below the label, and since the padding is
   deliberately lopsided (13 top to align the caption, 6 bottom) that put the
   number visibly high. Taking it out of flow centers it on the box itself,
   which is what the eye measures it against. */
.spec-qtybox .qty { position: absolute; inset: 0; display: flex;
  align-items: center; justify-content: center;
  font-size: var(--spec-qty-num); font-weight: 800; font-variant-numeric: tabular-nums; }
/* No rule above the columns (owner 2026-08-14: "The divider here is not
   needed") — the black type box and the pills already mark where identity
   ends and specification begins. The scope block keeps its heavy rule: down
   there, nothing else marks the boundary. */
.spec-cols { display: grid; grid-template-columns: var(--spec-cols-grid);
  gap: 0 var(--spec-col-gap); margin-top: var(--spec-cols-gap-above);
  padding-top: var(--spec-cols-pad-top); }
/* The right column is a flex column so the photograph can be PINNED to its
   floor (owner 2026-08-15: bottom right, just above the scope info): grid
   stretches both columns to the taller one, and margin-top:auto drops the
   frame to the bottom of that shared height — the same mechanism that holds
   the scope block to the foot of the sheet. */
.spec-cols > div:last-child { display: flex; flex-direction: column; }
.spec-cols > div:last-child .spec-photo { margin-top: auto; }
/* Flex items stretch cross-axis by default, which would pull each pill out to
   the full column width; a pill is exactly as wide as its word. */
.spec-cols > div:last-child > .pill-label { align-self: flex-start; }
/* THE DEVICE PHOTOGRAPH. One column wide, which is what it was asked to be,
   and a 4:3 frame so every sheet in a set has the picture in the same place at
   the same size however the photographs themselves were shot.

   `contain`, never `cover`: this is a specification, and cropping a device to
   fill a frame can cut off the very connector somebody is looking for. A
   portrait photograph letterboxes inside the frame instead, which is honest
   and still reads as deliberate because the frame is constant. */
/* margin-top 2px, not 8: the divider sits 6px under the quantity box
   (.spec-cols margin-top), and .spec-cols already pads 4px below it, so 2 more
   puts the picture the same 6px clear of the line as the box above it. */
/* 2:1, down from 4:3 via 16:9 (owner 2026-08-15: "make the image block
   shorter... we've got a few more things to add to this page"). Measured, not
   chosen: with Connections and Certifications both on the left, the 16:9
   frame left the two photo-carrying sheets 20px past the page, and 2:1 buys
   23 back. The photo is the one block on this sheet that flexes. */
/* WHITE BEHIND THE PHOTO, not grey (owner 2026-08-23). Product photography is
   almost always cut out on white, so a grey field put a visible rectangle
   behind the product on a printed sheet -- and on paper the difference between
   the frame and the page should be the rule, not a panel. */
.spec-photo { border: var(--spec-photo-rule) solid var(--spec-ink);
  border-radius: var(--spec-photo-sweep); overflow: hidden;
  background: var(--spec-paper); aspect-ratio: var(--spec-photo-ratio); margin: var(--spec-photo-gap-y) 0;
  display: flex; align-items: center; justify-content: center; }
/* Classification lives at the head of the left column now (owner 2026-08-15),
   a pill + text block like its neighbors; the band boxes it briefly wore
   (.spec-kindbox/.spec-kind) are retired. */
.spec-photo img { width: 100%; height: 100%; object-fit: contain; display: block; }
.spec-kv { display: flex; justify-content: space-between; gap: var(--spec-kv-gap);
  border-bottom: var(--spec-kv-rule) solid var(--spec-kv-rule-color);
  padding: var(--spec-kv-pad-y) 0; font-size: var(--spec-kv-value); }
.spec-kv span { color: var(--spec-kv-label-color); text-transform: uppercase;
  font-size: var(--spec-kv-label); letter-spacing: var(--spec-kv-label-track);
  padding-top: var(--spec-kv-label-pad-top); }
.spec-kv strong { text-align: right; }
/* pre-line: a Software block is routinely a LIST (owner 2026-08-15), and a
   line break somebody stored is part of what they said. Collapses nothing on
   the single-line values every other text block holds. */
.spec-text { font-size: var(--spec-text); min-height: var(--spec-text-min-h); white-space: pre-line; }
/* THREE ACROSS, not four. The block carries five or six roles now that both
   terminations print, and four columns left an orphan row of one or two. Three
   divides both cases evenly, and the wider cell suits "Line Voltage
   Termination By", which is the longest label on the sheet. The row gap is new
   and load-bearing: with two rows the cells sat directly on each other. */
.spec-scope { display: grid; grid-template-columns: var(--spec-scope-grid);
  gap: var(--spec-scope-row-gap) var(--spec-scope-col-gap);
  border-top: var(--spec-scope-rule) solid var(--spec-ink);
  margin-top: var(--spec-scope-gap-above); padding-bottom: var(--spec-scope-pad-bottom); }
.spec-note { font-size: var(--spec-note); color: var(--spec-note-ink);
  margin-top: var(--spec-note-gap-above); }
@media print { .spec-sheet { margin: 0; border-radius: var(--spec-sheet-sweep); } }

/* THE CONNECTOR PICKER. A <datalist> is drawn by the browser and cannot carry a
   symbol, so the four connector columns get a list of our own (owner
   2026-08-28). It lives on <body> and is positioned in the viewport, because
   the Device Types grid scrolls inside its own box and would clip it. */
.cp-pop { position: fixed; z-index: 90; max-height: 260px; overflow-y: auto;
  background: var(--panel2); border: 1px solid var(--line); border-radius: var(--r-soft);
  box-shadow: 0 10px 26px rgba(0, 0, 0, .45); padding: 4px; }
.cp-opt { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
  background: transparent; border: 0; border-radius: var(--r-ctl); padding: 5px 8px;
  color: var(--text); font: inherit; font-size: var(--fs-small); cursor: pointer;
  white-space: nowrap; }
.cp-opt:hover, .cp-opt.on { background: color-mix(in srgb, var(--accent) 22%, transparent); }
/* THE ICON KEEPS ITS COLUMN whether or not the option has one. A terminal block
   has no face on purpose, and letting its label slide left would make the list
   read as two lists. */
.cp-ico { flex: none; width: 18px; height: 18px; display: inline-flex;
  align-items: center; justify-content: center; color: var(--text-2); }
.cp-opt.on .cp-ico, .cp-opt:hover .cp-ico { color: var(--accent); }
.cp-t { overflow: hidden; text-overflow: ellipsis; }

/* THE OEM BINDER'S SETUP. A short screen, because the questions are short: what
   goes in, how it is divided, and on what paper. It borrows the ask-dialog's
   ground and shape rather than inventing a third kind of overlay. */
.bn-wrap { position: fixed; inset: 0; z-index: 120; display: flex; align-items: center;
  justify-content: center; background: rgba(0, 0, 0, .5); padding: 20px; }
.bn-box { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-card);
  padding: 18px 20px 16px; width: min(460px, 100%); max-height: 90vh; overflow-y: auto;
  box-shadow: 0 18px 48px rgba(0, 0, 0, .5); }
.bn-h { font-size: var(--fs-h3); font-weight: 700; }
.bn-sub { color: var(--text-2); font-size: var(--fs-small); line-height: 1.45; margin: 6px 0 14px; }
/* A GROUP HEADING, not a card in a card: the panel is already a container and
   nesting a second one is the tell of a screen nobody designed. */
.bn-grp { font-size: var(--fs-mini); text-transform: uppercase; letter-spacing: .09em;
  color: var(--dim); margin: 14px 0 6px; }
.bn-opt { display: flex; gap: 9px; align-items: flex-start; padding: 5px 0; cursor: pointer; }
.bn-opt input { margin-top: 2px; flex: none; }
.bn-opt b { font-weight: 600; font-size: var(--fs-body-s); }
.bn-opt em { display: block; font-style: normal; color: var(--dim); font-size: var(--fs-small);
  line-height: 1.4; margin-top: 1px; }
.bn-paper { width: 100%; }
/* WHAT IS ACTUALLY GOING IN, which is the question this screen is really asked.
   It follows the checkboxes, so the answer is never stale. */
.bn-count { color: var(--text-2); font-size: var(--fs-small); margin: 16px 0 0;
  padding-top: 12px; border-top: 1px solid var(--line); }
/* WHAT THE CHECK FOUND. The count above is what this screen already knows; this
   is what the SERVER found when it opened the documents — the page count and,
   more to the point, the ones it cannot use. A download cannot report a
   failure, so the failure is reported before there is a download. */
.bn-report { margin-top: 12px; padding: 10px 12px; border-radius: var(--r-soft);
  background: var(--panel2); border: 1px solid var(--line); }
.bn-r-sum { margin: 0; font-size: var(--fs-body-s); font-weight: 600; }
.bn-r-ok { margin: 4px 0 0; font-size: var(--fs-small); color: var(--text-2); }
.bn-r-bad { margin: 6px 0 0; font-size: var(--fs-small); color: var(--warn); line-height: 1.45; }
/* THE LIST HAS ITS OWN CEILING. Nine unreadable documents is a real answer and
   so is ninety; letting the panel grow to fit them pushes the button that acts
   on the answer off the screen. */
.bn-r-list { margin: 6px 0 0; padding-left: 18px; font-size: var(--fs-small);
  color: var(--text-2); line-height: 1.45; max-height: 168px; overflow-y: auto; }
.bn-r-list li { margin-bottom: 3px; }
.bn-r-list b { color: var(--text); font-weight: 600; }
/* STUCK TO THE FOOT OF THE PANEL, because the panel scrolls. Before the check
   existed the dialog always fitted; with a page count and a list of problems
   above them, the two buttons sat below the fold and the screen looked as
   though it had stopped working. */
.bn-btns { position: sticky; bottom: -16px; display: flex; justify-content: flex-end;
  gap: 8px; margin-top: 14px; padding: 10px 0 0; background: var(--panel);
  box-shadow: 0 -10px 12px -8px var(--panel); }

/* Zone Attribute Definition: compact cards in a responsive grid. */
.ao-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; align-items: start; }
.ao-card { border: 1px solid var(--line); border-radius: var(--r-soft); background: var(--panel2); padding: 10px 12px; }
.ao-card .ao-cat { margin: 0; }
.ao-card .ao-cat-h { margin-bottom: 6px; }
.ao-card .ao-f { font-size: var(--fs-small); padding: 4px 6px; }
.ao-card-wide { grid-column: 1 / -1; }

/* Device ID panel: orderable piece chips. */
.did-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 6px 0; }
.did-chip { display: inline-flex; align-items: center; gap: 5px; border: 1px solid var(--line);
  border-radius: var(--r-ctl); padding: 3px 10px; background: var(--panel2); font-size: var(--fs-small); }
.did-chip.on { border-color: var(--accent); }
.did-chip label { display: inline-flex; align-items: center; gap: 5px; cursor: pointer; }
.did-grip { cursor: grab; color: var(--dim); }
.did-grip:active { cursor: grabbing; }
.did-text, .did-newtext { background: var(--panel);
  border-radius: 5px; padding: 2px 5px; font-size: var(--fs-small); width: 70px; }
.did-serial { border-style: dashed; color: var(--dim); }
.did-example { color: var(--good); border-color: var(--good); }

/* QC tab: checks with findings + lead acknowledgments. */
.qc-check { border: 1px solid var(--line); border-radius: var(--r-soft); background: var(--panel2);
  padding: 10px 14px; margin-bottom: 10px; }
/* `.on` is "this check is switched on" (app.js sets it from the enable
   checkbox), so it is a state signal and it was wearing the left stripe.
   Retired (restyle phase 2a, 2026-08-07, owner) — an enabled check now reads
   as a lit card, which also stops the enabled/disabled difference from living
   at the one edge a reader scanning the checkboxes is least likely to look at. */
.qc-check.on {
  background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 14%, transparent),
                                      color-mix(in srgb, var(--accent) 5%, transparent)), var(--panel2);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  border-top-color: var(--edge-hi); }
.qc-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: var(--fs-body-s); }
.qc-badge { margin-left: auto; font-size: var(--fs-mini); font-weight: 700; border-radius: var(--r-ctl); padding: 2px 10px; }
.qc-badge.open { background: color-mix(in srgb, var(--bad) 18%, transparent); color: var(--bad); }
/* The clear badge's wash was a hand-mixed green (#7fc98f) sitting beside
   --good text — close enough to look intentional, far enough that the light
   theme drifted. Mixed from --good now, so the wash and the ink are the same
   colour by construction. (restyle phase 1a, 2026-08-07) */
.qc-badge.clear { background: color-mix(in srgb, var(--good) 15%, transparent); color: var(--good); }
.qc-finding { display: flex; align-items: center; gap: 8px; border-top: 1px solid var(--line-soft, var(--line));
  padding: 6px 0 6px 6px; font-size: var(--fs-body-s); }
.qc-finding.acked { opacity: .6; }
.qc-finding .qc-flag { flex: none; }
.qc-finding .qc-text { flex: 1; }
.qc-finding .qc-ackinfo { font-size: var(--fs-mini); }
.qc-clear { padding: 8px 0 2px 6px; font-size: var(--fs-small); }

/* The depth badge inside a zone chip: the rank's dot in the level color and
   its L number, so the chip says WHERE in the hierarchy it reads from
   (mockup B + "include the colored dot and L1, L2, L3", 2026-08-28). */
.did-lv { display: inline-flex; align-items: center; gap: 4px; margin-right: 6px;
  font-family: var(--mono); font-size: var(--fs-mini); color: var(--lvc, var(--dim)); }
.did-lvdot { width: 8px; height: 8px; border-radius: 50%; background: var(--lvc, var(--dim)); }
/* One pattern line per branch shape, each led by its zone-type path. */
.did-branchlist { display: flex; flex-direction: column; gap: 3px; }
.did-branch { display: flex; align-items: baseline; gap: 10px; }
.did-branchlab { font-family: var(--mono); font-size: var(--fs-mini); letter-spacing: .07em;
  text-transform: uppercase; color: var(--dim); min-width: 110px; text-align: right; }
.did-branches { align-items: flex-start; }

/* Per-piece separator boxes between Device ID chips. */
.did-sepi { width: 26px; text-align: center; background: var(--panel); color: var(--accent);
  border: 1px dashed var(--line); border-radius: 5px; padding: 2px 3px; font-size: var(--fs-small); font-family: var(--mono); }
.did-sepi:focus { border-color: var(--accent); outline: none; }

/* The characters-will-not-print warning, floated under the field being typed
   into. Not part of the layout on purpose: a grid row cannot grow by two lines
   under one cell without moving every row below it while somebody is typing. */
#paper-warn { position: absolute; z-index: 999; background: var(--panel, #131a26);
  color: var(--fg, #e8eef7); border: 1px solid var(--warn, #faa73f); border-left-width: 3px;
  border-radius: 6px; padding: 7px 11px; font-size: var(--fs-small); line-height: 1.45;
  max-width: 460px; pointer-events: none; box-shadow: 0 6px 20px rgba(0, 0, 0, .35); }

/* Dev mode: floating backend-field hint + menu toggle. */
#dev-hint { position: absolute; z-index: 999; background: #0e1420; color: #9fd0ff;
  border: 1px solid var(--accent); border-radius: 6px; padding: 5px 10px;
  font-family: var(--mono); font-size: var(--fs-small); pointer-events: none; max-width: 420px;
  /* Formula hints are two lines: source on the first, arithmetic on the second. */
  white-space: pre-line; line-height: 1.45; }
.dev-mode-toggle { display: flex; align-items: center; gap: 6px; padding: 6px 12px;
  font-size: var(--fs-small); color: var(--dim); border-top: 1px solid var(--line); cursor: pointer; }

/* Attr option inputs must be allowed to SHRINK inside the cards (flex items
   won't go below their intrinsic width without min-width: 0) — this is what
   made Notes fields spill out of narrow cards. */
.ao-f { min-width: 0; }
.ao-card { overflow: hidden; }
.ao-card .ao-del { width: 24px; }
.ao-card .pd-room-in, .ao-card .pd-code-in { min-width: 0; }

/* QC findings indent under their check title for visual clarity. */
.qc-finding { margin-left: 26px; }

/* ---- 2026-07-22 GUI pass -------------------------------------------------- */

/* Panel headers are flex rows that WRAP — heading text can never overlap a
   floated control. Exports ride the right edge via margin-left:auto. */
.ws-panel-h { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.ws-panel-h > .muted { flex: 0 1 auto; min-width: 0; }
.ws-panel-h .ws-export { float: none; margin-left: auto; }

/* Setup tab: subject nav (left) + one section's content (right). */
.setup-split { display: flex; gap: 16px; align-items: flex-start; }
/* One line per subject, description on hover — kept identical to Operations'
   Settings nav, which is the same component and was compressed for height
   (owner 2026-08-05). These two blocks are deliberate duplicates: the apps load
   separate stylesheets, and the navs are meant to look like the same thing.
   THE TWIN IS /style.css, the block that also begins `.setup-split` — the five
   rules below (.setup-nav / .setup-navbtn / :hover / .active / .setup-nav-label)
   are byte-identical to it and must stay that way; only .setup-body's child
   selector differs, because Operations wraps its content in .section and
   Workspace in .ws-panel. Verified in step after the restyle sweeps
   (restyle phase 1a, 2026-08-07): the 8px corner and the 12.5px label are
   deliberately still literals HERE, because a token can only be introduced on
   both sides at once — tokenise this block in the same commit as its twin or
   the two navs drift the moment the token moves.
   THE TWIN NOW HAS A SECOND HALF (restyle phase 2a, 2026-08-07): the selected
   item's glass tint is appended at the bottom of each file rather than edited
   into these blocks, so the pairing is now FOUR rules across two places per
   app — `.setup-navbtn.active` here and `.setup-navbtn.active` in §4 of the
   Mission Console block at the end of this file, and the same two in
   /style.css. All four move together or the navs drift. */
.setup-nav { flex: none; width: clamp(170px, 22vw, 250px); display: flex; flex-direction: column; gap: 3px;
  position: sticky; top: 14px; }
.setup-navbtn { display: flex; align-items: center; text-align: left;
  border: 1px solid var(--line); border-radius: 8px; background: var(--panel);
  padding: 6px 11px; min-height: 32px; cursor: pointer; color: var(--text); }
.setup-navbtn:hover { border-color: var(--accent); }
.setup-navbtn.active { border-color: var(--accent); background: var(--panel2); }
.setup-nav-label { font-size: var(--fs-body-s); font-weight: 600; line-height: 1.3; }
.setup-body { flex: 1; min-width: 0; }
.setup-body .ws-panel { margin-top: 0; }
/* The tab that could not be drawn (wsPaintFailed): a plain card, the message,
   and the way out. Nothing decorative; this is the app admitting a fault. */
.ws-paint-failed { padding: 20px 24px; max-width: 720px; }
.ws-paint-failed h2 { margin: 0 0 8px; }
.ws-paint-failed p { margin: 0 0 10px; }
/* The owner keeps the section list on the LEFT (2026-08-07): at narrow widths
   the column tightens (clamp above) instead of jumping to the top. The stacked
   fallback survives only for genuinely phone-narrow windows. TWIN RULE — this
   line must match its counterpart in the other stylesheet. */
@media (max-width: 560px) { .setup-split { flex-direction: column; } .setup-nav { width: 100%; position: static; } }

/* Level Definition: attribute chips wrap onto as many lines as they need. */
.ld-attrs { flex-wrap: wrap; }

/* Zones grid alignment: hierarchy indents INSIDE the fixed-width coded-name
   gutter, so the level pulldown / name / code / size columns line up across
   every row. */
.zn-gutter { flex: none; width: 250px; min-width: 250px; overflow: hidden; box-sizing: border-box; }
.zn-size-sp { visibility: hidden; }
/* Attributes live in their own wrapping cluster with caption labels. */
.zn-attrs { display: flex; flex-wrap: wrap; gap: 4px 10px; flex: 1 1 auto; min-width: 240px; align-items: flex-end; }
.zn-fld { display: inline-flex; flex-direction: column; gap: 2px; flex: none; }
.zn-cap { font-size: 9px; text-transform: uppercase; letter-spacing: .06em; color: var(--dim);
  padding-left: 2px; white-space: nowrap; line-height: 1; }
.zn-fld input[type="checkbox"].zn-attr { margin: 3px 0 4px 2px; align-self: flex-start; }
/* Zones toolbar: the sort control sits with the data; exports stay in the header. */
.zn-toolbar { display: flex; align-items: center; gap: 8px; margin: 8px 0 2px; font-size: var(--fs-small); flex-wrap: wrap; }
/* Find + whole-tree fold controls (review #209). The spacer pushes the fold
   buttons to the far end so the two jobs — narrowing, and opening/closing —
   do not read as one row of five equal things. */
.zn-toolbar-sp { flex: 1; }
.zn-find { width: 230px; padding: 4px 10px; font-size: var(--fs-small); margin: 0; }
.zn-findn { font-size: var(--fs-mini); font-variant-numeric: tabular-nums; }
/* chrome from global control rule (restyle phase 1a, 2026-08-07) */
.zn-toolbar select { padding: 4px 8px; font-size: var(--fs-small); }

/* Device List: column headings wrap instead of clipping when narrowed. */
.dev-head .dev-c { white-space: normal; overflow-wrap: break-word; line-height: 1.2;
  align-items: flex-end; padding-top: 3px; padding-bottom: 3px; }

/* QC inline setting (Circuit Load breaker amps). */
.qc-cfg { display: inline-flex; align-items: center; gap: 5px; font-size: var(--fs-small); }
.qc-cfg input { width: 58px; background: var(--panel);
  padding: 3px 6px; font-size: var(--fs-small); text-align: right; }
.qc-cfg input:focus { border-color: var(--accent); outline: none; }

/* "Open LTC Operations" in the project header — identical to the small ghost
   header buttons in Operations (Manage Access / Open LTC Workspace / Edit project). */
.ws-ops-link { display: inline-flex; align-items: center; background: transparent;
  color: var(--text); border: 1px solid var(--line); border-radius: var(--r-ctl);
  padding: 2px 8px; font-size: var(--fs-small); line-height: normal; text-decoration: none; white-space: nowrap; }
.ws-ops-link:hover { border-color: var(--accent); }

/* Tab badges (Setup ✓, blue counts, amber QC) + row flash (QC "Go to" landing) */
/* The washes used to be frozen rgba copies of the dark theme's accents, so on
   the light theme a badge's fill and its own text were different colours.
   Mixed from the tokens now. (restyle phase 1a, 2026-08-07) */
/* Same park, same release as .ws-ro-tag (restyle phase 1b, 2026-08-07): a count
   badge is a chip, and chips are pills. */
.tab-badge { margin-left: 6px; font-size: var(--fs-mini); border-radius: var(--r-ctl); padding: 1px 6px; vertical-align: 1px; }
.tab-badge.open { background: color-mix(in srgb, var(--accent2) 18%, transparent); color: var(--accent2); border: 1px solid color-mix(in srgb, var(--accent2) 50%, transparent); }
/* NOT converted (restyle phase 1a, 2026-08-07): #3fa650 is its own green — it
   is neither --ok nor --good, so naming it would have changed the colour. */
.tab-badge.clear { background: rgba(63,166,80,.15); color: #3fa650; border: 1px solid rgba(63,166,80,.45); }
.tab-badge.off { background: var(--panel2); color: var(--dim); border: 1px solid var(--line); }
.tab-badge.num { background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--accent); border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent); }
.qc-go { white-space: nowrap; }
@keyframes ws-flash-kf { 0%, 60% { background: color-mix(in srgb, var(--accent2) 22%, transparent); } 100% { background: transparent; } }
.ws-flash { animation: ws-flash-kf 2.4s ease-out; }

/* Empty-state shortcut button ("No zones yet — Define Levels in Setup →") */
.ws-empty-go { display: inline-block; margin-left: 8px; background: transparent; color: var(--accent);
  border: 1px solid var(--line); border-radius: var(--r-ctl); padding: 2px 9px; font-size: var(--fs-small); cursor: pointer; }
.ws-empty-go:hover { border-color: var(--accent); }

/* In-cell unit tag for device-grid unit fields: the input shares the cell with
   a small gray unit label to its right (e.g. "12 [in]", "60 [W]"). */
.dev-c-unit .dev-cell, .dev-c-unit .dev-a { flex: 1; min-width: 0; width: auto; }
.u-tag-cell { flex: none; padding: 0 4px 0 3px; }

/* ---- grid clipboard: rectangle selection + transient notice ---------------
   Shift-click marks a copy rectangle from the last-focused cell;
   #ws-notice reports paste/copy/undo results bottom-center. */
.ws-selcell { outline: 2px solid var(--accent); outline-offset: -2px;
  background: color-mix(in srgb, var(--accent) 12%, transparent); border-radius: 3px; }
#ws-notice { position: fixed; bottom: 18px; left: 50%; transform: translateX(-50%);
  max-width: min(720px, 90vw); background: var(--panel); border: 1px solid var(--accent);
  color: var(--text); padding: 9px 16px; border-radius: var(--r-soft); font-size: var(--fs-body-s);
  opacity: 0; pointer-events: none; transition: opacity .25s; z-index: 99;
  box-shadow: 0 8px 28px rgba(0, 0, 0, .45); }
#ws-notice.show { opacity: 1; }
/* The error variant, matching #ops-notice.err in the shared sheet. */
#ws-notice.err { border-color: var(--bad); color: var(--bad); }

/* rows touched by an undo flash briefly so off-screen changes are seen */
.ws-undoflash { animation: ws-undoflash 1.8s ease-out; }
@keyframes ws-undoflash {
  0% { background: color-mix(in srgb, var(--accent) 30%, transparent); }
  100% { background: transparent; }
}

/* ---- Racks & Enclosures: load-center outputs editor ----------------------- */
.cn-outs-btn { font: inherit; font-size: var(--fs-small); color: var(--accent); background: transparent;
  border: 1px dashed var(--line); border-radius: 5px; padding: 1px 8px; cursor: pointer; white-space: nowrap; }
.cn-outs-btn:hover { border-color: var(--accent); }
/* left-stripe sweep, 2026-08-07 — the load-center outputs editor is the same
   kind of attached sub-panel as .nic-panel and converts identically. */
.cn-outs { margin: 2px 0 8px 44px; padding: 8px 10px;
  background-color: var(--panel);
  background-image: linear-gradient(90deg, color-mix(in srgb, var(--accent) 12%, transparent),
                                           color-mix(in srgb, var(--accent) 4%, transparent) 55%,
                                           transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  border-top-color: var(--edge-hi-soft); border-radius: 6px; max-width: 620px; }
.cn-out-row { display: grid; grid-template-columns: 56px 96px 60px 68px 40px 1fr 26px;
  gap: 6px; align-items: center; margin-bottom: 4px; }
.cn-out-row.cn-out-head { font-size: var(--fs-mini); color: var(--dim); text-transform: uppercase; letter-spacing: .04em; margin-bottom: 6px; }
/* chrome from global control rule; only the tighter corner and the density
   are stated here (restyle phase 1a, 2026-08-07). */
.cn-out-row input:not([type=checkbox]), .cn-out-row select {
  border-radius: 5px; padding: 3px 6px; font: inherit; font-size: var(--fs-small); min-width: 0; }
.cn-out-row input[type=checkbox] { justify-self: center; accent-color: var(--accent); }
.cn-out-del { background: transparent; border: 1px solid transparent; color: var(--dim); border-radius: 5px; cursor: pointer; }
.cn-out-del:hover { color: var(--bad); border-color: var(--bad); }
.cn-out-add { margin-top: 2px; }
.cn-groups .dev-grouphdr { font-size: var(--fs-mini); color: var(--dim); text-transform: uppercase; letter-spacing: .05em; }

/* Racks & Enclosures: pinned lead columns + two-tier group headers + bands */
.cn-fixedhdr { flex: none; box-sizing: border-box; }
.cn-subs { min-height: 20px; }
.cn-subhdr { flex: none; box-sizing: border-box; display: flex; align-items: center; gap: 3px;
  font-size: var(--fs-mini); color: var(--dim); text-transform: uppercase; letter-spacing: .04em; padding: 0 4px; }
.cn-subhdr .cn-subbtn { flex: none; width: 16px; padding: 0; background: transparent;
  border: 1px solid transparent; border-radius: 4px; color: var(--dim); cursor: pointer; font-size: var(--fs-micro); }
.cn-subhdr .cn-subbtn:hover { border-color: var(--line); color: var(--text); }
.cn-sub-collapsed-col { flex: none; box-sizing: border-box; }
.cn-row-chev { display: inline-flex; align-items: center; justify-content: center; width: 20px;
  background: transparent; border: 1px solid transparent; border-radius: 4px;
  color: var(--accent); cursor: pointer; font-size: var(--fs-mini); padding: 0; }
button.cn-row-chev:hover { border-color: var(--accent); }
span.cn-row-chev { cursor: default; }

/* rack contents editor (shares the .cn-outs panel shell) */
.cn-ct-row { display: grid; grid-template-columns: 52px 76px 1fr 110px 90px 76px 26px;
  gap: 3px; align-items: center; margin-bottom: 1px; }
.cn-ct-row.cn-out-head { font-size: var(--fs-mini); color: var(--dim); text-transform: uppercase; letter-spacing: .04em; margin-bottom: 3px; }
/* chrome from global control rule; only the tighter corner and the density
   are stated here (restyle phase 1a, 2026-08-07). */
.cn-ct-row input:not([type=checkbox]), .cn-ct-row select {
  border-radius: 4px; padding: 1px 5px; font: inherit; font-size: var(--fs-small);
  min-width: 0; line-height: 1.45; }
.cn-ct-del { background: transparent; border: 1px solid transparent; color: var(--dim); border-radius: 5px; cursor: pointer; }
.cn-ct-del:hover { color: var(--bad); border-color: var(--bad); }
.cn-ct-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 4px; }
.cn-cts { max-width: 760px; }

/* containers' own collapse buttons — styled like the Device List's but NOT
   sharing its wired classes (dev-grpbtn/dev-bandbtn have their own handlers) */
.cn-grpbtn { flex: none; width: 18px; padding: 0; background: transparent; border: 1px solid transparent;
  border-radius: 4px; color: var(--dim); cursor: pointer; font-size: var(--fs-micro); }
.cn-grpbtn:hover { border-color: var(--line); color: var(--text); }
.cn-bandbtn { background: transparent; border: none; color: var(--dim); cursor: pointer;
  padding: 0 4px; font-size: var(--fs-small); }
.cn-bandbtn:hover { color: var(--text); border: none; }

/* rack contents: F/R | Slot | H RU | Item | Device Type | Wattage | Weight |
   BTU | Network; enclosures skip the three rack-position columns.
   Widths come from custom properties set on the PANEL, so a drag updates one
   place and every row plus the header follows — no per-cell bookkeeping and
   nothing can fall out of alignment. */
.cn-ct-row { grid-template-columns:
  var(--w-pos, 72px) var(--w-ru, 48px) var(--w-ruh, 48px) var(--w-label, minmax(90px, 1fr))
  var(--w-dev, 110px) var(--w-watts, 76px) var(--w-weight, 90px) var(--w-btu, 76px)
  var(--w-notes, 150px) var(--w-net, 104px) 22px; }
.cn-ct-row-enc .cnr-lk { display: flex; align-items: center; color: var(--dim); padding: 0 6px;
  box-sizing: border-box; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* THE ENCLOSURE PANEL IS AS WIDE AS ITS COLUMNS. It kept the 760px cap that
   fitted its six columns, and the four product lookups (2026-09-18) took the
   rows to ~1200px: every row ran out of the right-hand edge of its own box
   (owner: "this view is kinda broken"). Same answer the elevation reached:
   fit-content, no wider, scrolling inside itself if the table is narrower. */
.cn-cts.cn-cts-enc { max-width: none; width: fit-content; overflow-x: auto;
  --enc-minw: calc(var(--w-label, 220px) + var(--w-dev, 110px)
    + var(--w-mfr, 110px) + var(--w-product, 110px) + var(--w-part, 100px) + var(--w-pdesc, 160px)
    + var(--w-watts, 76px) + var(--w-weight, 90px) + var(--w-btu, 76px)
    + var(--w-notes, 150px) + var(--w-net, 104px) + 22px + 33px); }
.cn-cts-enc .cn-ct-row-enc, .cn-cts-enc .cn-ct-foot { min-width: var(--enc-minw); }
.cn-ct-row.cn-ct-row-enc { grid-template-columns:
  var(--w-label, 220px) var(--w-dev, 110px)
  var(--w-mfr, 110px) var(--w-product, 110px) var(--w-part, 100px) var(--w-pdesc, 160px)
  var(--w-watts, 76px) var(--w-weight, 90px) var(--w-btu, 76px) var(--w-notes, 150px) var(--w-net, 104px) 22px; }

/* ---- rack contents as an elevation (cnr-mode) -----------------------------
   Front and Rear are stacked boxes at 24px per RU; each ITEM is a full editing
   row absolutely positioned at its slot, so the row is the block. The columns
   are the panel's own --w vars, shared with the header, minus F/R and Slot —
   the box an item sits in and its height ARE those facts. */
/* The elevation needs its own width and NOT its container's: the 760px cap
   squeezed the Network column clean off, but the panel also lives inside the
   horizontally-scrolling containers table, so an uncapped block stretched to
   the table's full scroll width and the Item column swallowed thousands of
   pixels. fit-content is the fix for both — as wide as its columns, no wider.
   --cnr-minw is the row template's own minimum, so the boxes and header stay
   full width if the panel ever does have to scroll. */
.cn-cts.cnr-mode { max-width: none; width: fit-content; overflow-x: auto;
  --cnr-minw: calc(38px + 18px + var(--w-did, 104px) + var(--w-ruh, 48px) + var(--w-label, 220px) + var(--w-dev, 110px)
    + var(--w-mfr, 110px) + var(--w-product, 110px) + var(--w-part, 100px) + var(--w-pdesc, 160px)
    + var(--w-watts, 76px) + var(--w-weight, 90px) + var(--w-btu, 76px)
    + var(--w-notes, 150px) + var(--w-net, 104px) + 22px + 36px); }
.cnr-bay, .cnr-head, .cnr-row.cnr-na, .cnr-mode .cn-ct-band, .cnr-face-l { min-width: var(--cnr-minw); }
.cnr-row { grid-template-columns:
  18px var(--w-did, 104px) var(--w-ruh, 48px) var(--w-label, 220px) var(--w-dev, 110px)
  var(--w-mfr, 110px) var(--w-product, 110px) var(--w-part, 100px) var(--w-pdesc, 160px)
  var(--w-watts, 76px) var(--w-weight, 90px) var(--w-btu, 76px)
  var(--w-notes, 150px) var(--w-net, 104px) 22px; }
/* Lookup cells: values that FOLLOW the Device Type link, so they read as
   facts, not fields — no input chrome, dim until a device is linked. */
.cnr-did { font-family: var(--mono); font-size: var(--fs-mini); }
.cnr-bay .cnr-row .cnr-lk, .cnr-row.cnr-na .cnr-lk { display: flex; align-items: center;
  color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cnr-bay .cnr-row { position: absolute; left: 34px; right: 4px; margin: 0; align-items: center;
  border: 1px solid var(--line); border-radius: 3px; box-sizing: border-box;
  background: var(--panel); overflow: hidden; }
.cnr-bay .cnr-row:hover { border-color: var(--accent); }
.cnr-row.cnr-clash, .cnr-row.cnr-over { border-color: var(--bad);
  background: color-mix(in srgb, var(--bad) 12%, var(--panel)); }
.cnr-row.cnr-dragging { opacity: .4; }
/* Controls sized for a single 30px rack unit: 22px border-box inside a 28px
   row interior leaves 3px of visible air above and below. EVERY child is in
   this list — the grid's row track is as tall as its TALLEST item, so one
   unsized control (the ✕, inheriting the global button padding) stretched the
   track past the rack unit and pushed all the others low and clipped. */
.cnr-bay .cnr-row .cn-ct, .cnr-bay .cnr-row .nic-btn, .cnr-bay .cnr-row .cn-ct-del {
  box-sizing: border-box; height: 22px; padding: 0 6px; font-size: var(--fs-small); line-height: 20px; }
.cnr-bay .cnr-row .cn-ct-del { padding: 0; }
.cnr-handle { cursor: grab; color: var(--dim); text-align: center; font-size: var(--fs-small);
  user-select: none; }
.cnr-handle:hover { color: var(--text); }
 /* The header shares the rows' template but not their box, so it takes the
   same inset the ruler gutter gives the rows — or every label sits one gutter
   left of its column. */
.cnr-head { position: static; border: 0; background: transparent; margin: 0 6px 2px 36px; }

/* The face label reuses the band look, so Front / Rear read as the sections
   the dropdown used to encode. */
.cnr-face-l { display: flex; align-items: center; gap: 8px; margin: 10px 0 4px;
  font-size: var(--fs-mini); font-weight: 600; text-transform: uppercase; letter-spacing: .06em;
  color: var(--dim); }
.cnr-face-l .cn-ct-band-r { flex: 1; border-top: 1px solid var(--line); }

.cnr-bay { position: relative; border: 2px solid var(--line); border-radius: 4px;
  background: var(--panel2); overflow: hidden; }
/* A NIC panel opened from a rack row sits IN the bay, directly under its row,
   in the space that row's rack unit has been stretched to provide (see
   cnrLayout). Absolutely positioned on the same left/right rails as the rows
   so its columns line up with them, and opaque so the grid lines behind it do
   not read through the fields. */
.cnr-bay .nic-panel.cnr-netpanel { position: absolute; left: 34px; right: 4px; margin: 0;
  z-index: 2; background: var(--panel); border: 1px solid var(--line);
  border-radius: 3px; box-sizing: border-box; }
.cnr-gl { border-top: 1px solid color-mix(in srgb, var(--line) 35%, transparent);
  box-sizing: border-box; display: flex; align-items: center; }
.cnr-gl:first-child { border-top: 0; }
.cnr-ru { width: 26px; text-align: right; padding-right: 6px; font-size: 9px;
  color: var(--dim); font-variant-numeric: tabular-nums; }
/* Drop preview: exactly the units the item would occupy; red = refused. */
.cnr-ghost { position: absolute; left: 34px; right: 4px; box-sizing: border-box;
  border: 1.5px dashed var(--accent); border-radius: 3px; pointer-events: none;
  background: color-mix(in srgb, var(--accent) 14%, transparent); }
.cnr-ghost.cnr-ghost-bad { border-color: var(--bad);
  background: color-mix(in srgb, var(--bad) 14%, transparent); }
.cnr-ghost-l { position: absolute; left: 6px; top: 50%; transform: translateY(-50%);
  font-size: var(--fs-micro); font-weight: 600; color: var(--text); white-space: nowrap; }
/* The unrack drop zone: exists only while a drag is in flight, so it costs no
   space the rest of the time. */
.cnr-unrack { display: none; margin: 8px 0 0; padding: 10px; border: 1.5px dashed var(--line);
  border-radius: 6px; text-align: center; font-size: var(--fs-small); color: var(--dim);
  min-width: var(--cnr-minw); box-sizing: border-box; }
.cnr-mode[data-drag] .cnr-unrack { display: block; }
.cnr-unrack.cnr-unrack-on { border-color: var(--accent); color: var(--text);
  background: color-mix(in srgb, var(--accent) 10%, transparent); }

/* Unracked rows sit in normal flow under the boxes. */
/* UNDER THE SAME COLUMNS AS THE BAY ROWS. A bay row sits 36px in from the
   bay's outer edge (2px border + the 34px ruler) and 6px short of its right;
   the heading already wears those margins. The not-racked rows had none, so
   every one of their columns sat 36px left of the heading and the rows above
   (owner 2026-09-18: "the vertical lines are not consistent"). Same margins,
   and the minimum width shrinks by the same 42px so the right edges meet. */
.cnr-row.cnr-na { position: static; margin: 2px 6px 2px 36px; box-sizing: border-box;
  min-width: calc(var(--cnr-minw) - 42px); }
.cnr-nets { margin-top: 4px; }

/* The racked chip on a Device Schedule row, and the three-way ask. */
.sc-rack { margin-left: 5px; font-size: var(--fs-mini); cursor: default; }
/* The app's own question box now lives in the SHARED sheet (style.css), because
   Operations needs the same dialog for the same reason — see the note there. */

/* Front / Rear / N/A bands within a rack's contents. Deliberately light — a
   label and a rule, not another panel, so grouping costs almost no height. */
.cn-ct-band { display: flex; align-items: center; gap: 7px; margin: 5px 0 2px; }
.cn-ct-band:first-child { margin-top: 0; }
.cn-ct-band-l { font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .06em; color: var(--dim); flex: none; }
.cn-ct-band-n { font-size: var(--fs-micro); color: var(--dim); opacity: .75; flex: none; }
.cn-ct-band-r { flex: 1; height: 1px; background: var(--line-soft, var(--line)); opacity: .55; }

/* header cells carry a sort button and a resize grip, like the other grids */
.cn-ct-h { display: flex; align-items: center; gap: 2px; min-width: 0; position: relative; }
.cn-ct-h > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cn-ct-sortbtn { background: none; border: none; color: inherit; font: inherit; cursor: pointer;
  padding: 0; text-transform: inherit; letter-spacing: inherit; white-space: nowrap; }
.cn-ct-sortbtn:hover { color: var(--text); }
.cn-ct-h-on { color: var(--accent); }
.cn-ct-grip { position: absolute; right: -3px; top: -2px; bottom: -2px; width: 7px; cursor: col-resize; }
.cn-ct-grip:hover { background: color-mix(in srgb, var(--accent) 45%, transparent); border-radius: 2px; }
/* a contents item's own NIC panel once sat flush left "so its columns have
   room to breathe" — reversed 2026-08-16: every attached expansion keeps the
   34px indent, because the indent is what ties it to the row above. */
.cn-sum { font-size: var(--fs-small); align-items: center; display: inline-flex; justify-content: flex-end; padding-right: 8px; font-variant-numeric: tabular-nums; }
.cn-cts .cn-ct[placeholder]:not([value=""]) { }
.cn-cts input.calc-cell { color: var(--accent); font-style: italic; }
.cn-cts input.calc-cell::placeholder { color: color-mix(in srgb, var(--accent) 70%, transparent); font-style: italic; opacity: 1; }
/* An overridden value is a REAL entry, so it stays upright and normal-coloured;
   only the dashed outline turns amber (the shared rule in /style.css). Italic
   here means "this came from somewhere else" — the opposite of an override. */
.cn-cts input.calc-overridden { color: var(--text); font-style: normal; }

/* load-center outputs: Slot | Circuit | Addr | Type | V | Limit A | Ctrl | Label */
.cn-outs:not(.cn-cts) .cn-out-row { grid-template-columns:
  var(--w-oslot, 46px) var(--w-ocirc, 56px) var(--w-oaddr, 58px) var(--w-otype, 92px)
  var(--w-ov, 52px) var(--w-olimit, 62px) var(--w-octrl, 38px)
  var(--w-olabel, minmax(90px, 1fr)) 22px; }
/* Outputs get the same density treatment as contents — same panel, same job. */
.cn-out-row { gap: 3px; margin-bottom: 1px; }
.cn-outs:not(.cn-cts) .cn-out-row input:not([type=checkbox]),
.cn-outs:not(.cn-cts) .cn-out-row select { padding: 1px 5px; font-size: var(--fs-small); line-height: 1.45; border-radius: 4px; }
.cn-outs input.calc-cell { color: var(--accent); font-style: italic; }
.cn-outs input.calc-cell::placeholder { color: color-mix(in srgb, var(--accent) 70%, transparent); font-style: italic; opacity: 1; }
.cn-outs { max-width: 780px; }

/* container templates panel (Setup) */
.cn-tpl-list { display: flex; flex-direction: column; gap: 6px; max-width: 720px; }
.cn-tpl-row { display: flex; align-items: center; gap: 10px; padding: 7px 10px;
  background: var(--panel2); border: 1px solid var(--line); border-radius: 7px; font-size: var(--fs-body-s); }
.cn-tpl-row .muted { flex: 1; font-size: var(--fs-small); }
.cn-tpl-save { background: transparent; border: 1px solid transparent; border-radius: 4px; cursor: pointer; padding: 0 2px; }
.cn-tpl-save:hover { border-color: var(--accent); }

/* ---- NDA + the printed CONFIDENTIAL stamp (owner 2026-08-05) --------------
   The chip itself (.nda-chip) and the stamp (.pr-stamp) are defined once in the
   shared /style.css and used verbatim here, so the two apps cannot drift. Only
   the placement inside a Workspace report belongs in this file. */
.ws-proj .code .nda-chip, .proj-head .nda-chip { vertical-align: middle; }
/* The stamp sits at the top of the report's right-hand identity column; .proj
   is right-aligned and .pr-stamp carries its own auto left margin. */
.rpt-page .proj .pr-stamp { margin-bottom: 6px; }
/* Names the NDA under the title block, on every sheet. */
.rpt-page .rpt-nda { border-top: 1px solid #999; margin: -6px 0 12px; padding-top: 5px;
  font-size: 10.5px; color: #444; }

/* Device Specification used to draw its own identity: a CONFIDENTIAL pill, a
   project/client topline, and a footer with the sheet number. All of that is
   the shared page furniture now (LTCDoc.furniture), so those rules and their
   markup are gone — the sheet keeps only what is specific to a fixture.
   NOTE: its confidentiality mark is now the standard one, which means it
   follows the project's stamp setting instead of being unconditional. That is
   a deliberate change and worth the owner knowing. */

/* ---- Issued reports (Reports tab, owner 2026-08-05) -------------------------
   A draft is work in progress and reads as such; a sealed issue is a record of
   what was sent, so it is quieter and carries no controls but View. */
/* The composer row. Every control is a labelled column (.iss-f); the row wraps
   on a narrow window. Sizes are set on the FIELD, never on the bare input —
   an earlier `.iss-new input { flex: 1 1 220px }` still matched once the inputs
   moved inside a column-direction .iss-f, where a flex-basis governs HEIGHT,
   and blew the date and revision boxes up to 220px tall. */
.iss-new { display: flex; gap: 10px; align-items: flex-end; margin-bottom: 12px; flex-wrap: wrap; }
.iss { border: 1px solid var(--line); border-radius: var(--r-soft); background: var(--panel2);
  padding: 9px 12px; margin-bottom: 9px; }
/* Draft vs sealed is the single most consequential state on this tab — one is
   still editable, the other is a record of what was sent — and it was being
   carried by a 3px mark at the left edge. Stripe retired (restyle phase 2a,
   2026-08-07, owner); the state now tints the whole card, amber for the draft
   still in progress and green for the sealed issue, which is the same amber /
   green pairing the .iss-draft / .iss-sealed edges used. */
.iss-draft {
  background: linear-gradient(135deg, color-mix(in srgb, var(--accent2) 14%, transparent),
                                      color-mix(in srgb, var(--accent2) 5%, transparent)), var(--panel2);
  border: 1px solid color-mix(in srgb, var(--accent2) 30%, transparent);
  border-top-color: var(--edge-hi); }
.iss-sealed {
  background: linear-gradient(135deg, color-mix(in srgb, var(--ok) 14%, transparent),
                                      color-mix(in srgb, var(--ok) 5%, transparent)), var(--panel2);
  border: 1px solid color-mix(in srgb, var(--ok) 30%, transparent);
  border-top-color: var(--edge-hi); }
.iss-head { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.iss-name { font-size: var(--fs-body); font-weight: 600; color: var(--text-strong); }
.iss-rev { font-size: var(--fs-mini); font-weight: 700; letter-spacing: .06em; border: 1px solid var(--line-strong);
  border-radius: var(--r-ctl); padding: 0 7px; color: var(--dim); }
.iss-state { font-size: var(--fs-small); color: var(--dim); }
.iss-note { font-size: var(--fs-small); color: var(--dim); margin-top: 3px; }
.iss-docs { margin-top: 7px; display: flex; flex-direction: column; gap: 2px; }
.iss-doc { display: flex; align-items: center; gap: 10px; padding: 4px 7px;
  background: var(--panel); border-radius: 6px; font-size: var(--fs-body-s); }
.iss-doc-t { flex: 1; min-width: 0; color: var(--text); }
.iss-doc-m { font-size: var(--fs-mini); color: var(--dim); white-space: nowrap; }
.iss-view { flex: none; }

/* The issue composer: phase · milestone · date · revision, with a live preview
   of the name they assemble into (owner 2026-08-05). */
.iss-f { display: flex; flex-direction: column; gap: 3px; flex: 0 0 auto; }
.iss-f > span { font-size: var(--fs-mini); color: var(--text-2); text-transform: uppercase; letter-spacing: .05em; }
/* One height for every control in the row, so the line reads evenly. */
/* chrome from global control rule (restyle phase 1a, 2026-08-07) */
.iss-f select, .iss-f input { padding: 0 8px; font: inherit; font-size: var(--fs-body-s);
  height: 30px; box-sizing: border-box; }
.iss-f-phase select { min-width: 210px; }
.iss-f-milestone select { min-width: 130px; }
.iss-f-date input { width: 150px; }
.iss-f-rev input { width: 110px; }
.iss-preview { flex: 1 1 180px; min-width: 160px; font-size: var(--fs-body-s); color: var(--dim);
  align-self: flex-end; padding-bottom: 7px; }
.iss-preview b { color: var(--accent2); }

/* ==========================================================================
   DRAWING LINK (Setup → Drawing Link, 2026-08-08)
   The field map is a patch bay: the drawing's fields on the left, ours on the
   right, drawn connections between. Colour carries the DIRECTION of a strand
   and a midpoint marker repeats it, because "who wins this field" is the one
   thing that must be readable without clicking anything.
   Placed above the restyle voice blocks so those still have the last word.
   ========================================================================== */
.vwx-band { display: flex; gap: 14px; flex-wrap: wrap; padding: 10px 12px; margin-bottom: 10px;
  border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--line));
  border-radius: var(--r-soft); background: var(--panel2); }
.vwx-id { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--dim); }
.vwx-id select { min-width: 170px; }

.vwx-tools { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; }
.vwx-tools #vwx-q { width: 210px; }
.vwx-showall { font-size: 12px; color: var(--dim); display: flex; align-items: center; gap: 5px; }
.vwx-legend { margin-left: auto; display: flex; align-items: center; gap: 8px;
  font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; color: var(--dim); }
.vwx-legend i { display: inline-block; width: 16px; height: 3px; border-radius: 2px; margin-right: 2px; vertical-align: middle; }
.vwx-legend i.w-in { background: var(--ltc-steel); }
.vwx-legend i.w-out { background: var(--accent2); }
.vwx-legend i.w-ask { background: var(--ltc-purple); }
.vwx-legend i.w-once { background: var(--ltc-green); }
/* The dot's fill is the connected/available state — stated here because a
   filled circle next to a hollow one is information nobody can be expected to
   infer (owner 2026-08-08). Same shape and size as the real thing. */
.vwx-legend b.vwx-legdot { display: inline-block; width: 9px; height: 9px; border-radius: 50%;
  background: var(--surface-0); border: 2px solid var(--line-strong); margin-right: 2px;
  vertical-align: middle; }
.vwx-legend b.vwx-legdot.on { background: var(--accent); border-color: var(--accent); }

/* BANDS, not two independent stacks: each grid ROW holds one group from each
   side, so matching group names begin at the same height and their wires run
   almost straight across (owner 2026-08-08). Rows within a group are not
   aligned to each other — only the groups are.

   Nothing scrolls independently either. The columns did at first, and the
   wires drew to dots that had scrolled out of their box — strands leaving the
   panel and running down the page. The patch grows to its content and the
   PAGE scrolls, so an endpoint is always where the wire says it is. */
.vwx-patch { position: relative; display: grid; grid-template-columns: 1fr 1fr;
  column-gap: 120px; row-gap: 4px; align-items: start; padding: 8px 0; }
.vwx-cell { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
/* The overlay spans every band. It is absolutely positioned, so it is already
   out of the grid flow — do NOT give it a grid area as well: that changes its
   containing block, and `inset: 0` then measures the grid tracks instead of
   the patch, which offsets every wire by the patch's padding. */
.vwx-grp { font-family: var(--mono); font-size: var(--fs-mini); text-transform: uppercase; letter-spacing: .1em;
  color: var(--faint); padding: 8px 2px 2px; }
.vwx-f { position: relative; display: flex; align-items: center; gap: 8px;
  padding: 5px 9px; border: 1px solid var(--line); border-radius: var(--r-soft);
  background: var(--panel2); font-size: var(--fs-body-s); }
.vwx-f.on { border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); background: var(--surface-3); }
.vwx-f.sel { border-color: var(--accent); box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent); }
.vwx-f.locked { opacity: .72; }
.vwx-fl { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vwx-ft { font-family: var(--mono); font-size: var(--fs-mini); color: var(--faint); }
.vwx-lock { font-size: 10px; }
.vwx-f.absent { opacity: .6; border-style: dashed; }
.vwx-absent { font-family: var(--mono); font-size: 10px; text-transform: uppercase;
  letter-spacing: .06em; color: var(--faint); white-space: nowrap; }
/* The connector. Left-column dots sit on the right edge, right-column dots on
   the left, so the wires leave and arrive where the eye expects. */
.vwx-dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto;
  background: var(--surface-0); border: 2px solid var(--line-strong); cursor: crosshair; }
.vwx-f.on .vwx-dot { background: var(--accent); border-color: var(--accent); }
.vwx-left .vwx-f { padding-right: 4px; }
.vwx-right .vwx-f { padding-left: 4px; }
.vwx-patch.dragging .vwx-f { cursor: crosshair; }
.vwx-patch.dragging .vwx-f:hover { border-color: var(--accent); }

/* width/height 100% are REQUIRED, not decoration: `inset: 0` cannot stretch a
   replaced element that has its own width/height, and an <svg> defaults to
   300x150. Sized here in CSS so the element genuinely fills the patch; the
   script then only sets the viewBox, and never the width/height attributes —
   writing those from a measurement of the SVG itself made it lock to its own
   default size and clip every wire outside 300x150. */
.vwx-wires { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.vwx-wire { fill: none; stroke-width: 2; pointer-events: stroke; cursor: pointer; opacity: .85; }
/* Reference strands — the read-only truth we merely keep visible — are the
   most numerous by far and all land on one target. Drawn quieter so the
   handful of strands that actually EXCHANGE data stay readable through them. */
.vwx-wire.w-in { stroke-width: 1.5; opacity: .38; }
.vwx-wire.w-in:hover, .vwx-wire.w-in.sel { opacity: 1; stroke-width: 2.5; }
.vwx-tip.w-in { opacity: .5; }
.vwx-wire:hover, .vwx-wire.sel { opacity: 1; stroke-width: 3; }
/* Stroke belongs to the wire, fill to the marker, and the two are kept in
   SEPARATE rules on purpose: a shared `.vwx-wire.w-out, .vwx-tip.w-out { fill }`
   out-specifies `.vwx-wire { fill: none }` and fills the bezier itself, which
   turns every strand into a solid ribbon. */
.vwx-wire.w-in { stroke: var(--ltc-steel); }
.vwx-wire.w-out { stroke: var(--accent2); }
.vwx-wire.w-ask { stroke: var(--ltc-purple); }
.vwx-wire.w-once { stroke: var(--ltc-green); stroke-dasharray: 5 4; }
.vwx-wire.w-temp { stroke: var(--accent); stroke-dasharray: 4 4; }
.vwx-tip { stroke: none; }
.vwx-tip.w-in { fill: var(--ltc-steel); }
.vwx-tip.w-out { fill: var(--accent2); }
.vwx-tip.w-ask { fill: var(--ltc-purple); }
.vwx-tip.w-once { fill: var(--ltc-green); }

/* The strand inspector STICKS to the bottom of the viewport while the field
   list scrolls past it (owner 2026-08-08: "needs to be visible all the time").
   It sat under a patch two thousand pixels tall, so clicking a wire near the
   top meant scrolling to the floor to find out what you had selected. */
.vwx-edit { position: sticky; bottom: 0; z-index: 3;
  margin-top: 10px; padding: 9px 12px; border: 1px solid var(--line);
  border-radius: var(--r-soft); background: var(--panel);
  box-shadow: 0 -6px 18px -8px rgba(0,0,0,.7); font-size: var(--fs-body-s);
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.vwx-owns { display: flex; gap: 12px; flex-wrap: wrap; }
.vwx-owns label { display: flex; align-items: center; gap: 4px; color: var(--dim); }
.vwx-owns label.off { opacity: .4; }

/* ---- the sync round trip ---- */
.vwx-sync { margin-top: 18px; border-top: 1px solid var(--line); padding-top: 14px; }
.vwx-docs { display: flex; flex-direction: column; gap: 6px; margin-bottom: 10px; }
.vwx-doc { display: flex; align-items: center; gap: 10px; font-size: var(--fs-body-s);
  padding: 7px 10px; border: 1px solid var(--line); border-radius: var(--r-soft); background: var(--panel2); }
.vwx-doc .vwx-dl { margin-left: auto; display: flex; gap: 8px; white-space: nowrap; align-items: center; }
/* BETA on a Setup subject — the section works, but its round trip is still
   being proven against a real Vectorworks (owner 2026-08-08). */
.setup-beta { margin-left: 8px; font-family: var(--mono); font-size: var(--fs-micro);
  letter-spacing: .12em; padding: 1px 5px; border-radius: var(--r-ctl);
  border: 1px solid var(--accent2); color: var(--accent2); vertical-align: middle; }
/* SOON on a Setup subject: the section and its button exist so the shape of
   Setup is settled, but there is nothing to configure yet. Same badge as BETA
   because it is the same kind of statement about a subject, in the dim ink
   rather than the accent, because BETA means "works, still being proven" and
   this means "does not work yet" — the stronger claim should stay louder. */
.setup-soon { margin-left: 8px; font-family: var(--mono); font-size: var(--fs-micro);
  letter-spacing: .12em; padding: 1px 5px; border-radius: var(--r-ctl);
  border: 1px dashed var(--line-strong); color: var(--dim); vertical-align: middle; }
.vwx-drop { display: flex; align-items: center; gap: 10px; padding: 16px;
  border: 1px dashed var(--line-strong); border-radius: var(--r-soft); font-size: var(--fs-body-s); }
.vwx-drop.over { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); }

/* The plug-in is the faster road; the file above it is the one that always
   works. Collapsed by default and sitting UNDER the drop zone, so neither
   reads as deprecated and the instructions are there when wanted, not in the
   way when not. */
.vwx-plugin-note { margin: 10px 2px 0; font-size: 12px; line-height: 1.55; max-width: 70ch; }
.vwx-plugin { margin-top: 12px; border: 1px solid var(--line); border-radius: var(--r-soft);
  background: color-mix(in srgb, var(--panel) 60%, transparent); }
.vwx-plugin > summary { cursor: pointer; padding: 11px 14px; font-size: var(--fs-body-s);
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; list-style: none; }
.vwx-plugin > summary::-webkit-details-marker { display: none; }
.vwx-plugin > summary::before { content: '▸'; color: var(--dim); font-size: 10px; }
.vwx-plugin[open] > summary::before { content: '▾'; }
.vwx-plugin > summary:hover { background: color-mix(in srgb, var(--accent) 6%, transparent); }
.vwx-plugin > summary .muted { font-size: var(--fs-small); }
.vwx-plugin-body { padding: 0 16px 14px 30px; font-size: var(--fs-body-s); line-height: 1.6; max-width: 78ch; }
.vwx-plugin-body p { margin: 0 0 10px; }
.vwx-steps { margin: 4px 0 14px; padding-left: 20px; }
.vwx-steps li { margin-bottom: 9px; }
.vwx-steps .vwx-dl { display: inline-flex; gap: 8px; flex-wrap: wrap; margin-left: 4px; }
/* An anchor never picked up button.ghost — it is a button selector — so a
   download link rendered as bare link text against a dark panel. These are the
   primary action of the section and have to read as controls. */
.vwx-dl a.ghost { display: inline-flex; align-items: center; background: transparent;
  color: var(--text); border: 1px solid var(--line); border-radius: var(--r-ctl);
  padding: 2px 8px; font-size: var(--fs-small); line-height: normal;
  text-decoration: none; white-space: nowrap; }
.vwx-dl a.ghost:hover { border-color: var(--accent); color: var(--accent); }
.vwx-plugin-body a:not(.ghost) { color: var(--accent); text-decoration: underline;
  text-underline-offset: 2px; }
/* Which Workspace served the page — and therefore which one the downloaded
   script will talk to. Named out loud because a drafter may have a sandbox
   copy and a live copy installed at the same time (owner 2026-08-08). */
.vwx-where { --vec: var(--accent); margin: 12px 0; padding: 9px 12px;
  border-radius: var(--r-card);
  border: 1px solid color-mix(in srgb, var(--vec) 45%, var(--line));
  border-top-color: var(--edge-hi-soft);
  background-color: color-mix(in srgb, var(--vec) 9%, transparent);
  font-size: 12px; line-height: 1.55; }
.vwx-where code { font-family: var(--mono); font-size: var(--fs-small); }
.vwx-where.dev { --vec: var(--accent2); }
.vwx-substeps { margin: 6px 0 4px; padding-left: 2px; }
.vwx-substeps > div { margin: 3px 0; }

/* The one install step that is easy to get wrong, and whose failure looks like
   a broken script rather than a wrong setting (owner hit it, 2026-08-08).

   left-stripe sweep: this shipped with a 2px accent bar down its left edge —
   the tinted-box-with-a-coloured-rule that every generic app uses, and the
   exact thing the 2026-08-07 sweep removed app-wide (owner: "looks like
   standard AI generated apps"). Attention now reads the way it reads
   everywhere else here: the whole surface takes the wash, the whole border
   takes the colour, and the corners are swept. */
.vwx-gotcha { margin: 8px 0; padding: 9px 12px; border-radius: var(--r-card);
  border: 1px solid color-mix(in srgb, var(--warn) 45%, var(--line));
  border-top-color: var(--edge-hi-soft);
  background-color: color-mix(in srgb, var(--warn) 10%, transparent);
  font-size: 12px; line-height: 1.5; }

.vwx-blankin { width: 54px; text-align: center; font-family: var(--mono); }

.vwx-summary { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }
.vwx-chip { font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: .07em;
  padding: 2px 7px; border-radius: var(--r-ctl); border: 1px solid var(--line-strong); color: var(--dim); }
.vwx-chip.new { border-color: var(--ok); color: var(--ok); }
.vwx-chip.chg { border-color: var(--ltc-steel); color: var(--ltc-steel); }
.vwx-chip.out { border-color: var(--accent2); color: var(--accent2); }
.vwx-chip.cf { border-color: var(--bad); color: var(--bad); }
.vwx-chip.miss, .vwx-chip.warn { border-color: var(--warn); color: var(--warn); }
.vwx-warn { margin: 0 0 10px; padding-left: 18px; color: var(--warn); font-size: var(--fs-body-s); }

.vwx-entries { display: flex; flex-direction: column; gap: 6px; max-height: 460px; overflow-y: auto; }
/* left-stripe sweep: these diff rows shipped in v0.42.0 — AFTER the 2026-08-07
   sweep — carrying exactly the 2px coloured left edge it had just removed
   everywhere else. Same conversion as .ws-leveldef: `--vec` holds the row's
   status colour, spent as a whole-surface wash plus a tinted full border, so
   the bucket still reads at a glance without a rule down one side. */
.vwx-entry { --vec: var(--line-strong);
  border: 1px solid color-mix(in srgb, var(--vec) 45%, var(--line));
  border-top-color: var(--edge-hi-soft);
  border-radius: var(--r-card);
  background-color: color-mix(in srgb, var(--vec) 8%, var(--panel2));
  padding: 8px 10px; font-size: var(--fs-body-s); }
.vwx-entry.new { --vec: var(--ok); }
.vwx-entry.chg { --vec: var(--ltc-steel); }
.vwx-entry.out { --vec: var(--accent2); }
.vwx-entry.cf { --vec: var(--bad); }
.vwx-entry.miss, .vwx-entry.warn { --vec: var(--warn); }
.vwx-entry.skipped { opacity: .5; }
.vwx-eh { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.vwx-choice { margin-left: auto; display: flex; gap: 10px; color: var(--dim); }
.vwx-choice label { display: flex; align-items: center; gap: 4px; }
.vwx-notes { color: var(--warn); font-size: 12px; margin-top: 4px; display: flex; gap: 12px; flex-wrap: wrap; }
.vwx-changes { margin-top: 6px; display: flex; flex-direction: column; gap: 2px; }
.vwx-ch { display: flex; align-items: baseline; gap: 10px; font-size: 12px; }
.vwx-cf { font-family: var(--mono); font-size: 11px; letter-spacing: .06em; color: var(--faint);
  min-width: 130px; }
.vwx-cv { color: var(--text-2); }
.vwx-ch.watch .vwx-cv { color: var(--accent2); }
.vwx-ch.cf { color: var(--bad); }
.vwx-pick { display: flex; align-items: center; gap: 4px; color: var(--text-2); }
.vwx-actions { display: flex; align-items: center; gap: 10px; margin-top: 12px; font-size: var(--fs-body-s); }

/* ---- application picker + the folding map ---- */
.vwx-approw { display: flex; align-items: flex-end; gap: 16px; flex-wrap: wrap; margin-bottom: 12px; }
.vwx-app { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--dim); }
.vwx-app select { min-width: 190px; }
.vwx-fold { margin-left: auto; }
.vwx-map { margin-top: 16px; border-top: 1px solid var(--line); padding-top: 14px; }
.vwx-nolearn { padding-top: 4px; padding-bottom: 10px; }

/* Column headings — which stack is whose. Sticky so they survive the scroll
   down a long field list. */
.vwx-colh { position: sticky; top: 0; z-index: 2; margin-bottom: 6px; padding: 6px 2px;
  background: var(--panel); font-family: var(--mono); font-size: 11px;
  text-transform: uppercase; letter-spacing: .1em; color: var(--text-2);
  border-bottom: 1px solid var(--line); }

/* The direction chip on a connected row — the answer to "which way does this
   go" without clicking the strand or reading the legend. */
/* One glyph, not a word. The full sentence lives in the title; repeating
   "FROM VECTORWORKS" down thirty rows was clutter (owner 2026-08-08). */
.vwx-dir { font-size: 13px; line-height: 1; cursor: help; flex: 0 0 auto; }
.vwx-dir.d-in { color: var(--ltc-steel); }
.vwx-dir.d-out { color: var(--accent2); }
.vwx-dir.d-both { color: var(--ltc-purple); }
.vwx-dir.d-none { color: var(--faint); }
.vwx-x { background: none; border: 0; color: var(--faint); cursor: pointer; font-size: 12px;
  padding: 0 2px; line-height: 1; }
.vwx-x:hover { color: var(--bad); }

.vwx-edit-h { display: flex; align-items: center; gap: 10px; width: 100%; }
.vwx-edit-h button { margin-left: auto; }
.vwx-owns label.on { color: var(--text); }
.sc-selbar { border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--line));
  border-radius: var(--r-soft); background: var(--panel2); padding: 8px 12px; }
.sc-selbar strong { color: var(--accent); }
/* The Device Schedule's action cell carries a select checkbox as well as the
   duplicate and delete buttons, so it is wider than the shared 56px — set on
   the TABLE, so the header spacer and every row move together and the columns
   stay aligned. Only this grid; the BOM and Racks cells are untouched. */
/* Three controls, not two: the checkbox is 16px and the two buttons are the
   house 25px each, and 82px left them touching each other and the column edge
   (owner 2026-08-13, "some of the items in the left side of each row are having
   trouble fitting"). Measured, not guessed.
   FOUR now — the revision-history clock joined them on 2026-08-17, at the
   body size the Controllers grid already gives it (an emoji at --fs-mini is a
   dot, not a clock). 94 + one 22px control + its 4px gap. */
.sched-table .bom-actions { width: 120px; gap: 4px; }
/* THE ZONE HIERARCHY, INDENTED INTO THE DEVICE ID COLUMN. Set per row as
   --sc-indent and spent inside the first cell only, so the column keeps its
   width and nothing to the right of it shifts. */
.sched-table .bom-row > [data-sck="devid"] { padding-left: var(--sc-indent, 0); }
/* The indent spends the column's width, so what is left has to clip rather than
   run into Device Type. The full identifier stays reachable as the title. */
.sched-table .bom-row > [data-sck="devid"] { overflow: hidden; }
.sc-devid { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
/* The Device Schedule's own row checkbox: sized to the ROW, not to a photo
   tile. 14px sits between the 18px buttons and the 22px fields beside it. */
.bom-actions .sc-pick, .bom-actions #sc-pick-all {
  width: 14px; height: 14px; margin: 0 2px 0 0; cursor: pointer; flex: none;
  accent-color: var(--accent); }
.bom-row:has(.sc-pick:checked) { background: color-mix(in srgb, var(--accent) 10%, transparent); }

.vwx-zero { display: flex; align-items: center; gap: 6px; width: 100%; color: var(--dim); cursor: help; }

/* ---- diff: disclosure + bulk ---- */
.vwx-bulk { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: var(--fs-body-s); }
.vwx-bulk .muted { margin-right: auto; }
.vwx-exp { background: none; border: 0; color: var(--dim); cursor: pointer; font-size: 11px;
  width: 16px; padding: 0; line-height: 1; }
.vwx-exp:disabled { opacity: 0; cursor: default; }
.vwx-exp-sp { display: inline-block; width: 16px; }
.vwx-seedn { font-family: var(--mono); font-size: 11px; }

/* ---- the drawing badge on a Device Schedule row ---- */
.sched-vwx { font-size: 11px; color: var(--ltc-steel); cursor: help; margin-left: 4px; }
.sched-vwx.flagged { color: var(--warn); }

/* ==========================================================================
   MISSION CONSOLE (restyle phase 2a, 2026-08-07)
   --------------------------------------------------------------------------
   Workspace's half of the voice change. The shared half — the global `th`
   rule, the lamps, the focus glow, the button hover scale — lives in
   /style.css, which this file layers on top of; only Workspace's own classes
   are stated here.

   THE REPORT SIDE IS UNTOUCHED, on the same terms as every earlier phase:
   nothing below reaches .rpt-* / .pnl-* / .spec-* / .rk-* / .vc-* or either
   `@media print` block. Their type is paper-correct and must not follow a
   screen treatment. The frozen grids keep every size, padding and height they
   have; where a header cell appears below, ONLY the font face moves.
   ========================================================================== */

/* ---- 1. TELEMETRY LABELS -------------------------------------------------
   The same mono/uppercase/.1em micro-label voice as Operations, on
   Workspace's own chips and captions. */
.ws-ro-tag,
.tab-badge,
.qc-badge,
.zn-cap {
  font-family: var(--mono); text-transform: uppercase; letter-spacing: .1em; }
/* GRID HEADER CELLS — FONT FACE ONLY. These three carry inline pixel widths
   written by app.js from the user's stored column widths, so not one size or
   padding is touched here. Mono is a wider face at the same size, so a long
   header will reach its column edge sooner than it does today; that is the
   accepted cost of the change, not an oversight. The widths themselves are
   draggable, which is the user's existing remedy. */
.dev-head .dev-c,
.bom-head .bom-c,
.cn-subhdr {
  font-family: var(--mono); letter-spacing: .1em; }
/* The device-list group header takes the face and the tracking but NOT
   uppercase: its text is a DATA VALUE — whatever the rows were grouped by, a
   room name or a device category someone typed — and uppercasing user data
   misrepresents it. `overflow: hidden` is already on the element, so the
   wider face ellipsizes rather than spilling into the next column. The Racks
   variant IS uppercase in its own rule further up and stays that way, because
   there the header is a fixed category label. */
.dev-grouphdr { font-family: var(--mono); letter-spacing: .06em; }
/* .ws-crumbs IS DELIBERATELY NOT CONVERTED, for two independent reasons and
   either one alone would be enough:
     · It is not always a breadcrumb. app.js renders a full sentence through
       it on the picker screen ("Pick a project — configure Setup, build zones
       and device types, then review QC before issuing reports."), and that is
       body prose. Mono uppercase would turn a paragraph into shouting.
     · One of its four uses is `<div class="ws-crumbs rpt-toolbar">` on the
       report screen. A rule on .ws-crumbs reaches inside the report zone,
       which this phase does not touch under any circumstances.
   The trail still reads as navigation; it just does it in the body voice. */

/* ---- 2. MONO FIGURES -----------------------------------------------------
   The computed rollup cells are already mono (see `.dev-c.dev-ro` and
   `.bom-c.bom-ro` above); they gain tabular figures so a column of counts and
   subtotals lines up and a value does not re-flow when it recalculates. */
.dev-c.dev-ro, .bom-c.bom-ro, .bom-band .bom-devtotal,
.bom-cathdr .bom-catsub, .bom-total strong {
  font-variant-numeric: tabular-nums; }

/* ---- 3. LAMPS ------------------------------------------------------------
   The level-colour dot squares off with the rest of the indicator language. */
.ld-dot { border-radius: 1px; }
/* NO GLOW ON THIS ONE, and the reason is structural rather than aesthetic:
   `.ld-dot`'s fill arrives as an inline `background` from app.js (the level's
   own colour), and CSS cannot read an element's background-color to build a
   box-shadow from it. A glow would have to be a fixed colour, which on a dot
   whose whole job is to BE a colour would read as a second, wrong one. The
   square corner alone carries the change.
   `.vlan-swatch` is left exactly as it is: it shows a user-chosen cable
   colour, and it already carries the 1px neutral outline that keeps a pale
   colour visible against the panel (verified, not assumed — the rule is
   `border: 1px solid rgba(128,128,128,.55)`). */

/* ---- 4. GLASS ACTIVE STATE — THE SETTINGS-NAV TWIN -----------------------
   THIS BLOCK IS A DELIBERATE DUPLICATE of the one in /style.css. See the twin
   note at `.setup-split` above: the two apps load separate stylesheets and
   their Settings/Setup navs are meant to be the same component, so these two
   rules are byte-identical to their counterparts in /style.css §4 and must
   stay that way. Change one, change both, in the same commit.

   `.active.scope-site` is again absent on purpose — amber means "site-wide"
   and must not be repainted blue. */
.setup-navbtn.active, .setup-navbtn.active.scope-you {
  background-image: linear-gradient(135deg,
    color-mix(in srgb, var(--ltc-blue) 26%, transparent),
    color-mix(in srgb, var(--ltc-navy) 22%, transparent));
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  border-top-color: var(--edge-hi-strong);
  color: var(--text-strong); }
.setup-navbtn.active, .setup-navbtn.active.scope-you {
  background-color: var(--panel2); }
/* No left bar (owner 2026-08-07) — the 3px inset edge was the last left-stripe;
   the glass tint alone is the selected state. Twin rule removed in /style.css
   in the same commit. */
/* Workspace's project tabs need nothing here — `.tabs button.active` is
   defined once, in /style.css, and this app loads that file. */

/* ---- 6. APPLE-TV HOVER (ALLOWLIST ONLY) ----------------------------------
   Two free-floating card grids, checked against the same five conditions as
   the Operations allowlist: no sticky descendant, no scroll container inside,
   nothing absolutely positioned that escapes, no dense-grid row, nothing on
   the frozen list. The transform goes on the CARD and nowhere above it. */
.report-card, .ws-picker .ws-proj {
  transition: transform 160ms ease, border-color 160ms ease,
              box-shadow 160ms ease, background 160ms ease; }
.report-card:hover, .ws-picker .ws-proj:hover {
  transform: scale(1.02);
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  box-shadow: 0 10px 32px rgba(0,0,0,.5); }
/* Reduced motion: the lift goes, the emphasis stays — border and shadow say
   the same thing without moving anything. */
@media (prefers-reduced-motion: reduce) {
  .report-card, .ws-picker .ws-proj { transition: none; }
  .report-card:hover, .ws-picker .ws-proj:hover {
    transform: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent), 0 6px 18px rgba(0,0,0,.45); }
}

/* ==========================================================================
   GRID VOICE — WORKSPACE HALF (owner 2026-08-07, restyle "grid-voice pass")
   --------------------------------------------------------------------------
   The twin of the GRID VOICE block at the foot of /style.css. Read that one
   first: it states the layering rule (background-COLOR for state, -IMAGE for
   wash, never the `background` shorthand) and it defines --grid-lift /
   --grid-wash / --grid-band, which this file inherits because Workspace loads
   /style.css. The tokens are the reason the two apps' grids cannot drift —
   retune them there and both move together.

   COLOUR AND TYPOGRAPHY ONLY. No cell padding, row height, column width or
   sticky offset is touched. That matters more here than in Operations: every
   column width in these grids is an INLINE pixel value written by app.js from
   the user's stored prefs, and the band rows are `width: max-content;
   min-width: 100%` sticky elements whose geometry a stray padding would move.

   TWO THINGS THE BRIEF ASSUMED THAT ARE NOT TRUE OF THIS FILE, both verified
   against app.js rather than guessed:

     • THERE IS NO `.ws-panel > h2`. Every Workspace panel header is a
       `<div class="ws-panel-h">`. The only `<h2>`s in the app are inside
       report windows (`.rpt-page`), which are printed documents and must
       never take a screen voice.
     • THERE ARE NO FROZEN COLUMNS. `.col-frozen` / `.col-frozen-edge` exist
       only in Operations. Workspace holds its left edge by making the BAND
       ROWS sticky (`.dev-band`, `.bom-band`, `.sched-band`, `.bom-cathdr`),
       and a band spans the full grid width — it has no right-hand column
       boundary for an edge fade to mark. So the soft-edge treatment has no
       counterpart here and none is invented; giving a full-width row a
       one-sided shadow would draw a boundary that does not exist.
   ========================================================================== */

/* ---- 1. GRID PANEL HEADERS ----------------------------------------------
   Scoped by what a panel CONTAINS, because `.ws-panel-h` is one component
   shared by eighteen panels and only the grids are in this pass. The `:has()`
   list resolves to exactly nine — Device List, Bill of Materials, Device
   Schedule, Racks & Enclosures, VLANs, Conflicts, Utilization, Address
   Register, Quality Control — and leaves the Setup panels (Project Settings,
   Level Definition, Zones, Reports, Issued reports) on the sans heading they
   share with the rest of Setup. Walked tab by tab in the running app, not
   reasoned about. If `:has()` is ever unavailable the headers simply stay as
   they are, which is the right way for this to fail.

   THE `.muted` HERE IS PROSE, AND IT IS THE OPPOSITE CALL TO OPERATIONS. In a
   budget panel header `.sub` is "144 lines" — meta, four words, and it takes
   the label voice. In a Workspace header the subordinate span is a whole help
   sentence ("— define each device type (A1, A2, B1…) and its specs. Arrow
   keys navigate cells; collapse a category header to hide its columns; drag a
   column edge to resize."). Mono, uppercased and tracked at .14em, that is
   unreadable and about a fifth wider. It is restated to sans at --fs-small:
   still clearly subordinate to an 11px mono label, still a sentence.

   The export buttons inherit `font: inherit` and would take the caps and the
   tracking, so they are restated too — same leak §1 of the Operations block
   patches on `.panel-add`. */
.ws-panel:has(.dev-table, .bom-table, .fc-table, .qc-check, .net-utils) > .ws-panel-h,
.ws-panel.da-panel > .ws-panel-h,
.ws-panel.net-findings > .ws-panel-h {
  padding-bottom: 9px; border-bottom: 1px solid var(--line-soft);
  font-family: var(--mono); font-size: var(--fs-mini); font-weight: 600;
  text-transform: uppercase; letter-spacing: .14em; color: var(--text-2); }
.ws-panel:has(.dev-table, .bom-table, .fc-table, .qc-check, .net-utils) > .ws-panel-h .muted,
.ws-panel.da-panel > .ws-panel-h .muted,
.ws-panel.net-findings > .ws-panel-h .muted {
  font-family: var(--sans); font-size: var(--fs-small); font-weight: 400;
  text-transform: none; letter-spacing: normal; }
.ws-panel:has(.dev-table, .bom-table, .fc-table, .qc-check, .net-utils) > .ws-panel-h .ws-export button,
.ws-panel.net-findings > .ws-panel-h .ws-export button {
  font-family: var(--sans); text-transform: none; letter-spacing: normal; }

/* ---- 2. BAND ROWS -------------------------------------------------------
   Band labels are device codes, categories and zone names — user data — so
   they take the mono face and keep their casing, exactly as the budget group
   rows do. `.bom-devcode` and `.bom-devtotal` were already mono; this brings
   the rest of the row with them instead of leaving three faces in one band.

   `.dev-band` and `.bom-band` gain the neutral wash. Their `background:
   var(--panel2)` shorthand runs EARLIER in this file, so it supplies the
   background-COLOR and the wash lands as the background-IMAGE on top; had
   these rules been written the other way round the shorthand would have
   erased the wash.

   `.sched-band` TAKES THE FACE AND NOTHING ELSE, and this is the one rule in
   the block that would do real damage if copied carelessly. Its
   background-image is ALREADY SPENT — it carries the `--lvc` level ramp, the
   left-to-right tier gradient that says which depth of the zone tree a row
   belongs to, and it is one of only three carriers of that ramp across the
   app. A second wash here would not stack; it would replace the ramp, and the
   Device Schedule would lose its level colours outright. Face only. */
.dev-band, .bom-band, .sched-band, .bom-cathdr, .fc-band { font-family: var(--mono); }
.dev-band, .bom-band, .fc-band {
  background-image: linear-gradient(var(--grid-wash), var(--grid-wash)); }
/* `.bom-cathdr` is a group row too — sticky, with a label, a count and a
   subtotal — but it is drawn as a dashed rule with no fill of its own. It
   takes the face and stays unfilled; a wash would be a new surface, not a
   restyled one. */
/* "＋ Line" is a control, not a label. */
.bom-band .bom-add { font-family: var(--sans); letter-spacing: normal; }

/* ---- 3. SUBTOTALS -------------------------------------------------------
   Workspace has no subtotal ROW: its totals are inline spans inside the
   bands. Both were already mono; they gain tabular figures so a wattage or a
   price does not re-flow as it recalculates. */
.bom-devtotal, .bom-catsub { font-variant-numeric: tabular-nums; }

/* ---- 5. ROW HOVER -------------------------------------------------------
   The same one-step lift as every Operations grid, replacing a flat
   `background: var(--panel2)` shorthand.

   THE SHORTHAND WAS ALSO ERASING THE BOM'S PRIMARY-LINE MARK. `.bom-primary-row`
   is a 7% accent wash at (0,1,0); the hover is (0,3,0) and won outright, so
   pointing at the auto-generated Primary line made the one thing identifying
   it disappear. As a background-IMAGE the lift now stacks over that wash and
   over `.ws-selcell`'s accent fill, so a hovered row keeps saying what it is.
   Rows never scale — these grids are on the transform allowlist's exclusion
   list for the sticky and scroll reasons stated at the foot of this file. */
.dev-row:not(.dev-head):hover, .bom-row:not(.bom-head):hover,
.fc-row:not(.fc-head):not(.fc-grouprow):hover {
  background-image: linear-gradient(var(--grid-lift), var(--grid-lift)); }

/* ---- 7. FILTER CAPTIONS -------------------------------------------------
   The two captions in the Device List filter bar ("Sort", "Group by") join
   the telemetry label set. Their SIZE cannot be reached from here — app.js
   writes `style="font-size:12px"` on each one and an inline style beats any
   stylesheet — so this moves the face, the case, the tracking and the colour
   and leaves the size where the markup pins it. Fixing that properly means
   removing an inline style, which is a JS edit and out of scope. */
.dev-filters label {
  font-family: var(--mono); text-transform: uppercase;
  letter-spacing: .1em; color: var(--dim); }

/* ---- 8. COUNT CAPTIONS --------------------------------------------------
   Mono and tabular, not uppercase — the meta voice, matching the Operations
   half. Same inline-`font-size` caveat as §7.

   ONE `.ws-addbar` SPAN IS NOT A COUNT, and it is excluded by its PANEL
   rather than by itself. The Device List's addbar ends with the hint
   "Category auto-fills from the device type's first letter" — a sentence,
   carried by exactly the same bare `<span class="muted">` as the four real
   counts in the Schedule, Containers and Network addbars, with nothing on the
   element to tell them apart. Left in, it put a mono sentence forty pixels
   below the sans sentence in the panel header: two faces for the same kind of
   prose, which reads as a mistake rather than a decision.

   The panels DO separate cleanly — the hint is the only one inside a
   `.dev-table` panel, every real count is inside a `.bom-table` or
   `.net-utils` one — so the scope is stated there and no JS class is needed.
   The Device List's own count lives in `.dev-filters` and is picked up by the
   first selector, so nothing is lost. */
.dev-filters > span.muted,
.ws-panel:has(.bom-table, .net-utils) .ws-addbar > span.muted,
.bom-count {
  font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* Device Schedule column bands. "From the drawing" gathers everything the
   drafter owns — symbol, layer, class, position, rotation — into one group
   that folds away, because the schedule is already wide and this is reference
   rather than a place anyone works (owner 2026-08-09). Same grammar as the
   Device List's Product Details header. */
.sched-bandhead { border-bottom: 0; padding-bottom: 0; min-height: 0; }
.sched-bandhead .bom-c { padding: 0; }
.sched-bandgap { visibility: hidden; }
.sched-grouphdr { display: inline-flex; align-items: center; gap: 6px; box-sizing: border-box;
  font-size: var(--fs-mini); text-transform: uppercase; letter-spacing: .07em; color: var(--dim);
  padding: 2px 8px; white-space: nowrap;
  border: 1px solid var(--line); border-bottom: 0;
  border-radius: var(--r-card) var(--r-card) 0 0;
  background: color-mix(in srgb, var(--accent) 6%, transparent); }
/* A COLLAPSED BAND MUST BE EXACTLY AS WIDE AS THE COLUMNS IT STANDS IN FOR.
   This was `width: auto`, so a shut group took however many pixels its NAME
   happened to need, while the header row and every data row emitted nothing at
   all for it. The band therefore ran wider than the grid beneath it by the
   length of a label: "FROM THE DRAWING" cost 154px. Nobody had seen it because
   the drawing band is last, so the drift had nothing after it to push out of
   line; collapsing a middle group like Patch has always misaligned everything
   to its right. Found 2026-09-06 when a column somebody ADDED landed after the
   drawing band and sat 154px away from its own heading.
   A fixed width, and a matching placeholder in the header and the rows, which
   is exactly how the Device Types grid has always done it (dev-grp-collapsed). */
.sched-grouphdr.sched-grp-shut { width: 76px; overflow: hidden; }
.sched-grp-collapsed-col { flex: none; width: 76px; box-sizing: border-box;;
  /* A FOLD, NOT A COLUMN. Blank, it read as a column whose heading and resize
     handle had gone missing; the crease says the grid is folded here. */
  background: repeating-linear-gradient(135deg, transparent 0 5px, var(--surface-2) 5px 6px);
  border-left: 1px solid var(--line); border-right: 1px solid var(--line); }
.sched-grpbtn { background: transparent; border: none; color: var(--dim); cursor: pointer;
  font-size: 10px; padding: 0; line-height: 1; }
.sched-grpbtn:hover { color: var(--accent); }
/* These are read-only text cells in fixed-width columns, and the spans they
   sit in do not clip by default — a long symbol name ran straight over the
   next column (2026-08-09). Truncate with an ellipsis; the full value is in
   the title. */
.sched-table .bom-c[data-sck^="d_"] { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* FROZEN IDENTITY COLUMNS (owner 2026-08-09). Scroll far enough right on the
   Device Schedule and every row becomes an anonymous line of numbers — the
   thing that says WHICH fixture you are editing has gone off the left edge.
   The actions cell and Device ID stay put, and so does the zone band's label,
   so a row can always be identified and placed no matter how far across you
   have gone.

   These need an opaque background: sticky cells sit over the scrolling ones,
   and a transparent cell shows the values sliding underneath. */
.sched-table .bom-actions { position: sticky; left: 0; z-index: 4;
  background: var(--surface-1); }
.sched-table .bom-c[data-sck="devid"] { position: sticky; left: 82px; z-index: 4;
  background: var(--surface-1); box-shadow: 1px 0 0 var(--line-soft); }
.sched-table .bom-head .bom-actions,
.sched-table .bom-head .bom-c[data-sck="devid"],
.sched-table .sched-bandhead .bom-actions,
.sched-table .sched-bandhead .bom-c[data-sck="devid"] { background: var(--surface-2); z-index: 5; }
/* The group strip is frozen over the same two columns. Without this the labels
   of groups that have scrolled off keep sliding across the Device ID, which
   reads as the wrong heading over the wrong column. A hidden cell paints no
   background, so the frozen gap is shown and simply left empty. */
.sched-table .sched-bandhead .bom-c[data-sck="devid"] { visibility: visible; }
/* The band label rides the left edge rather than scrolling away with its row —
   "the Zone should also stick... until scrolling into a new Zone". */
.sched-table .sched-band { position: sticky; left: 0; width: max-content; z-index: 3; }

/* A ROW THE WINDOW DID NOT DRAW. It holds its place and its height and nothing
   else, so the scrollbar, the page height and the visual order are the same as
   they would be with every cell in the document. See SCHED_WIN in app.js.

   The height is a MEASURED drawn row, written onto the table as --sc-rowh
   after each paint; the fallback is only for the first frame of the first
   project ever opened. No border: an empty row is not a rule across the page,
   and the drawn rows above and below already carry theirs. */
.sched-table .sc-ghost { height: var(--sc-rowh, 26px); border-bottom: 0; }

/* FROZEN DEVICE TYPE, Device List (owner 2026-08-19: "please keep the Device
   Type pinned to the left as I scroll horizontally"). Same reasoning as the
   Device Schedule above: past Product Details a row is a line of part numbers
   with nothing saying WHICH device type they belong to. Category is left to
   scroll — it repeats down the whole column and the type letter already
   carries it.

   84px is the actions gutter (.dev-actions), the same constant the header and
   the group row use, so all three rows stay in register.

   Device Type is itself drag-resizable, but nothing is frozen to its RIGHT, so
   its width never has to become a custom property the way the Controllers'
   Device ID did.

   The opaque background is what makes it a freeze rather than a smear: sticky
   cells sit over the scrolling ones, and a transparent cell shows the values
   passing underneath. */
.dev-table .dev-actions { position: sticky; left: 0; z-index: 4;
  background: var(--surface-1); }
.dev-table .dev-c.dev-code { position: sticky; left: 84px; z-index: 4;
  background: var(--surface-1); box-shadow: 1px 0 0 var(--line-soft); }
/* The header and the group strip ride above the rows, so they take the header
   surface and sit one layer higher again. */
.dev-table .dev-head .dev-actions,
.dev-table .dev-head .dev-c.dev-code,
.dev-table .dev-grouprow .dev-actions,
.dev-table .dev-grouprow .dev-c.dev-code { background: var(--surface-2); z-index: 5; }
/* AND DESCRIPTION BEHIND IT (owner 2026-08-19): "once the Description hits the
   pinned column, it should be pinned. The rest of the items can then scroll
   past it." Z101 on its own is a code; the description is what tells you what
   the thing IS, and it was the first casualty of scrolling right.

   It is NOT frozen from the start — it sits seventh in Product Details and
   scrolls normally until it reaches Device Type, then stops. That is sticky
   doing exactly what it is for, and it is why the offset has to be the LIVE
   width of Device Type rather than a constant: the same problem the
   Controllers table solved with --ctl-devid-w. The table carries
   --dev-code-w, seeded at render and updated during the drag.

   One layer BELOW Device Type, so if a fractional layout ever laps them by a
   pixel the identity column is the one that wins. */
.dev-table .dev-c[data-colk="description"] { position: sticky;
  left: calc(84px + var(--dev-code-w, 92px)); z-index: 3;
  background: var(--surface-1); box-shadow: 1px 0 0 var(--line-soft); }
.dev-table .dev-head .dev-c[data-colk="description"] { background: var(--surface-2); z-index: 5; }

/* ROW HOVER HAS TO REACH THE FROZEN CELLS. The lift is a background-IMAGE on
   the row (see section 7), and an opaque background-color on a sticky child
   paints straight over it — so the two frozen cells would stay unlit while the
   rest of the row lifted, which reads as the row ending at the freeze. */
.dev-table .dev-row:not(.dev-head):not(.dev-grouprow):hover .dev-actions,
.dev-table .dev-row:not(.dev-head):not(.dev-grouprow):hover .dev-c.dev-code,
.dev-table .dev-row:not(.dev-head):not(.dev-grouprow):hover .dev-c[data-colk="description"] {
  background-image: linear-gradient(var(--grid-lift), var(--grid-lift)); }
/* A grouping band ("Z — Controls") is already pinned left and spans the row;
   it must pass OVER the frozen cells, not under them. */
.dev-table .dev-band { z-index: 6; }

/* A rack face folds shut (owner 2026-08-09). A 45RU rack is two full-height
   boxes and you are usually reading one of them; the other is a screenful of
   grid lines in the way. The whole label bar is the control, so there is no
   small target to find. */
.cnr-face-l { cursor: pointer; user-select: none; }
.cnr-face-l:hover { color: var(--accent); }
.cnr-fold { display: inline-block; color: var(--dim); font-size: 10px;
  transition: transform .12s; transform: rotate(0deg); }
.cnr-fold.open { transform: rotate(90deg); }
.cnr-face-l:hover .cnr-fold { color: var(--accent); }
/* Zone Definition is two panels on one page — the levels, then the option
   lists their zones choose from. A rule between them, because they are
   separate subjects doing one job. */
.ws-zonedef-split { border-top: 1px solid var(--line); margin: 22px 0 4px; }

/* ---- Device List › Digital Assets ----------------------------------------
   The Device List read as documents instead of as rows: one section per device
   type, one card per document slot. Modelled on the LTC Resources grid, which
   is the other place in the suite where the thing you are looking for is a
   file and the picture of it is what you aim at.

   The card is a fixed size on purpose. A repository is scanned vertically, and
   cards that resize themselves around a long filename turn that scan into
   reading. */
/* The view switch: its own row at the top of the panel, above the heading, so
   its position is the panel's padding and nothing else. In the filter row it
   sat at a different height in each view, because the List heading carries a
   help sentence and export buttons and the Digital Assets one does not. */
.dev-viewbar { display: flex; align-items: center; margin: 0 0 10px; }
.ws-devview { flex: 0 0 auto; }
.da-tally-line { margin: 2px 0 10px; font-size: var(--fs-small); }
.da-list { display: flex; flex-direction: column; gap: 12px; }

.da-dev { border: 1px solid var(--line); border-radius: var(--r-card); background: var(--panel2);
  padding: 10px 12px 12px; }
.da-dev-h { display: flex; align-items: baseline; gap: 10px; margin-bottom: 10px; }
.da-code { font-family: var(--mono); font-size: var(--fs-body-s); font-weight: 600; color: var(--text-strong);
  background: var(--surface-3); border-radius: var(--r-ctl); padding: 2px 10px; }
.da-ident { font-size: var(--fs-body-s); color: var(--text-2); }
/* The tally is the answer to "what is still missing", so it sits at the end of
   the header line where the eye lands after the name. */
.da-tally { margin-left: auto; font-family: var(--mono); font-size: var(--fs-small); color: var(--dim); }
.da-tally.da-full { color: var(--ok); }
.da-tally.da-none-yet { color: var(--warn); }

.da-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(228px, 268px));
  gap: 10px; justify-content: start; }
.da-card { display: flex; flex-direction: column; position: relative;
  border: 1px solid var(--line); border-top-color: var(--edge-hi-soft);
  border-radius: var(--r-card); background: var(--panel); }
.da-card.da-empty { background: transparent; border-style: dashed; }
.da-label { font-size: var(--fs-small); font-weight: 600; color: var(--text-2);
  padding: 6px 10px 5px; border-bottom: 1px solid var(--line); }

/* The document itself: type icon, then name and size. The whole row is the
   link, because the icon is what people aim at. */
.da-file { display: flex; align-items: center; gap: 9px; padding: 8px 10px;
  text-decoration: none; color: inherit; min-width: 0; }
a.da-file:hover .da-name { color: var(--accent); }
.da-fmeta { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.da-name { font-size: var(--fs-small); color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.da-sub { font-size: var(--fs-mini); }
.da-note { font-size: var(--fs-mini); color: var(--dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---- the file-type icon --------------------------------------------------
   A drawn page with a folded corner and the extension across it. One inline
   SVG per card: no image requests, and it takes the theme's own colors.
   `--ico` is the family tint; everything else is shared. */
.da-ico { flex: none; width: 34px; height: 42px; }
.da-ico-page { fill: var(--surface-2); stroke: var(--line-strong); stroke-width: 1; }
.da-ico-fold { fill: var(--surface-3); stroke: var(--line-strong); stroke-width: 1; }
.da-ico-band { fill: var(--ico, var(--dim)); }
.da-ico-txt { fill: var(--on-accent); font-family: var(--sans); font-size: 9px;
  font-weight: 700; letter-spacing: .02em; }
.da-ico-pdf   { --ico: var(--ltc-red); }
.da-ico-word  { --ico: var(--ltc-blue); }
.da-ico-excel { --ico: var(--ltc-green); }
.da-ico-ppt   { --ico: var(--ltc-orange); }
.da-ico-img   { --ico: var(--ltc-teal); }
.da-ico-cad   { --ico: var(--ltc-purple); }
.da-ico-zip   { --ico: var(--ltc-steel); }
.da-ico-video { --ico: var(--ltc-red); }
.da-ico-text  { --ico: var(--ltc-navy); }
.da-ico-other { --ico: var(--dim); }
/* The empty slot's placeholder is the same footprint, so a half-filled row of
   cards keeps one baseline. */
.da-ico-empty { display: flex; align-items: center; justify-content: center;
  width: 34px; height: 42px; border: 1px dashed var(--line-strong); border-radius: 3px;
  color: var(--faint); font-size: var(--fs-body); }

.da-acts { display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  padding: 0 10px 8px; margin-top: auto; }
.da-acts .small { font-size: var(--fs-mini); }
.da-hint { font-size: var(--fs-mini); }
/* The file input is driven by the buttons; it is never a control anyone aims
   at, so it is out of the layout rather than merely hidden from view. */
.da-file-input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }

/* Remove is a corner control, not a third pill: at this card width the three
   would wrap onto a second line, which reads as a mistake. It is deliberately
   quiet until the card is under the pointer, because deleting a document is
   never the reason anyone came to this view. */
/* IT LIVES IN THE ACTIONS ROW NOW, beside Download and Replace.
   It used to be a bare ✕ pinned to the card's top-right corner and revealed on
   hover -- quiet, on the reasoning that deleting a document is never why anyone
   came to this view. That corner is now where the "Required" box sits, so the
   two were fighting over the same few pixels and the ✕ read as gone (owner
   2026-08-23: "it looks like we lost the delete button").
   Still the quietest thing in the row: it takes the ghost styling its
   neighbours use and only turns red under the pointer. */
.da-del { margin-left: auto; color: var(--faint); }
.da-del:hover { color: var(--bad); border-color: var(--bad); }

/* Dropping onto a card. The whole card lights up, because the whole card is
   the target. */
.da-card.da-over { border-color: var(--accent); border-style: solid;
  box-shadow: inset 0 0 0 1px var(--accent); }
.da-card.da-busy { opacity: .55; pointer-events: none; }

/* ---- the Digital Assets column band on the List view ---------------------
   The same documents, reachable without leaving the grid: one column per slot,
   holding a download link that names the type. */
.dev-asset { display: inline-flex; align-items: center; gap: 4px; padding: 1px 7px;
  border: 1px solid var(--line); border-radius: var(--r-ctl); text-decoration: none;
  font-family: var(--mono); font-size: var(--fs-mini); color: var(--dim); line-height: 1.6; }
.dev-asset:hover { border-color: var(--accent); color: var(--accent); }
.dev-asset-none { color: var(--faint); font-size: var(--fs-mini); }

.da-more { min-height: 20px; padding: 10px 0 2px; text-align: center; font-size: var(--fs-small); }

/* ---- revision history ----------------------------------------------------
   A drawer rather than a modal: reading what a value used to be is something
   you do WHILE looking at the grid, so the record stays visible behind it. */
.hist-wrap { position: fixed; inset: 0; z-index: 60; background: rgba(0,0,0,.42);
  display: flex; justify-content: flex-end; }
.hist-panel { width: min(520px, 94vw); height: 100%; overflow: auto;
  background: var(--panel); border-left: 1px solid var(--line);
  box-shadow: -12px 0 34px rgba(0,0,0,.35); }
.hist-head { position: sticky; top: 0; z-index: 1; display: flex; align-items: baseline; gap: 8px;
  padding: 13px 16px; background: var(--panel); border-bottom: 1px solid var(--line);
  font-size: var(--fs-body-s); }
.hist-head .muted { font-family: var(--mono); }
.hist-x { margin-left: auto; background: none; border: 0; color: var(--dim);
  font-size: var(--fs-body); cursor: pointer; padding: 0 2px; }
.hist-x:hover { color: var(--text); }
.hist-body { padding: 6px 16px 20px; }

/* ---- one VLAN, address by address ---------------------------------------
   A wide drawer rather than the history's narrow one: the unit here is a row
   of 32 squares plus its address, and squeezing that into 520px would either
   shrink the squares below a hit target or wrap the row, which destroys the
   only thing a grid is for — that column N is always the same last octet. */
.vm-wrap { position: fixed; inset: 0; z-index: 60; background: rgba(0,0,0,.42);
  display: flex; justify-content: flex-end; }
.vm-panel { width: min(760px, 96vw); height: 100%; overflow: auto;
  background: var(--panel); border-left: 1px solid var(--line);
  box-shadow: -12px 0 34px rgba(0,0,0,.35); }
.vm-head { position: sticky; top: 0; z-index: 1; display: flex; align-items: baseline; gap: 8px;
  padding: 13px 16px; background: var(--panel); border-bottom: 1px solid var(--line);
  font-size: var(--fs-body-s); }
.vm-head .muted { font-size: var(--fs-small); }
.vm-x { margin-left: auto; background: none; border: 0; color: var(--dim);
  font-size: var(--fs-body); cursor: pointer; padding: 0 2px; }
.vm-x:hover { color: var(--text); }
.vm-body { padding: 12px 16px 22px; }

.vm-sum { display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center;
  font-size: var(--fs-small); color: var(--text-2); margin-bottom: 12px; }
.vm-sum-i { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
.vm-sum-i b { font-family: var(--mono); color: var(--text-strong); }
.vm-clash { color: var(--ovr); }

/* A line has a hard minimum: two address columns plus 32 squares that must not
   crush below a hit target. Narrower than that, the GRID scrolls rather than
   the page — otherwise the right-hand address is pushed off and each line stops
   saying what it covers, which is the whole point of it being there. */
.vm-grid { display: flex; flex-direction: column; gap: 2px; overflow-x: auto; }
.vm-row, .vm-band { min-width: 620px; }
.vm-row, .vm-band { display: flex; align-items: center; gap: 10px; }
/* Tabular figures and a fixed width, so the address column is a ruler rather
   than a ragged list — that alignment is what lets somebody read down it. */
.vm-addr { font-family: var(--mono); font-size: var(--fs-mini); color: var(--dim);
  font-variant-numeric: tabular-nums; width: 104px; flex: none; }
.vm-addr-r { text-align: right; }
.vm-cells { display: grid; grid-template-columns: repeat(32, 1fr); gap: 2px; flex: 1 1 auto; }
/* 11px is the floor that still reads as a square at a glance and still takes a
   hover. Below that the gaps eat the colour and the grid turns to texture. */
.vm-c { display: block; height: 11px; min-width: 11px; border-radius: 2px;
  background: var(--line); }
.vm-cells .vm-c:hover { outline: 2px solid var(--text-strong); outline-offset: 1px; position: relative; }
.vm-sum-i .vm-c, .vm-band-t .vm-c { width: 11px; flex: none; }

/* The five states, on the app's existing meanings: blue is what is DONE (a
   device is really there), amber is deliberate-but-not-yours, and the gateway
   is the one singular address so it gets the strong ink rather than a hue. */
.vm-used { background: var(--accent); }
.vm-gw { background: var(--text-strong); }
.vm-dhcp { background: color-mix(in srgb, var(--accent) 45%, var(--panel2)); }
.vm-ext { background: var(--ovr); }
.vm-res { background: color-mix(in srgb, var(--ovr) 45%, var(--panel2)); }
.vm-edge { background: color-mix(in srgb, var(--line) 70%, var(--panel)); }
.vm-free { background: var(--line-soft, var(--line)); opacity: .55; }

/* A collapsed run. Same height as a row of squares so the address column down
   the left stays a straight ruler — but deliberately NOT dressed as one.
   A drawn line is 32 addresses wide; a band in the same width may be sixty-five
   thousand. That is a break in the scale, and if it looks like another row the
   reader believes one horizontal inch means the same thing on both. So: inset
   from where the squares begin, and bracketed by the arrows that mean "this
   distance is compressed" on a drawing. */
/* A GAP IS DRAWN AS SPACE, and the space grows with it (--vm-air, set per band
   by vmAir()). It is deliberately NOT a filled pill any more: a pill is an
   object, and an object in a list of rows reads as another row. What is left is
   air, a hairline through it, and a quiet label — the way a break is marked on
   a drawing.
   Its two addresses sit at the TOP and BOTTOM of that space rather than both
   centred, so the vertical distance is visibly bracketed by the range it
   stands for. */
.vm-band { min-height: var(--vm-air, 15px); align-items: stretch; position: relative; }
.vm-band .vm-addr { align-self: flex-start; padding-top: 1px; }
.vm-band .vm-addr-r { align-self: flex-end; padding-top: 0; padding-bottom: 1px; }
.vm-band-t { flex: 1 1 auto; display: flex; align-items: center; justify-content: center;
  gap: 6px; margin: 0 10px; position: relative;
  font-size: var(--fs-mini); font-style: italic; color: var(--dim); }
/* The hairline the label sits on, and the ‹ › that mean a compressed distance
   on a drawing. Drawn behind the text, which masks the middle of it. */
.vm-band-t::before { content: ''; position: absolute; left: 0; right: 0; top: 50%;
  border-top: 1px dashed var(--line); }
.vm-band-t > * { position: relative; z-index: 1; }
/* The label carries the panel colour so the hairline does not run through the
   words. `.vm-c` inside it keeps its own swatch fill. */
.vm-band-lbl { position: relative; z-index: 1; padding: 0 8px; background: var(--panel);
  white-space: nowrap; display: inline-flex; align-items: center; gap: 6px; }
.vm-b-dhcp .vm-band-t, .vm-b-ext .vm-band-t, .vm-b-res .vm-band-t { color: var(--text-2); }

.hist-entry { padding: 10px 0; border-bottom: 1px solid var(--line-soft); }
.hist-meta { display: flex; gap: 8px; align-items: baseline; margin-bottom: 5px; }
.hist-when { font-family: var(--mono); font-size: var(--fs-mini); color: var(--dim); }
.hist-who { font-size: var(--fs-small); color: var(--text-2); }
/* Who was really at the keyboard, when the edit was made under a "Viewing as"
   preview. Quieter than the Person it ran as, because that is still the answer
   to "whose account changed this"; this is the footnote. */
.hist-really { font-size: var(--fs-small); color: var(--dim); }
/* One line per field: what it is, what it was, what it became. The arrow is
   the only ornament, because the two values are the content. */
.hist-field { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap;
  padding: 3px 0; font-size: var(--fs-small); }
.hist-key { min-width: 120px; color: var(--text-2); }
.hist-from { color: var(--dim); text-decoration: line-through; text-decoration-color: var(--line-strong); }
.hist-arrow { color: var(--faint); }
.hist-to { color: var(--text); }
.hist-empty { color: var(--faint); font-style: italic; text-decoration: none; }
.hist-undo { margin-left: auto; font-size: var(--fs-mini); }
/* The history control sits with duplicate and delete, and reads as the quiet
   one of the three: it changes nothing by itself. */
.dev-hist { background: none; border: 0; color: var(--dim); cursor: pointer;
  font-size: var(--fs-small); padding: 0 2px; }
.dev-hist:hover { color: var(--accent); }

/* ---- Work Notes: the project punch list -----------------------------------
   A list of what is left, not a spreadsheet: each row is one job, readable at a
   glance, and the whole row opens it. Columns are fixed so the eye can run down
   priority and status without re-finding them on every line. */
/* The grid SCROLLS rather than spilling. Eight of the nine columns are fixed
   pixels, so the row has a hard minimum (~1126px with the default widths) and
   cannot shrink past it; without this it ran off the panel and clipped the
   Status column on a narrower window. Same treatment as .dev-table, .bom-table
   and .fc-table, including the room left below for the scrollbar. */
.wn-list { display: flex; flex-direction: column; overflow-x: auto; padding-bottom: 6px; }
/* One template, set on the LIST as a custom property and inherited by the
   header and every row, so a drag moves all of them together and nothing can
   drift out of alignment. Same mechanism as the rack-contents grid. */
.wn-row { display: grid; align-items: center; gap: 10px;
  grid-template-columns: var(--wn-cols, 62px 76px minmax(160px, 1fr) 190px 116px 170px 92px 44px 116px);
  padding: 8px 10px; border-bottom: 1px solid var(--line-soft);
  cursor: pointer; border-radius: var(--r-soft);
  min-width: var(--wn-min, 1126px); }
/* Every cell clips rather than pushing its neighbours: a column is exactly as
   wide as the reader made it. */
.wn-row > * { min-width: 0; overflow: hidden; }
.wn-row:hover { background: var(--surface-2); }
.wn-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.wn-head { cursor: default; font-family: var(--mono); font-size: var(--fs-mini);
  text-transform: uppercase; letter-spacing: .1em; color: var(--dim);
  border-bottom: 1px solid var(--line); }
.wn-head:hover { background: none; }
.wn-h { position: relative; overflow: visible; }
/* The grip sits ON the column edge and is wider than it looks, because a 1px
   target is a target nobody hits. */
.wn-grip { position: absolute; top: -6px; right: -8px; width: 12px; bottom: -6px;
  cursor: col-resize; }
.wn-grip:hover { background: linear-gradient(to right, transparent 4px, var(--accent) 4px 6px, transparent 6px); }
/* A closed note stays in the list when you ask for it, and reads as done. */
.wn-done { opacity: .58; }
.wn-seq { font-family: var(--mono); font-size: var(--fs-small); color: var(--dim); }
.wn-title { font-size: var(--fs-body-s); color: var(--text); min-width: 0; }
.wn-target { display: block; font-size: var(--fs-mini); color: var(--dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The thing this note was raised against has since been deleted. The note is
   still true, so it stays and says so rather than quietly losing its subject. */
.wn-target-gone { color: var(--warn); font-style: italic; }
/* "Linked to" is its own column (owner 2026-08-11) and its cell is a control,
   because the link is meant to be followed. Two lines: what the thing is
   called, and what KIND of thing it is, because "RK-003" alone does not say
   whether you are looking at a rack or a placed device. */
.wn-linkcell { min-width: 0; }
.wn-link { display: block; width: 100%; text-align: left; background: none;
  border: 0; padding: 2px 6px; margin: -2px -6px; border-radius: var(--r-ctl);
  font: inherit; font-size: var(--fs-small); color: var(--accent);
  cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wn-link:hover { background: var(--surface-2); text-decoration: underline; }
.wn-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.wn-link-kind { display: block; font-size: var(--fs-mini); color: var(--dim);
  text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The card a link opens. Fixed to the viewport and positioned in script, so it
   is never clipped by the row's own overflow rule. */
.wn-card { position: fixed; z-index: 60; width: 300px; max-width: calc(100vw - 16px);
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-soft); box-shadow: 0 12px 28px rgba(0, 0, 0, .38);
  padding: 12px 14px; }
.wn-card-k { font-size: var(--fs-mini); text-transform: uppercase; letter-spacing: .06em;
  color: var(--dim); }
.wn-card-h { font-size: var(--fs-body); color: var(--text); font-weight: 600; margin: 2px 0 8px;
  overflow-wrap: anywhere; }
.wn-card-r { display: flex; justify-content: space-between; gap: 12px;
  font-size: var(--fs-small); padding: 3px 0; border-top: 1px solid var(--line-soft); }
.wn-card-r span { color: var(--dim); white-space: nowrap; }
.wn-card-r b { color: var(--text-2); font-weight: 500; text-align: right; overflow-wrap: anywhere; }
.wn-card-gone { font-size: var(--fs-small); color: var(--dim); margin: 0; }
.wn-card .wn-card-go { margin-top: 10px; width: 100%; }
.wn-people { display: flex; flex-wrap: wrap; gap: 4px; min-width: 0; }
.wn-who { font-size: var(--fs-mini); color: var(--text-2);
  background: var(--surface-2); border-radius: var(--r-ctl); padding: 1px 8px; }
.wn-nobody { color: var(--faint); background: transparent; border: 1px dashed var(--line); }
.wn-by { font-size: var(--fs-small); color: var(--dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wn-when { font-size: var(--fs-small); }
/* A band per status (or priority, or person). Same grammar as the Device
   List's group bands, so the two grids fold the same way. */
/* A band spans the whole grid, so it takes the same floor as a row — otherwise
   it stops at the viewport edge and the list looks torn when scrolled right. */
.wn-band { display: flex; align-items: baseline; gap: 8px; margin: 12px 0 2px;
  padding: 5px 10px; background: var(--panel2); border-top: 1px solid var(--line);
  border-radius: 6px; font-family: var(--mono); font-size: var(--fs-small);
  min-width: var(--wn-min, 1126px); }
.wn-band .muted { font-size: var(--fs-mini); }
.wn-due { color: var(--dim); font-family: var(--mono); font-size: var(--fs-mini); }
.wn-late { color: var(--bad); font-weight: 600; }
.wn-today { color: var(--warn); }
.wn-files { font-size: var(--fs-mini); color: var(--dim); text-align: right; }

/* Priority and status are the two things scanned rather than read, so both are
   a chip. Priority earns colour only when it is above the ordinary. */
.wn-pri { font-size: var(--fs-mini); border-radius: var(--r-ctl); padding: 1px 9px;
  border: 1px solid var(--line); color: var(--dim); text-align: center; }
.wn-pri-normal { color: var(--dim); }
.wn-pri-low { color: var(--faint); }
.wn-pri-high { color: var(--accent2); border-color: var(--accent2); }
.wn-pri-urgent { color: var(--on-accent); background: var(--bad); border-color: var(--bad); font-weight: 600; }
.wn-status { font-size: var(--fs-mini); border-radius: var(--r-ctl); padding: 1px 9px;
  border: 1px solid var(--line); color: var(--dim); text-align: center; }
.wn-st-open { color: var(--warn); border-color: var(--warn); }
.wn-st-assigned { color: var(--accent); border-color: var(--accent); }
.wn-st-in_progress { color: var(--on-accent); background: var(--accent); border-color: var(--accent); }
.wn-st-awaiting_review { color: var(--accent2); border-color: var(--accent2); }
.wn-st-complete { color: var(--ok); border-color: var(--ok); }
.wn-st-cancelled { color: var(--faint); }

/* ---- one work note, in full ----------------------------------------------- */
/* CENTRED, not a right-hand drawer (owner 2026-08-11: "the pop-up opens way to
   the right"). Revision history is something you read beside the grid, so it
   stays a drawer; a work note is the thing you are working on, so it takes the
   middle of the screen like every other dialog in the suite. */
.wn-wrap { justify-content: center; align-items: center; padding: 24px; }
.wn-detail { width: min(660px, 96vw); height: auto; max-height: 90vh;
  border: 1px solid var(--line); border-left: 1px solid var(--line);
  border-radius: var(--r-card); box-shadow: 0 18px 48px rgba(0,0,0,.45); }
.wn-detail.wn-over { outline: 2px dashed var(--accent); outline-offset: -6px; }
/* Air between the groups of fields, in the order the owner set: task and notes,
   then what it is linked to, then who and when, then the status ladder, then
   the attachments. */
.wn-sep { border-top: 1px solid var(--line-soft); margin: 4px 0 2px; }
.wn-body { display: flex; flex-direction: column; gap: 10px; }
.wn-f-title { font-size: var(--fs-lead); }
/* `flex: none` and a floor: the panel is a flex column with a max height, and
   without these the notes box is squeezed to a strip the moment the note grows
   enough to scroll. */
.wn-f-body { resize: vertical; font: inherit; font-size: var(--fs-body-s);
  flex: none; min-height: 74px; }
.wn-f-title { flex: none; }
.wn-grid { display: grid; grid-template-columns: 108px 1fr; gap: 8px 10px; align-items: center; }
.wn-lbl { font-size: var(--fs-small); color: var(--dim); }
.wn-linkrow { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.wn-link-type { flex: 0 0 auto; }
.wn-link-id { flex: 1 1 190px; min-width: 0; }
/* Priority and Due share one line: they are both "how urgent is this", and two
   full rows for two short controls left the form loose. */
.wn-sel-pri, .wn-date { justify-self: start; }

/* ---- assignees as buttons -------------------------------------------------
   A row of names you click, not a column of checkboxes. Several assignees means
   ANY ONE of them may do it and close it; a job that needs all of them is a
   task, and lives in Operations. */
.wn-picker { display: flex; flex-direction: column; gap: 6px; }
/* Only shown when nobody is picked. With somebody picked, the lit-up button
   already says who — repeating it above was the same name twice in a row. */
.wn-chosen { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.wn-chips { display: flex; flex-wrap: wrap; gap: 5px; }
.wn-chip { font-size: var(--fs-small); padding: 3px 11px; border-radius: var(--r-ctl);
  border: 1px solid var(--line); background: transparent; color: var(--dim); cursor: pointer; }
.wn-chip:hover { border-color: var(--accent); color: var(--accent); }
.wn-chosen .wn-chip { cursor: default; }
.wn-chip.on { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 600; }
.wn-chip[disabled] { cursor: default; opacity: .6; }
.wn-pq { font-size: var(--fs-small); }
.wn-anyone { font-size: var(--fs-mini); }

/* ---- the New work note dialog --------------------------------------------
   Always a dialog, even with nothing typed, the same as ＋ Trip on a budget. */
.wn-new-box { width: min(520px, 94vw); text-align: left; }
.wn-new-h { font-size: var(--fs-lead); margin: 0 0 12px; }
.wn-new-h .sub { font-weight: 400; color: var(--dim); font-size: var(--fs-body-s); }
.wn-new-l { display: block; font-family: var(--mono); font-size: var(--fs-mini);
  text-transform: uppercase; letter-spacing: .1em; color: var(--dim); margin: 10px 0 4px; }
.wn-req { color: var(--bad); }
.wn-new-box input[type="text"], .wn-new-box input:not([type]), .wn-new-title,
.wn-new-box textarea, .wn-new-box select { width: 100%; box-sizing: border-box; }
.wn-new-box textarea { resize: vertical; font: inherit; font-size: var(--fs-body-s); }
.wn-new-two { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.wn-new-two input, .wn-new-two select { width: 100%; box-sizing: border-box; }
.wn-new-err { color: var(--bad); font-size: var(--fs-small); min-height: 18px; margin-top: 8px; }
/* The dialog now carries the whole note — status and photos included — so it
   scrolls rather than growing past the screen. */
.wn-new-box { max-height: 88vh; overflow: auto; }
.wn-new-box.wn-over { outline: 2px dashed var(--accent); outline-offset: -6px; }
.wn-new-status { border: 0; padding: 0; }
.wn-new-files { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* A file chosen but not yet uploaded: it belongs to a note that does not exist
   until Save, so it is shown as intent rather than as a stored attachment. */
.wn-staged .da-name { color: var(--text-2); }
.wn-staged .da-del { position: static; opacity: 1; }
.wn-assignees { display: flex; flex-wrap: wrap; gap: 4px 12px; }
.wn-chk { display: inline-flex; align-items: center; gap: 5px; font-size: var(--fs-small); }
/* The status ladder as buttons, in the order the work travels. Closing states
   are disabled rather than hidden for people who may not use them, so the rule
   is visible instead of mysterious. */
.wn-statusrow { display: flex; flex-wrap: wrap; gap: 6px; padding: 4px 0 2px;
  border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); }
.wn-files-h { display: flex; align-items: center; gap: 8px; font-size: var(--fs-small);
  font-weight: 600; color: var(--text-2); margin-top: 2px; }
.wn-hint { font-size: var(--fs-mini); font-weight: 400; }
.wn-file { display: flex; align-items: center; gap: 9px; padding: 6px 0;
  border-bottom: 1px solid var(--line-soft); }
.wn-file .da-fmeta { flex: 1; min-width: 0; }
/* A fixed BOX with the media contained inside it. Portrait and landscape both
   fit whole — neither is cropped, and neither stretches the row (owner
   2026-08-11: "photos and videos may be uploaded in landscape or portrait
   orientation, allow for both"). A photo's own EXIF rotation is applied by the
   browser; a video carries its rotation in the file. */
.wn-thumb { flex: none; width: 62px; height: 62px; object-fit: contain;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: 3px; }
.wn-file .da-name { text-decoration: none; }
.wn-file .da-name:hover { color: var(--accent); }
/* In a list the remove control is always there: there is no card to hover. */
.wn-file .da-del { position: static; opacity: 1; }
.wn-nofiles { font-size: var(--fs-small); padding: 6px 0; }
.wn-foot { font-size: var(--fs-mini); margin: 6px 0 0; }
.wn-danger { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  border-top: 1px solid var(--line-soft); padding-top: 10px; margin-top: 4px; }

/* ---- Cheat Sheets (owner 2026-08-11) --------------------------------------
   The deck a project hands to EVA. Two halves on one page: the editor on the
   left, and on the right a preview drawn from exactly the data the phone will
   receive, so what the author approves is what the crew reads. */
.cs-tabs { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 12px 0 14px; }
.cs-tab { display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px;
  border: 1px solid var(--line); border-radius: var(--r-ctl); background: var(--panel2);
  color: var(--text-2); font-size: var(--fs-body-s); cursor: pointer; }
.cs-tab:hover { border-color: var(--accent); }
.cs-tab.on { border-color: var(--accent); color: var(--text); background: var(--surface-2); }
.cs-off { font-size: var(--fs-mini); color: var(--faint); text-transform: uppercase; letter-spacing: .06em; }
/* A card that never prints. A dot rather than a word: the row already carries a
   name, and the tab is not where the rule gets explained. */
.cs-lock { color: var(--warn); font-size: 9px; }
.cs-split { display: grid; grid-template-columns: minmax(0, 1fr) 374px; gap: 22px; align-items: start; }
@media (max-width: 1100px) { .cs-split { grid-template-columns: minmax(0, 1fr); } }
.cs-chk { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-body-s); color: var(--text-2); }
.cs-block { border: 1px solid var(--line-soft); border-radius: var(--r-soft); padding: 10px 12px; margin: 10px 0; }
.cs-block-h { display: flex; align-items: center; justify-content: space-between;
  font-size: var(--fs-mini); text-transform: uppercase; letter-spacing: .06em; color: var(--dim); margin-bottom: 8px; }
/* A row is one big field and one small field, and the editor says so by giving
   the lead the wider column. */
.cs-rowline { display: grid; grid-template-columns: auto minmax(0, 1fr) minmax(0, 1.3fr) auto auto auto;
  gap: 6px; margin-bottom: 6px; align-items: center; }
/* The GRIP is draggable, not the row — the row is two text inputs, and making
   those draggable takes selecting a word away from the author. */
.cs-grip { cursor: grab; color: var(--faint); font-size: 13px; line-height: 1; padding: 0 2px;
  user-select: none; }
.cs-grip:active { cursor: grabbing; }
.cs-tab[draggable="true"] { cursor: grab; }
.cs-dragging { opacity: .4; }
/* Where it will land. A line, not a fill: the row keeps its own shape so the
   author is looking at the same thing they picked up. */
.cs-dropto { box-shadow: 0 -2px 0 0 var(--accent); }
.cs-tab.cs-dropto { box-shadow: -2px 0 0 0 var(--accent); }
.cs-block input, .cs-block textarea { width: 100%; }
/* A sourced block has no fields to type in, so it reads as a setting and a
   statement rather than a form. The select sizes to its content instead of
   filling the row like the typed inputs above. */
.cs-src { display: flex; flex-direction: column; gap: 7px; }
.cs-src-l { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font-size: var(--fs-body-s); color: var(--text-2); }
.cs-src-l select { width: auto; min-width: 150px; }
.cs-live { margin: 0; font-size: var(--fs-small); color: var(--text-2); }

/* The Image block's drop box. A DASHED outline, which is the one place in this
   app an outline is the right answer: it says "put something here", and a
   filled surface with no border would read as an empty panel somebody forgot
   to finish. */
.cs-img { position: relative; border: 1px dashed var(--line-strong, var(--line));
  border-radius: var(--r-soft); padding: 10px; cursor: pointer;
  background: var(--surface-2, var(--panel2)); transition: border-color .12s, background .12s; }
.cs-img:hover { border-color: var(--accent); }
.cs-img-over { border-color: var(--accent); border-style: solid;
  background: color-mix(in srgb, var(--accent) 10%, var(--panel2)); }
.cs-img-busy { opacity: .55; pointer-events: none; }
.cs-img-drop { display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 16px 8px; text-align: center; font-size: var(--fs-body-s); }
.cs-img-drop .muted { font-size: var(--fs-small); }
/* Bounded, because the point of the thumbnail is to confirm WHICH picture,
   not to preview it — the card beside it already does that. */
.cs-img-shot { display: block; max-width: 100%; max-height: 160px; margin: 0 auto;
  object-fit: contain; border-radius: var(--r-soft); }
.cs-img-meta { display: flex; align-items: center; gap: 8px; margin-top: 8px;
  font-size: var(--fs-small); color: var(--text-2); }
.cs-img-meta span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cs-img-meta button { margin-left: auto; flex: none; }
/* The tag says where the content comes from, which the design asks to be
   visible at the point the decision is made rather than in a tooltip. */
.cs-live-tag { display: inline-block; margin-right: 6px; padding: 1px 7px;
  border-radius: var(--r-ctl); background: var(--surface-2); border: 1px solid var(--line-soft);
  color: var(--accent); font-size: var(--fs-mini); font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; }
.cs-lines { font-family: var(--mono); font-size: var(--fs-small); }
.cs-addblock { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
.cs-side-h { display: flex; align-items: center; justify-content: space-between; gap: 8px;
  font-size: var(--fs-mini); text-transform: uppercase; letter-spacing: .06em;
  color: var(--dim); margin: 0 0 8px; }
.cs-side-h + .ws-hint { margin-top: -4px; }
.cs-palrow { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin: 6px 0; }
.cs-palrow > .tk-fld-l { width: 44px; flex: none; }
.cs-stop { display: inline-flex; align-items: center; }
.cs-stop-x { background: none; border: 0; color: var(--faint); cursor: pointer; padding: 0 2px; }
.cs-stop-x:hover { color: var(--bad); }
.cs-pal { width: 34px; height: 26px; padding: 0; border: 1px solid var(--line); border-radius: var(--r-ctl);
  background: none; cursor: pointer; }

/* The card itself, at the size EVA draws it: 374 x 702, fixed. A point is a
   pixel here, so every number matches EVA/docs/CHEAT-SHEETS.md verbatim and
   CS_GEO in app.js. No app token appears inside .cs-card — the download is
   rendered from these same values with no stylesheet to resolve one against.

   FIXED HEIGHT IS THE POINT. A card with two rows is as tall as one with
   twelve, because the author's real question is how much room is left. */
.cs-card { position: relative; width: 374px; height: 702px; border-radius: 18px;
  font-family: inherit; flex: none; overflow: hidden; }
.cs-panel { position: absolute; inset: 12px; border-radius: 14px; padding: 18px;
  display: flex; flex-direction: column; gap: 12px; overflow: hidden; }
.cs-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.cs-title { font-size: 22px; font-weight: 800; letter-spacing: 1.2px; line-height: 1.1; }
.cs-proj { margin-top: 5px; display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
.cs-proj-c { font-size: 10px; font-weight: 700; font-family: var(--mono); }
.cs-proj-n { font-size: 10px; }
.cs-sens { font-size: 10px; font-weight: 600; white-space: nowrap; }
/* The body takes the slack, so the foot is pinned to the bottom of the panel
   whatever the card holds. Clipped, not scrolled: an author who cannot see the
   bottom of their own card has been told something true. */
.cs-body { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
  gap: 12px; overflow: hidden; }
.cs-rows { display: flex; flex-direction: column; gap: 7px; }
/* The SWEPT cut — `--r-card`, TL TR BR BL — not a capsule (owner 2026-08-13).
   A row is a small surface, and the suite's shape law gives a surface the
   corner treatment from LTC's printed material. The literal is written out
   rather than using the token because this preview must draw exactly what the
   phone draws, and the phone does not read this stylesheet: the numbers are
   CS_GEO.row.cut, kept identical by a test. */
.cs-row { border-radius: 2px 16px 2px 16px; padding: 7px 14px; display: flex; flex-direction: column;
  gap: 1px; opacity: .9; }
.cs-lead { font-size: 14px; font-weight: 600; }
.cs-sub { font-size: 11px; }
.cs-legend { border-radius: 10px; padding: 11px; background: rgba(255, 255, 255, .07); }
.cs-legend-h { font-size: 11px; font-weight: 800; letter-spacing: 1.1px; margin-bottom: 6px; }
.cs-legend-l { font-size: 12px; font-family: var(--mono); }
.cs-legend-l + .cs-legend-l { margin-top: 6px; }
/* A WiFi block: the name and passphrase to read, then the code to scan.
   The plate is WHITE and stays white whatever the project's palette is — a
   scanner needs dark modules on a light field, and this card's panel is
   near-black. `crispEdges` on the svg keeps a module from being drawn as two
   grey half-pixels at fractional scales, which is what a phone camera sees as
   a smudge. */
.cs-wifi { display: flex; flex-direction: column; gap: 2px; align-items: stretch; }
.cs-wf-ssid { font-size: 15px; font-weight: 700; }
.cs-wf-pass { font-size: 13px; font-weight: 500; font-family: var(--mono);
  word-break: break-all; }
.cs-wf-plate { margin-top: 6px; align-self: center; background: #fff;
  border-radius: 2px 16px 2px 16px; padding: 12px; line-height: 0; }
.cs-qr { display: block; width: 190px; height: 190px; max-width: 100%; }
.cs-wf-cap { font-size: 10px; margin-top: 6px; }
/* A picture on the card: FITTED, never cropped. A patch diagram with its edge
   cut off is worse than a small one. */
.cs-shot { display: block; max-width: 100%; max-height: 260px; margin: 0 auto;
  object-fit: contain; border-radius: 2px 16px 2px 16px; }
.cs-note { font-size: 11px; margin: 0; }
.cs-unknown { font-size: 11px; opacity: .6; font-style: italic; }
/* LTC's mark sits at the FOOT and never the head. Not a layout preference: in
   the header it puts our branding above the client's on their own paperwork
   (print-doc.js, and the same rule applies at 3 inches). The icon, not the
   wordmark, whose second line sets at 1.8pt down here. */
.cs-foot { margin-top: auto; display: flex; align-items: center; gap: 8px;
  font-size: 9px; padding-top: 9px; border-top: 1px solid transparent; }
.cs-date { margin-left: auto; }
.cs-ltc { width: 16px; height: 16px; border-radius: 3px; object-fit: contain; }
/* The preview column is the width of the card plus its own breathing room, so
   the card is never scaled and what the author measures is what ships. */
.cs-side { width: 374px; }
.cs-overflow { font-size: var(--fs-body-s); color: var(--warn); margin: 8px 0 0; }

/* ---- Controllers --------------------------------------------------------- */
/* A controller is a Device Schedule row, so this reads as a card ABOUT a row
   rather than as another grid: the identity comes first, then what is true of
   it, then what it drives. */
/* THREE LEVELS, THREE SURFACES (owner 2026-08-13: "the black and gray is a bit
   confusing... we're losing sight of the items we just discussed being WITHIN
   another slightly larger card"). The tab's panel is --surface-1, one
   controller is --surface-2 sitting on it, and a section inside that controller
   is --surface-3. They were two tones doing three jobs, so a section read as a
   sibling of the card holding it rather than as something inside it. */
.ctl-card { border: 1px solid var(--line); border-radius: var(--r-card);
  background: var(--surface-2); padding: 12px 14px; margin: 14px 0; }
.ctl-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
/* The spacer only spaces if it grows. It never had a rule that reached it — the
   two `.grow` rules in this file are scoped to other components — so the
   revision-history button sat wherever the identity text happened to end, which
   is what the owner was looking at when he asked for it on the right
   (2026-08-13). */
.ctl-head .grow { flex: 1; }
.ctl-did { font-family: var(--mono); font-weight: 700; color: var(--accent); }
.ctl-type { font-size: var(--fs-small); color: var(--text-2); }
.ctl-where { font-size: var(--fs-small); color: var(--dim); }
.ctl-nics { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0 0; }
.ctl-nic { font-family: var(--mono); font-size: var(--fs-mini); padding: 2px 8px;
  border-radius: var(--r-ctl); background: var(--surface-2); border: 1px solid var(--line-soft); }
.ctl-up { margin: 8px 0 0; font-size: var(--fs-small); color: var(--text-2); }
/* SECTIONS INSIDE A CONTROLLER CARD (owner 2026-08-13: "this is a bit messy...
   maybe we need cards inside each controller's card"). Seven subjects, each
   boxed, so the eye can skip the six it is not asking about. They flow into as
   many columns as the width allows; Input and Output carry lists and a
   relationship each, so they claim the full width when there is not room for
   two of them side by side. */
/* TWO COLUMNS, SPLIT IN THE CENTRE, and the owner's order (2026-08-13):
     General | Access & Startup     a pair, heights matched
     Software & OS | Show Files     a pair — show files are their own subject
     Network                        full width, so the NIC fields can breathe
     Input | Output                 a pair, rows aligned (see .ctl-io)
     Notes                          full width
   Heights match because grid stretches items in a row to the tallest, which is
   what "as one grows, the other matches it" asks for. */
.ctl-secs { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px; }
.ctl-sec { border: 1px solid var(--line-soft); border-radius: var(--r-soft);
  padding: 9px 11px 11px; background: var(--surface-3); min-width: 0; }
.ctl-sec-full { grid-column: 1 / -1; }
/* INPUT AND OUTPUT, ROW BY ROW (owner 2026-08-13: "the start of Control Inputs
   and the start of Control Outputs should align. Similarly, Driven By and
   Control Scope should align. This means adding inputs or outputs might push
   both Driven by and Control Scope down.")
   Matching the two boxes' overall HEIGHTS is not that: two boxes can be equally
   tall with everything inside them at different heights. What aligns rows
   across two independent boxes is a shared row template, which is subgrid —
   the pair declares three rows (heading, list, relationship) and each section
   maps its three children onto them. Row 2 takes the height of the taller list,
   so a fourth output pushes BOTH relationships down together. */
.ctl-io { grid-column: 1 / -1; display: grid; grid-template-columns: 1fr 1fr;
  grid-template-rows: auto auto auto; gap: 10px; }
.ctl-sec-io { display: grid; grid-row: span 3; grid-template-rows: subgrid; align-content: start; }
/* One column under about a laptop's width: two halves of a half are unusable.
   The pair stops being a pair, so the subgrid stops too — a single column has
   nothing to align with. */
@media (max-width: 900px) {
  .ctl-secs { grid-template-columns: 1fr; }
  .ctl-sec-full { grid-column: auto; }
  .ctl-io { grid-template-columns: 1fr; grid-template-rows: none; }
  .ctl-sec-io { display: block; grid-row: auto; }
}
.ctl-sech { margin: 0 0 8px; font-size: var(--fs-mini); text-transform: uppercase;
  letter-spacing: .07em; color: var(--dim); font-weight: 600;
  display: flex; align-items: center; gap: 6px; }
.ctl-secicon { display: inline-flex; align-items: center; line-height: 1; }
/* ONE LINE OF THE OWNER'S LAYOUT. The card is read line by line rather than as
   a bag of boxes that wraps wherever it runs out of room (owner 2026-08-13:
   "Description should be BOLD. Then, Role / Next line: Location / Next line:
   Manufacturer, Product, Variation, Serial Number"). */
.ctl-line { display: flex; flex-wrap: wrap; gap: 8px 10px; margin-bottom: 8px; }
.ctl-line:last-child { margin-bottom: 0; }
.ctl-fnf-cap { font-size: 9px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--dim); display: block; margin-bottom: 1px; }
.ctl-fnfield { display: inline-flex; flex-direction: column; }
.ctl-ro { font-size: var(--fs-body-s); color: var(--text-2); padding: 6px 0; }

.ctl-groups { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 12px; margin-top: 12px; }
.ctl-group { border-top: 1px solid var(--line-soft); padding-top: 8px; }
.ctl-glabel { font-size: var(--fs-mini); text-transform: uppercase; letter-spacing: .06em;
  color: var(--dim); margin-bottom: 6px; }
.ctl-gfields { display: flex; flex-wrap: wrap; gap: 8px; }
.ctl-ghint { margin: 6px 0 0; font-size: var(--fs-mini); color: var(--faint); }
.ctl-fld { display: inline-flex; flex-direction: column; gap: 2px; flex: none; }
.ctl-cap { font-size: 9px; text-transform: uppercase; letter-spacing: .06em; color: var(--dim); }
/* ---- Verification Status report (owner 2026-08-21) ----------------------
   Ten columns, and the two that carry a closed vocabulary are the ones that
   look worst when they wrap: "Not focused" breaking after "Not" reads as two
   facts. Both are short and known, so they hold one line and the columns that
   carry real prose give up the width instead. */
.rpt-page .vsr-st, .rpt-page .vsr-did { white-space: nowrap; }
/* The status heading is the document's spine — the reader is looking for one
   of five words and then stops. Its count sits beside it rather than under. */
.rpt-page h2.vsr-status { margin-top: 20px; }
/* padding-top, NOT margin-top. The zone panel has no top border or padding of
   its own, so a margin here COLLAPSES THROUGH the panel's edge: the panels
   printed 10px apart while the paginator, which measures a block's rect plus
   its own margins, saw them touching — and the last zone of page 1 ran 25px
   past the foot of the sheet with nothing saying so (sweep, 2026-08-25).
   Padding stays inside the panel, so what is measured is what stacks. */
.rpt-page .vsr-zone > h3 { font-size: 12px; margin: 0 0 4px; padding-top: 10px; color: #111; }
/* A work note under the verification note: one line each, smaller, and the
   number in figures that line up so a column of them can be scanned. */
.rpt-page .vsr-wn { font-size: 9px; line-height: 1.25; margin-top: 2px;
  font-variant-numeric: tabular-nums; }
/* THE NOTES COLUMN GETS NO MINIMUM, and that is a measurement rather than a
   preference: a 150px floor here put the table 3px over the content width on
   A4 PORTRAIT — the tightest of the five papers — while every other paper and
   orientation had room to spare. The two nowrap rules above are worth more
   than a floor on a column that already carries the most text and therefore
   already gets the most width from the table layout. Measured, not guessed:
   zero overflow across five papers, both orientations. */

/* HOUSE CONTROL SIZE (owner 2026-08-13: "the text boxes all appear a little too
   small. Are they following our standard sizes?"). They were not: these boxes
   had a width and no padding or font, so they fell through to the browser's own
   ~11px default while every other panel in the app says 6px 8px at
   --fs-body-s. Said here once for every control on a controller card, which is
   why the NIC editor's octet boxes are excluded — those are four 3-character
   cells with their own metrics. */
.ctl-card :where(.ctl-fld, .ctl-line, .ctl-fnrow, .ctl-add, .ctl-linkhead)
  :where(input:not(.nic-oct), select, textarea) {
  padding: 6px 8px; font: inherit; font-size: var(--fs-body-s); }
/* AND THE SAME HEIGHT, which equal padding does not buy (owner 2026-08-13: "the
   drop-down and neighboring field are different sizes. I think we have rules
   about this"). A native <select> adds its own metrics on top of whatever is
   asked of it, so a select and an input with identical padding and font come
   out different heights on macOS. Stated once, as a height rather than as
   padding, so the two agree by construction. Textareas are excluded — they are
   sized in rows, which is the point of them. */
.ctl-card :where(.ctl-fld, .ctl-line, .ctl-fnrow, .ctl-add, .ctl-linkhead)
  :where(input:not(.nic-oct):not([type=checkbox]), select) {
  height: 30px; line-height: 1.2; }
.ctl-fld input, .ctl-fld select { width: 148px; }
.ctl-fld-wide input { width: 300px; max-width: 100%; }
/* The description is what the unit IS, and on a card of forty boxes it is the
   one you read first (owner 2026-08-13). */
.ctl-fld-desc input { font-weight: 700; }
/* A gated field, with its checkbox unticked: greyed, inert, and still there.
   "No web interface" is an answer, and an answer needs somewhere to be. */
.ctl-fld-off { opacity: .5; }
.ctl-fld-full { flex: 1 1 100%; min-width: 0; }
.ctl-fld-full textarea { width: 100%; }
/* Inherited from the rack, shown through as a placeholder rather than copied:
   the same grammar voltage and wattage use for the Device Type. */
.ctl-a.calc-cell::placeholder { color: color-mix(in srgb, var(--calc) 80%, transparent);
  font-style: italic; opacity: 1; }
.ctl-chk { display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fs-body-s); color: var(--text-2); }
.ctl-scope { border-top: 1px solid var(--line-soft); margin-top: 12px; padding-top: 8px; }
.ctl-linkhead { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.ctl-kind { font-size: var(--fs-mini); text-transform: uppercase; letter-spacing: .06em;
  color: var(--dim); }
.ctl-via { width: 100%; min-width: 0; }
/* The derived list. Deliberately quiet: it is a consequence of the choice
   above it, not something anybody edits. */
/* ONE LINK IS ONE ROW, AND THE ROWS LINE UP (owner 2026-08-19: "One entry
   should be a row... with multiple fields across. Description, Device ID, then
   Zone.... followed by the notes field").

   The grid is on the LIST, not on each entry, which is what makes every
   entry's columns land on the same edges. Each entry contributes five cells
   and, in Control scope, a sixth that spans the width for the derived "what is
   under it" line belonging to the entry above it.

   Description takes the slack because it is the part that varies most; Device
   ID is monospaced and sized to a real assembled ID. */
.ctl-linkgrid { display: grid; align-items: center; gap: 2px 10px; margin-top: 4px; }
/* The heading cell carries the drag handle, so it needs somewhere to put it. */
.ctl-lch { position: relative; }
.ctl-linkgrid > .ctl-linkfull { grid-column: 1 / -1; }
.ctl-lch { font-size: var(--fs-mini); text-transform: uppercase; letter-spacing: .06em;
  color: var(--dim); border-bottom: 1px solid var(--line-soft); padding-bottom: 3px; }
.ctl-lc { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ctl-lc-desc { color: var(--text); }
.ctl-lc-id { font-family: var(--mono); font-weight: 700; }
.ctl-lc-zone { color: var(--text-2); }
/* A zone target has no Device ID and never will, so the column says what it IS
   rather than printing a dash that could mean "none" or "not yet". */
.ctl-lc-kind { font-family: var(--font); font-weight: 400; font-size: var(--fs-small); color: var(--dim); }
.ctl-lc-none { color: var(--dim); }
.ctl-lc-x { text-align: right; }
.ctl-under { margin: 2px 0 8px; font-size: var(--fs-small); color: var(--text-2); }
.ctl-add { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin-top: 8px; }

/* Folding a card. Shut, it is one line: enough to tell two consoles in the same
   rack apart, and nothing else (owner 2026-08-13). */
.ctl-fold { background: transparent; border: none; color: var(--dim); cursor: pointer;
  font-size: 11px; line-height: 1; padding: 0 2px; }
.ctl-fold:hover { color: var(--accent); }
.ctl-card.ctl-shut { padding: 8px 14px; }
.ctl-brief { margin: 4px 0 0 16px; font-size: var(--fs-small); color: var(--text-2); }
/* An emoji at --fs-mini is an illegible dot rather than a clock, so this one
   keeps body size. It is a control people look for, not a label. */
.ctl-hist, .ctl-table .bom-actions .ctl-hist { background: transparent; border: none;
  color: var(--dim); cursor: pointer; font-size: var(--fs-body-s); padding: 0 2px; line-height: 1; }
.ctl-hist:hover { color: var(--text); }
/* The same clock, now in every grid that has a history to open (2026-08-17).
   Each host sets its own `button` rules — .dt-btns and .ld-btns give theirs a
   border and .bom-actions shrinks them to --fs-mini — so the control is
   restated at each one rather than left to inherit a box around a clock face.
   One declaration block, so the control cannot look like four controls. */
.dt-btns .ctl-hist, .ld-btns .ctl-hist, .rowbtns .ctl-hist, .bom-actions .ctl-hist {
  background: transparent; border: none; color: var(--dim); cursor: pointer;
  font-size: var(--fs-body-s); padding: 0 2px; line-height: 1; }
.dt-btns .ctl-hist:hover, .ld-btns .ctl-hist:hover,
.rowbtns .ctl-hist:hover, .bom-actions .ctl-hist:hover { color: var(--text); }
/* The network ports on a card. The editor is the Network tab's own, so an
   address typed here is the one every tab shows.
   OPEN, AND ONE BOX FEWER (owner 2026-08-13: "this should default to the
   expanded view... there's no need to collapse that. Expanding this should
   allow you to remove one layer of card stacking and borders"). The NIC panel
   draws itself as an attached sub-panel — tinted fill, accent hairline, inset
   margin — which is right where it hangs off a grid row and wrong inside a
   section that is already a box saying the same thing. Here it is just the
   section's contents. */
.ctl-net { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 10px 0 0; }
.ctl-nethint { margin: 0 0 8px; font-size: var(--fs-mini); color: var(--faint); }
.ctl-sec-net .nic-panel { border: none; background: transparent; margin: 0; padding: 0; }

/* Control input and control output are LISTS of functions, so they get a row
   editor rather than a field box: a media server can take timecode on one port
   and Art-Net on four others at the same time (owner 2026-08-13). */
.ctl-fns { display: grid; grid-template-columns: repeat(auto-fit, minmax(520px, 1fr));
  gap: 12px 20px; margin-top: 12px; align-items: start; }
/* Each side of the relationship sits under the functions that carry it: what
   drives this one under Control input, what it drives under Control output
   (owner 2026-08-13). Both are the same table read from opposite ends. */
.ctl-rel { border-top: 1px solid var(--line-soft); margin-top: 10px; padding-top: 8px; }
.ctl-rellabel { font-size: var(--fs-mini); text-transform: uppercase; letter-spacing: .06em;
  color: var(--dim); margin-bottom: 6px; }
/* AN EMPTY BLOCK IS SHORTER THAN A FULL ONE (owner 2026-08-13: "Driven By and
   Control Scope are both taller without data than with data... please keep them
   shorter, expanding as things are added"; and of Applications, "when no
   application is present, it's taller than when one is added. the smaller
   height is better"). Both were using .ws-empty, whose 18px of vertical padding
   is right for an empty PAGE region and absurd for an empty three-row list. It
   is one quiet line now, and the block grows from there. */
.ctl-fnempty, .ctl-rel .ws-empty { padding: 2px 0; margin: 0; font-size: var(--fs-small);
  color: var(--dim); line-height: 1.4; }
.ctl-fnpanel { display: flex; flex-direction: column; gap: 4px; }
.ctl-fnrow { display: flex; gap: 6px; align-items: center; }
.ctl-fnhead span { font-size: 9px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--dim); flex: none; white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; align-self: end; }
.ctl-fnf { flex: none; }
/* On a card the rows are a GRID, so the headings sit over their own columns and
   stay there at any card width. The proportions come from the same `w` the
   table uses for pixel widths — one set of numbers, two ways of spending them. */
.ctl-fnpanel-card .ctl-fnrow { display: grid; gap: 5px; align-items: center; }
.ctl-fnpanel-card :where(.ctl-fnf, .ctl-fnchk) { width: 100%; min-width: 0; }
.ctl-fnpanel-card .ctl-fnchk { justify-content: flex-start; }
/* Out of range, and still stored. Marked rather than clamped: a value silently
   changed to 200 is a value nobody knows they got wrong. */
.ctl-fnbad { box-shadow: inset 0 0 0 1px var(--warn); }
/* A port the standard fixes: shown, not asked for. Reads as a stated fact
   rather than an empty box waiting to be filled in. */
.ctl-fnfixed { border-style: dashed; opacity: 1; }
.ctl-fnfixed::placeholder { color: var(--text-2); font-style: normal; opacity: 1; }
.ctl-fndel { background: transparent; border: none; color: var(--dim); cursor: pointer;
  font-size: var(--fs-small); padding: 0 4px; }
.ctl-fndel:hover { color: var(--bad); }
.ctl-fnadd { align-self: flex-start; margin-top: 2px; }
.ctl-fnchk { display: inline-flex; align-items: center; gap: 5px; flex: none;
  font-size: var(--fs-mini); color: var(--text-2); }
/* A row-list that lives inside a field group rather than in a band of its own:
   Applications under Software, because that is what it is. */
.ctl-sublist { margin-top: 10px; }
/* A section whose whole content IS the list starts it at the top: there is no
   field row above it to be spaced away from. */
.ctl-sublist-first { margin-top: 0; }
/* The table's cell: names the protocols rather than counting them, because
   "sACN (E1.31), Art-Net" is the answer and "2" is not.

   TEXT, not a button label. A button's text cannot be selected, copied, or
   pasted into, and in a table meant to behave like a spreadsheet that is a hole
   (owner 2026-08-13). The chevron beside it opens the panel where ports and the
   rest of each row are edited. */
.ctl-fncell { display: flex; align-items: center; gap: 2px; width: 100%; }
.ctl-fnsum { flex: 1; min-width: 0; border: none; background: transparent; color: var(--text);
  border-radius: 0; padding: 4px 6px; font-size: var(--fs-small); }
.ctl-fnsum:focus { background: var(--panel2); box-shadow: inset 0 0 0 1px var(--accent); outline: none; }
.ctl-fnbtn { flex: none; display: inline-flex; align-items: center; gap: 3px;
  background: transparent; color: var(--dim); border: none; border-radius: var(--r-soft);
  padding: 2px 5px; font-size: var(--fs-mini); cursor: pointer; line-height: 1; }
.ctl-fnbtn:hover { color: var(--accent); background: var(--panel2); }
.ctl-fncount { font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* Its own class, not .dev-grip, for the same reason .net-grip is: the Device
   List binds a resize handler to that one, so a grip carrying it would drag
   this column AND write a width into the Device List's saved preferences. */
.ctl-grip { position: absolute; top: 0; right: -3px; width: 7px; height: 100%;
  cursor: col-resize; z-index: 2; }
.ctl-grip:hover { background: var(--accent); opacity: .5; border-radius: 2px; }

/* ---- Controllers, Table view --------------------------------------------
   The Device Schedule's grid, reused rather than reinvented: same row and cell
   classes, same drag grips, same folding bands. Only the frozen columns need
   saying again, because those selectors are scoped per table. */
/* FROZEN: the actions cell, Device ID, and Description (owner 2026-08-13).
   Scroll to Access & Startup and a row is otherwise an anonymous line of
   settings; the description is the part people actually recognize it by.

   Description's offset is the actions cell plus WHATEVER DEVICE ID IS NOW, and
   Device ID is drag-resizable, so the offset cannot be a constant. The table
   carries `--ctl-devid-w` and the drag updates it live, which is why the width
   stays adjustable and the freeze still lands in the right place. */
.ctl-table .bom-actions { position: sticky; left: 0; z-index: 4; background: var(--surface-1); }
.ctl-table .bom-c[data-ctk="devid"] { position: sticky; left: 56px; z-index: 4;
  background: var(--surface-1); }
.ctl-table .bom-c[data-ctk="description"] { position: sticky;
  left: calc(56px + var(--ctl-devid-w, 150px)); z-index: 4;
  background: var(--surface-1); box-shadow: 1px 0 0 var(--line-soft); }
.ctl-table .bom-head .bom-actions,
.ctl-table .bom-head .bom-c[data-ctk="devid"],
.ctl-table .bom-head .bom-c[data-ctk="description"],
.ctl-table .sched-bandhead .bom-actions,
.ctl-table .sched-bandhead .bom-c[data-ctk="devid"],
.ctl-table .sched-bandhead .bom-c[data-ctk="description"] { background: var(--surface-2); z-index: 5; }
/* A hidden band gap paints no background, so the frozen strip would show the
   columns sliding underneath it. Shown, and simply left empty. */
.ctl-table .sched-bandhead .bom-c[data-ctk="devid"],
.ctl-table .sched-bandhead .bom-c[data-ctk="description"] { visibility: visible; }
/* EVERY read-only cell, not just the zone one: a span in a fixed-width column
   does not clip by default, so "Light System Manager" ran straight over the
   Variation beside it. The full value is in the title, and the column resizes. */
.ctl-table .bom-c.bom-ro { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ctl-table .bom-c[data-ctk="nics"],
.ctl-table .bom-c[data-ctk="inputs"],
.ctl-table .bom-c[data-ctk="outputs"],
.ctl-table .bom-c[data-ctk="scope"] { padding: 2px 4px; }
.ctl-table .bom-c[data-ctk="scope"] button { width: 100%; font-size: var(--fs-mini); }
/* A NIC or function panel is a full-width block under its row, and the rows are
   as wide as the table's content, so it has to be pinned to the left edge or it
   opens off screen once you have scrolled across. */
.ctl-table .nic-panel,
.ctl-table .ctl-fnpanel { position: sticky; left: 0; max-width: 100vw; }
.ctl-table .ctl-fnpanel { padding: 6px 8px; border-bottom: 1px solid var(--line-soft);
  background: var(--panel2); }
/* A Light Source cell on a device that is not a lighting fixture. Normally
   empty, and then it is just a gap in the row — no border treatment, nothing
   to look at. When it carries a value the fixture used to have, it reads as
   dimmed text you cannot edit, and the tooltip says why. */
.dev-c.dev-na { color: var(--dim); justify-content: flex-start; padding: 0 6px; }
.dev-c.dev-na:empty { background: repeating-linear-gradient(135deg,
  transparent 0 6px, var(--line-soft) 6px 7px); opacity: .25; }

/* A rack's Device ID reads as the identifier it is; a rack that has not been
   given a type says so quietly rather than showing a dash, which would be a
   blank that could mean anything. */
.cn-did { font-family: var(--mono); }
.cn-did-none { font-family: var(--font); color: var(--dim); font-size: var(--fs-small); font-style: italic; }

/* A device code containing the project's Device ID separator: the assembled ID
   cannot be read back apart. A warning, not an error — the code is valid, the
   COMBINATION is what bites, and the separator is configurable. */
.dev-code-sep { box-shadow: inset 0 -2px 0 0 var(--warn); }

/* ---- Site Conditions -------------------------------------------------------
   Survey and progress material: photographs and video filed against an Event.
   The layout is a fixed event rail beside a grid of tiles, because the event is
   the thing somebody navigates by and the pictures are what they came to see.
   Tokens only — no literal colors, so both themes follow the design system.

   RADIUS: --r-ctl is the PILL token (999px), for buttons and chips. A media
   tile styled with it renders as a circle, which is how this shipped the first
   time and was caught by looking at it. Rectangular surfaces here take
   --r-soft; only the video chip is a genuine pill. */
.sc-layout { display: grid; grid-template-columns: 232px minmax(0, 1fr); gap: 14px; }
@media (max-width: 900px) { .sc-layout { grid-template-columns: minmax(0, 1fr); } }

.sc-events { display: flex; flex-direction: column; gap: 4px; }
.sc-ev { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 8px;
  align-items: baseline; text-align: left; padding: 8px 10px;
  border: 1px solid var(--line-soft); border-radius: var(--r-soft);
  background: var(--panel); color: var(--text); cursor: pointer; }
.sc-ev:hover { border-color: var(--accent); }
.sc-ev.on { border-color: var(--accent); background: var(--panel2); }
.sc-ev-name { grid-column: 1; font-weight: 600; }
.sc-ev .muted { grid-column: 1; font-size: var(--fs-small); }
.sc-ev-count { grid-row: 1 / span 2; grid-column: 2; align-self: center;
  font-size: var(--fs-small); color: var(--dim); }

.sc-ev-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.sc-ev-head h3 { margin: 0; }
.sc-ev-notes { margin: 4px 0 0; color: var(--text-2); font-size: var(--fs-small); }

/* The drop target. Dashed only while it is a target — a permanently dashed box
   reads as an error state in every other part of this app. */
.sc-drop { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 10px 0; padding: 10px 12px; border: 1px dashed var(--line);
  border-radius: var(--r-soft); background: var(--panel); }
.sc-drop.over { border-color: var(--accent); background: var(--panel2); }
.sc-drop .muted { font-size: var(--fs-small); }

.sc-grid { display: grid; gap: 10px; margin-top: 10px;
  grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
  align-items: stretch; }
.sc-tile { position: relative; display: flex; flex-direction: column; gap: 2px;
  padding: 0 0 6px; overflow: hidden; text-align: left; cursor: pointer;
  border: 1px solid var(--line-soft); border-radius: var(--r-soft);
  background: var(--panel); color: var(--text); }
.sc-tile:hover { border-color: var(--accent); }
.sc-tile.on { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.sc-tile img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; display: block;
  background: var(--panel2); }
.sc-noimg { display: grid; place-items: center; aspect-ratio: 4 / 3;
  background: var(--panel2); color: var(--dim); font-size: var(--fs-small); }
.sc-tile-cap { padding: 0 8px; font-size: var(--fs-small);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sc-tile-zone { padding: 0 8px; font-size: var(--fs-mini); color: var(--dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sc-vid { position: absolute; top: 6px; left: 6px; padding: 1px 6px;
  border-radius: var(--r-ctl); font-size: var(--fs-mini);
  background: var(--panel); border: 1px solid var(--line); }

.sc-detail { margin-top: 12px; border: 1px solid var(--line);
  border-radius: var(--r-soft); background: var(--panel); }
.sc-detail-h { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 8px 12px; border-bottom: 1px solid var(--line-soft); }
.sc-detail-body { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
  gap: 14px; padding: 12px; }
@media (max-width: 900px) { .sc-detail-body { grid-template-columns: minmax(0, 1fr); } }
/* Sized to the media rather than assuming landscape: a portrait phone photo
   and a rotated clip both carry their own shape. */
.sc-detail-media img, .sc-detail-media video { max-width: 100%; max-height: 460px;
  display: block; margin: 0 auto; border-radius: var(--r-soft); }
.sc-detail-side { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.sc-lbl { font-size: var(--fs-mini); text-transform: uppercase; letter-spacing: .06em;
  color: var(--dim); margin-top: 6px; }

.sc-meta { margin-top: 8px; font-size: var(--fs-small); }
.sc-mrow { display: grid; grid-template-columns: minmax(88px, auto) minmax(0, 1fr);
  gap: 8px; padding: 2px 0; }
.sc-mk { color: var(--dim); }
.sc-mv { color: var(--text-2); min-width: 0; }

.sc-weather { margin-top: 10px; font-size: var(--fs-small); color: var(--text-2); }
/* A caveat that must travel with the number beside it — an unreliable heading,
   an indoor altitude, an exposure describing the flash. Quiet but never absent:
   a number whose error is invisible gets trusted. */
.sc-warn { color: var(--warn); font-size: var(--fs-mini); }

/* Recently deleted. Visible on purpose: a deletion nobody can see is
   indistinguishable from data loss, which is what the 30-day window exists to
   prevent. */
.sc-trash { margin: 10px 0; padding: 8px 10px; border: 1px solid var(--line);
  border-radius: var(--r-soft); background: var(--panel2); }
.sc-trash-h { font-weight: 600; margin-bottom: 6px; display: flex; gap: 10px;
  align-items: baseline; flex-wrap: wrap; }
.sc-trash-h .muted { font-weight: 400; font-size: var(--fs-small); }
.sc-trow { display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap;
  padding: 4px 0; border-top: 1px solid var(--line-soft); font-size: var(--fs-small); }
.sc-tkind { font-weight: 600; }
.sc-tnote { flex: 1; min-width: 120px; color: var(--text-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Selecting several, to file a colleague's photographs in one go. The checkbox
   sits beside the tile rather than inside it: nested in the button it would
   open the detail panel on every tick. */
/* The grid item is now the CELL, not the tile. A <button> shrink-wraps to its
   content, so without stretching it here the tiles stopped filling their
   columns and the grid came apart. Caught by looking at it. */
.sc-cell { position: relative; display: flex; }
.sc-cell > .sc-tile { flex: 1; min-width: 0; }
.sc-cell.picked .sc-tile { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
/* SCOPED TO THE TILE ON PURPOSE. `.sc-pick` is also the Device Schedule's
   row checkbox, and this rule used to reach it: a 20x20 box meant to sit over
   a photograph, dropped into a 23px grid row where every button is 18px and
   every field 22px. Measured, not guessed. The same mistake `.bl-cat` made —
   one class name serving two unrelated surfaces. */
.sc-cell > .sc-pick { position: absolute; top: 8px; right: 8px; z-index: 2;
  width: 20px; height: 20px; margin: 0; cursor: pointer;
  accent-color: var(--accent); }

.sc-selbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 10px 0 0; padding: 8px 10px; border: 1px solid var(--accent);
  border-radius: var(--r-soft); background: var(--panel2); }
/* Before anything is picked it is a hint, not a toolbar: quiet border, no fill,
   so an empty selection does not look like a mode you are stuck in. */
.sc-selbar.quiet { border-color: var(--line-soft); background: transparent; }
.sc-selbar .muted { font-size: var(--fs-small); }
.sc-selsep { width: 1px; height: 18px; background: var(--line); }

/* ---- grouped by zone, and the detail beside the gallery --------------------
   The detail used to sit BELOW the grid, which forced a choice between seeing
   the photograph and seeing the set it came from. On a survey you are always
   comparing, so it moved to its own column. */
.sc-layout.with-detail { grid-template-columns: 208px minmax(0, 1fr) minmax(300px, 380px); }
@media (max-width: 1280px) {
  .sc-layout.with-detail { grid-template-columns: 208px minmax(0, 1fr); }
  .sc-layout.with-detail .sc-side { grid-column: 1 / -1; }
}
@media (max-width: 900px) {
  .sc-layout, .sc-layout.with-detail { grid-template-columns: minmax(0, 1fr); }
}
.sc-main { min-width: 0; }
/* Sticky so the picture stays put while the gallery scrolls past it. */
.sc-side { min-width: 0; position: sticky; top: 12px; align-self: start;
  max-height: calc(100vh - 24px); overflow: auto; }
.sc-side .sc-detail { margin-top: 0; }
/* In a narrow column the media and its fields stack rather than squeeze. */
.sc-side .sc-detail-body { grid-template-columns: minmax(0, 1fr); }
.sc-side .sc-detail-media img, .sc-side .sc-detail-media video { max-height: 320px; }

.sc-group { margin-top: 10px; }
/* Indented to its depth, so the grouping reads as the hierarchy rather than as
   a list that happens to be sorted. Capped so a deep tree still leaves room
   for tiles. */
.sc-ghead { display: flex; align-items: baseline; gap: 8px; width: 100%;
  margin-left: calc(var(--sc-depth, 0) * 14px);
  padding: 4px 8px; text-align: left; cursor: pointer;
  border: 0; border-bottom: 1px solid var(--line-soft); border-radius: 0;
  background: transparent; color: var(--text); }
.sc-ghead:hover { border-bottom-color: var(--accent); }
.sc-gcaret { color: var(--dim); font-size: var(--fs-small); width: 10px; }
.sc-gname { font-weight: 600; }
.sc-gcount { margin-left: auto; color: var(--dim); font-size: var(--fs-small); }
/* Unfiled is work outstanding, not a place. It sits first and says so. */
.sc-group.unfiled .sc-gname { color: var(--warn); }

/* ---- WiFi (owner 2026-08-13) ----------------------------------------------
   The wireless networks a project puts on the air. A grid like the WAN list,
   with one column that is not a plain value: which VLANs an SSID reaches is a
   choice from the project's own plan, so it opens a panel of checkboxes under
   the row rather than asking somebody to type a number. */
.wf-table .bom-actions { width: 34px; }
.wf-vlanbtn { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  justify-content: flex-start; }
.wf-vlanpanel { display: flex; flex-wrap: wrap; gap: 6px 16px; padding: 8px 10px 10px 42px;
  border-bottom: 1px solid var(--line-soft); background: var(--surface-2); }
.wf-vlanchk { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-body-s);
  color: var(--text-2); white-space: nowrap; }
/* A chosen VLAN in the WiFi grid wears its field colour, the way it does in the
   checkbox panel below and in every VLAN picker on the NIC editor: the colour
   code is how a VLAN is identified on site (owner 2026-08-13). */
.wf-vlantag { display: inline-flex; align-items: center; gap: 4px; margin-left: 6px; white-space: nowrap; }
.wf-vlantag + .wf-vlantag { margin-left: 8px; }

/* A LIST SECTION ON A SHEET WEARS THE SAME CLOTHES AS A VLAN CARD (owner
   2026-08-13: "make the WAN connections look better... they should be styled
   like the other entries"). Same frame and band, so a page of WAN links and a
   page of VLANs read as one document rather than two stapled together. */
/* Each list in its own swept border (owner 2026-08-15), clean inside: no
   gray, no vertical lines, uniform insets. */
.rpt-page .vc-listcard { border: 2px solid #111; border-radius: 3px 18px 3px 18px;
  padding: 6px; margin-bottom: 16px; }
.rpt-page .vc-listh { margin: 2px 4px 4px; font-size: 12px; font-weight: 800;
  text-transform: uppercase; letter-spacing: .07em; }
.rpt-page .rpt-listtable { margin: 0; }
/* Horizontal rules only (owner 2026-08-15) — the WAN and Wireless lists read
   like the BOM's tables: the row line and the column spacing separate plenty. */
.rpt-page .rpt-listtable th { padding: 5px 8px 4px; background: transparent; border-top: 0; border-left: 0; border-right: 0; color: #666; }
.rpt-page .rpt-listtable td { padding: 5px 8px; border-left: 0; border-right: 0; }
.rpt-page .rpt-listtable tr:last-child td { border-bottom: 0; }
.rpt-page .rpt-listnone { margin: 0; padding: 10px 16px 12px; }
/* The WiFi password: masked, with the reveal inside the cell so it cannot be
   mistaken for a column of its own (owner 2026-08-13). */
.wf-secret { display: flex; align-items: center; gap: 2px; }
.wf-secret input { flex: 1; min-width: 0; }
.wf-eye { flex: none; background: transparent; border: none; color: var(--dim);
  cursor: pointer; padding: 2px 4px; line-height: 1; display: inline-flex; }
.wf-eye:hover { color: var(--accent); }

/* ---- Cheat Sheets: which slot, and where it is going (owner 2026-08-13) ----
   "Something needs to indicate which slot number it's in... and it needs to be
   clear where you're moving it to." A number on every draggable thing, and a
   LINE on the edge it will land against rather than a highlight around the
   whole target — "somewhere near this block" is not an answer to where it is
   going. */
.cs-slot { font-family: var(--mono); font-size: var(--fs-mini); font-weight: 700;
  color: var(--dim); background: var(--panel2); border-radius: 4px; padding: 0 5px; }
.cs-tab-i { font-family: var(--mono); font-size: var(--fs-mini); color: var(--dim); margin-right: 6px; }
.cs-tab.on .cs-tab-i { color: var(--on-accent); opacity: .75; }
.cs-grip .cs-slot { margin-left: 4px; }
.cs-block-h { display: flex; align-items: center; gap: 8px; }
.cs-block-h .grow { flex: 1; }
.cs-bgrip { cursor: grab; color: var(--dim); font-size: var(--fs-small); line-height: 1; user-select: none; }
.cs-bgrip:active { cursor: grabbing; }
.cs-block.cs-dragging { opacity: .45; }
.cs-block.cs-dropabove { box-shadow: 0 -3px 0 -1px var(--accent); }
.cs-block.cs-dropbelow { box-shadow: 0 3px 0 -1px var(--accent); }

/* The connector symbol, sitting in the text line it belongs to. Baseline
   alignment through vertical-align rather than flex, because it lives inside
   a <strong> in a key-value row. */
.spec-kv .conn-sym { vertical-align: var(--spec-conn-sym-drop); margin-right: var(--spec-conn-sym-gap); }

/* ---- Renumber Device IDs (public/workspace/app.js, openRenumber) ----------
   A working surface, not a confirmation: the pool is edited IN here and
   nothing is written until Commit. Wide, because "which fixture is this" is
   answered by the manufacturer and the patch, not by the row number, and the
   four ID columns stay pinned so scrolling out to Universe never costs you
   sight of what you are renumbering. */
.rn-modal { max-width: 1240px; width: calc(100vw - 64px); }
.rn-stats { display: flex; gap: 8px; flex-wrap: wrap; margin: 0 0 12px; }
.rn-stat { background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-ctl);
  padding: 4px 12px; font-size: var(--fs-small); color: var(--dim); white-space: nowrap; }
.rn-stat b { color: var(--text); font-variant-numeric: tabular-nums; margin-right: 4px; }
/* Errors is the only count that can change colour, and Zones warns above one:
   more than one zone is where "restart the numbering per zone" starts to
   matter, and a count is where you would notice. */
.rn-stat.bad { border-color: var(--bad); color: var(--bad); }
.rn-stat.bad b { color: var(--bad); }
.rn-stat.warn { border-color: var(--warn); color: var(--warn); }
.rn-stat.warn b { color: var(--warn); }

.rn-ctl { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 0 0 10px;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-soft); padding: 8px 12px; }
.rn-ctl label { font-size: var(--fs-mini); text-transform: uppercase; letter-spacing: .05em; color: var(--dim); }
.rn-ctl select, .rn-ctl input { width: auto; padding: 3px 8px; font-size: var(--fs-body-s); }
.rn-modal .rn-sm { width: 62px; text-align: right; font-variant-numeric: tabular-nums; }
.rn-sep { flex: 1; }
/* Once a row is dragged the header stops claiming a clean sort. Sorting again
   would silently throw those adjustments away. */
.rn-adj { font-size: var(--fs-small); color: var(--accent2); }

.rn-scroll { overflow: auto; max-height: min(52vh, 460px); border: 1px solid var(--line); border-radius: var(--r-soft); }
.rn-grid { width: 100%; border-collapse: separate; border-spacing: 0; font-size: var(--fs-body-s); }
.rn-grid th { position: sticky; top: 0; z-index: 2; text-align: left; font-size: var(--fs-mini);
  text-transform: uppercase; letter-spacing: .05em; color: var(--dim); font-weight: 600;
  padding: 7px 10px; background: var(--surface-2); border-bottom: 1px solid var(--line); white-space: nowrap; }
.rn-grid td { padding: 3px 10px; border-bottom: 1px solid var(--line-soft); white-space: nowrap;
  background: var(--panel); overflow: hidden; text-overflow: ellipsis; }
.rn-grid tbody tr:hover td { background: var(--surface-2); }
.rn-n { text-align: right; font-variant-numeric: tabular-nums; }
.rn-none::after { content: '\00B7'; color: var(--faint); }
.rn-desc { color: var(--dim); }
.rn-p { position: sticky; z-index: 1; }
.rn-grid th.rn-p { z-index: 3; }
.rn-grid .rn-c3 { box-shadow: 1px 0 0 var(--line); }
.rn-rz { position: absolute; top: 0; right: -3px; width: 7px; height: 100%; cursor: col-resize; z-index: 4; }
.rn-rz:hover { background: var(--accent); opacity: .5; }
/* The cell is sticky; the HANDLE INSIDE IT is what drags. A position:sticky
   drag source starts and is abandoned in the same frame, which read as "drag
   did nothing and left the row greyed" (owner 2026-08-18). */
.rn-grip { color: var(--faint); text-align: center; padding: 3px 4px; }
.rn-griph { display: inline-block; cursor: grab; line-height: 1;
  /* The pointer owns this gesture: no text selection under it, and no browser
     scroll stealing the drag on a trackpad or a touch screen. */
  user-select: none; touch-action: none; }
.rn-griph:active { cursor: grabbing; }
.rn-ord { color: var(--faint); font-variant-numeric: tabular-nums; text-align: right; font-size: var(--fs-small); }
.rn-cur { font-family: var(--mono); font-size: var(--fs-small); }
.rn-new { font-family: var(--mono); font-size: var(--fs-small); }
/* The recipe half is present and unreachable, the same grammar the Network tab
   uses for the octets a VLAN owns. One column, one ID, and only the part you
   own is live. */
.rn-fix { color: var(--faint); user-select: none; }
/* Two classes, because `.modal input { width: 100% }` in style.css outranks one
   and the field grew to 136px inside a 156px cell. */
.rn-grid .rn-in { width: 56px; margin: 0; height: 24px; box-sizing: border-box; font: inherit;
  text-align: left; background: var(--surface-2); color: var(--text);
  border: 1px solid transparent; border-radius: var(--r-ctl); padding: 2px 8px; }
.rn-grid .rn-in:hover { border-color: var(--line); }
.rn-grid .rn-in:focus { border-color: var(--accent); outline: none; }
/* A full-width wash, never a left edge (the shape law). BOTH offenders take it:
   marking only the second leaves you hunting for the first. */
.rn-err td { background: color-mix(in srgb, var(--bad) 16%, transparent); }
.rn-err .rn-in { border-color: var(--bad); color: var(--bad); }
.rn-warn td { background: color-mix(in srgb, var(--warn) 10%, transparent); }
.rn-dragging { opacity: .45; }
.rn-flag { text-align: center; }
.rn-chg { color: var(--accent); }
.rn-warnk { color: var(--warn); font-weight: 700; }
.rn-drop { background: transparent; border: none; color: var(--faint); cursor: pointer;
  padding: 0 3px; font-size: var(--fs-small); }
.rn-drop:hover { color: var(--bad); border: none; }

/* The list that names what you cannot see from here. A red cell says something
   is wrong; this says what you collided with, in a zone that is not open. */
.rn-coll { margin-top: 12px; border: 1px solid var(--bad); border-radius: var(--r-soft);
  padding: 10px 12px; background: color-mix(in srgb, var(--bad) 8%, transparent); }
.rn-coll-h { font-size: var(--fs-mini); text-transform: uppercase; letter-spacing: .05em;
  color: var(--bad); font-weight: 600; margin-bottom: 6px; }
.rn-coll-h .muted { text-transform: none; letter-spacing: 0; font-weight: 400; }
.rn-ctab { width: 100%; border-collapse: collapse; font-size: var(--fs-body-s); }
.rn-ctab th { text-align: left; font-size: var(--fs-mini); text-transform: uppercase;
  letter-spacing: .05em; color: var(--dim); font-weight: 600; padding: 0 10px 4px 0; }
.rn-ctab td { padding: 3px 10px 3px 0; border-top: 1px solid var(--line-soft); }
.rn-mono { font-family: var(--mono); font-size: var(--fs-small); }
.rn-msg { font-size: var(--fs-small); margin: 10px 0 0; }
.rn-msg.bad { color: var(--bad); }
.rn-foot { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px;
  margin-top: 14px; flex-wrap: wrap; }
.rn-legend { font-size: var(--fs-small); color: var(--dim); max-width: 60ch; }
.rn-foot .actions { margin-top: 0; }

/* The duplicate-Device-ID count, on the Device Schedule header. Loud on
   purpose: a duplicate can appear without anybody typing (a renamed zone, a
   changed recipe), so there is no save to warn at, and it can be sitting in a
   zone you have scrolled past. It is a button, because the thing that tells
   you something is wrong should be the thing that takes you to the fix. */
.sc-dupbtn { background: color-mix(in srgb, var(--bad) 14%, transparent); color: var(--bad);
  border: 1px solid var(--bad); border-radius: var(--r-ctl); padding: 3px 12px;
  font: inherit; font-size: var(--fs-small); cursor: pointer; margin-left: 10px; white-space: nowrap; }
.sc-dupbtn:hover { background: color-mix(in srgb, var(--bad) 24%, transparent); border-color: var(--bad); }
.sc-dupbtn .muted { color: inherit; opacity: .75; }

/* ---- Renumber Map (report) ------------------------------------------------
   Two columns of pairs, because sixty short strings down one side of a sheet
   is mostly white and makes the reader turn a page for a device that would
   have fitted beside the one above it. The columns break independently, so a
   long map simply carries on down the next page. */
.rpt-page .rnm-when { font-size: 11px; color: #444; margin: 0 0 8px; }
.rpt-page .rnm-cols { display: flex; gap: 22px; align-items: flex-start; }
.rpt-page .rnm-cols > .rnm-tbl { flex: 1 1 0; min-width: 0; }
.rpt-page .rnm-tbl { border-collapse: collapse; width: 100%; }
.rpt-page .rnm-tbl th { text-align: left; font-size: 8.5px; color: #666; text-transform: uppercase;
  letter-spacing: .06em; font-weight: 700; padding: 0 7px 3px 0; border-bottom: 1px solid #bbb; }
.rpt-page .rnm-tbl td { font-size: 10px; padding: 2.5px 7px 2.5px 0; border-bottom: 1px solid #e4e4e4;
  font-family: var(--mono); white-space: nowrap; }
/* The old ID is the one somebody is holding on paper, so it stays the plain
   one; the new ID is what they should write down, so it carries the weight. */
.rpt-page .rnm-tbl .rnm-old { color: #555; }
.rpt-page .rnm-tbl .rnm-new { font-weight: 700; }
.rpt-page .rnm-foot { font-size: 9px; margin-top: 10px; }

/* ==========================================================================
   FOCUS — where each fixture is pointed, and how (owner 2026-08-18)
   --------------------------------------------------------------------------
   The grid follows the Device List's skeleton exactly: a flex row of fixed,
   drag-resizable cells, a band strip above the column headers, and sticky
   group bands between runs of rows. Only the three controls this tab invents —
   the two five-position scales, the axis, and the images cell — are new, and
   they are drawn rather than typed because that is how the paper focus charts
   this replaces say the same things.
   ========================================================================== */
.fc-table { overflow-x: auto; padding-bottom: 6px; border-top: 1px solid var(--line); }
.fc-row { display: flex; align-items: stretch; width: max-content; }
.fc-row:not(.fc-grouprow):not(.fc-head) { border-bottom: 1px solid var(--line-soft); }
.fc-head { border-bottom: 1px solid var(--line); margin-bottom: 2px; }
.fc-head .fc-c { position: relative; color: var(--dim); font-size: var(--fs-mini);
  text-transform: uppercase; letter-spacing: .04em; padding: 4px 6px; }
.fc-c { flex: none; box-sizing: border-box; padding: 0;
  border-right: 1px solid var(--line-soft); display: flex; align-items: center; }
/* Read-only columns: owned by the Device List or the Device Schedule, and said
   so quietly rather than with a lock icon on every cell of every row. */
.fc-c.fc-ro { padding: 4px 6px; color: var(--dim); font-size: var(--fs-small);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The Device ID is the row's name, so it reads like one. */
.fc-c.fc-lead { font-family: var(--mono); font-size: var(--fs-small); padding: 4px 6px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fc-in { width: 100%; border: none; background: transparent; border-radius: 0;
  padding: 4px 6px; color: var(--text); font-size: var(--fs-small); box-sizing: border-box; }
.fc-in:focus { background: var(--panel2); box-shadow: inset 0 0 0 1px var(--accent); outline: none; }
/* Beam ° is a measurement: tabular figures so a column of angles lines up, and
   the degree sign drawn by the cell rather than typed into every one. */
.fc-c[data-fck="beam_deg"] { position: relative; }
.fc-c[data-fck="beam_deg"] .fc-in { font-variant-numeric: tabular-nums; padding-right: 14px; }
.fc-c[data-fck="beam_deg"]::after { content: '°'; position: absolute; right: 5px;
  color: var(--dim); font-size: var(--fs-small); pointer-events: none; }
.fc-grip { position: absolute; top: 0; right: -3px; width: 7px; height: 100%; cursor: col-resize; }
.fc-grip:hover { background: var(--accent); opacity: .5; border-radius: 2px; }

/* Column-group strip, same grammar as the Device List's Product Details. */
.fc-grouphdr { flex: none; box-sizing: border-box; display: flex; align-items: center; gap: 4px;
  padding: 4px 8px; font-size: var(--fs-small); font-weight: 600; color: var(--text); overflow: hidden;
  background: var(--panel2); border-bottom: 1px solid var(--line); box-shadow: inset -1px 0 0 var(--line); }
.fc-grouphdr.fc-grp-collapsed { width: 76px; }
.fc-grp-collapsed .fc-grp-lbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--dim); font-weight: 500; font-size: var(--fs-mini); }
.fc-grp-collapsed-col { flex: none; width: 76px; box-sizing: border-box; }
.fc-grpbtn { flex: none; width: 18px; padding: 0; background: transparent; border: 1px solid transparent;
  color: var(--dim); font-size: var(--fs-mini); border-radius: 4px; }
.fc-grpbtn:hover { border-color: var(--line); color: var(--text); }

/* Group bands. Sticky at the left edge, like every other Workspace band, so the
   zone or the status you are inside stays readable however far right you have
   scrolled. */
.fc-band { display: flex; align-items: center; gap: 10px; width: max-content; min-width: 100%;
  background: var(--panel2); border-top: 1px solid var(--line); border-radius: 6px;
  padding: 5px 8px; margin-top: 8px; font-size: var(--fs-body-s); position: sticky; left: 0; }
.fc-band .muted { font-size: var(--fs-small); }
.fc-bandbtn { background: transparent; border: none; color: var(--dim); cursor: pointer;
  padding: 0 4px; font-size: var(--fs-small); }
.fc-bandbtn:hover { color: var(--text); border: none; }
/* Grouped rows indent to where the band's LABEL starts (band padding 8px +
   chevron ~22px + gap 10px), so a row reads as belonging to its band. The
   header rows carry the same class and shift with them — the indent must
   never separate a column from its heading. */
.fc-row.fc-ind { padding-left: 40px; }

/* ---- the status ---------------------------------------------------------
   Colour carries the state, and only where the state is worth marking: the
   ordinary middle of the job is left plain. Decorate the exception, not the
   rule. */
.fc-status { width: 100%; border: none; background: transparent; border-radius: 0;
  padding: 3px 4px; color: var(--text); font-size: var(--fs-small); }
.fc-status:focus { background: var(--panel2); box-shadow: inset 0 0 0 1px var(--accent); outline: none; }
.fc-status.s-none { color: var(--dim); }
.fc-status.s-not_ready { color: var(--warn); }
.fc-status.s-awaiting_approval { color: var(--accent); }
.fc-status.s-complete { color: var(--good); }

/* ---- the two scales, and the axis ---------------------------------------
   Sf . + . Hd, drawn. Five dots between two end labels, the chosen one filled;
   a cell nobody has read has NO dot filled, which is a different statement
   from one read as dead centre. One tab stop per cell (a roving tabindex in
   app.js) with the arrow keys moving inside it. */
/* SIZED TO ITS CELL: 32 (label) + two 22px ends + five 15px slots + eight 2px
   gaps + 8px padding = 127, inside the 128px the column starts at. The cell
   does not clip, so a scale that outgrows it silently overlaps its neighbour. */
.fc-scale { display: flex; align-items: center; gap: 2px; width: 100%; padding: 0 4px;
  font-family: var(--mono); font-size: var(--fs-mini); color: var(--dim); }
/* Three letters at 11px measure 20px, so the slot is 22px and the scale cell
   below is widened to match: at the old 19px every Spt, Fld and Hrd on the tab
   was clipped by a pixel, which reads as a font bug rather than a narrow box. */
.fc-sc-end { flex: none; width: 22px; text-align: center; }
.fc-scale .fc-sl-lead { flex: none; width: 32px; }
/* THE BUTTON IS THE TARGET; THE CIRCLE INSIDE IT IS THE INK.
   The beam's dots grow from spot to flood and the edge's go from a fuzzy ring
   to a crisp one, so each control is a small picture of what it sets (owner
   2026-08-18). His own caveat rules the construction: "the smallest should NOT
   be too small to click on or to see", and "we're trying to ADD clarity, not
   make it harder to use". So the DRAWN circle varies and the HIT AREA never
   does — every dot is the same 16px target whatever it looks like, which is
   why this is a pseudo-element and not a smaller button. */
.fc-dot { flex: none; width: 15px; height: 15px; padding: 0; border: 0;
  background: transparent; cursor: pointer; display: flex;
  align-items: center; justify-content: center; }
/* UNSELECTED IS LIGHTER THAN CHOSEN (owner 2026-08-18): the five are a scale
   and one of them is the answer. */
.fc-dot::before { content: ''; display: block; width: 10px; height: 10px;
  box-sizing: border-box; border-radius: 50%; border: 1px solid var(--line-soft); }
.fc-dot:hover:not(:disabled)::before { border-color: var(--accent); }
.fc-dot.on::before { background: var(--text-2); border-color: var(--text-2); }
.fc-dot:focus-visible { outline: 2px solid var(--accent); outline-offset: 0; border-radius: 5px; }
/* The middle of the Focus scale is a SQUARE (owner 2026-08-18). Same footprint
   as a dot so the five stay evenly spaced; only the corner radius changes. */
.fc-dot.fc-sq::before { border-radius: 2px; }
.fc-dot:disabled { cursor: default; }
/* BEAM: spot to flood. The smallest ink is 8px inside a 16px target. */
.fc-dot.fc-d0::before { width: 7px; height: 7px; }
.fc-dot.fc-d1::before { width: 9px; height: 9px; }
.fc-dot.fc-d2::before { width: 11px; height: 11px; }
.fc-dot.fc-d3::before { width: 13px; height: 13px; }
.fc-dot.fc-d4::before { width: 15px; height: 15px; }
/* EDGE: soft to hard. Blur softens the RING, which is the edge of the beam the
   control describes. Hard is left completely alone — a crisp circle is the
   reference the other four are read against.
   THE RING THICKENS AS IT BLURS, and that is the whole trick. Blur alone
   spreads a 1px line into three faint ones: looked at 4× it read as a fading
   dot rather than a soft edge, and the five stopped being a progression. Wider
   ink under a wider blur keeps the weight constant, so what changes is the
   SOFTNESS and nothing else. */
.fc-dot.fc-e0::before { border-width: 2.5px; filter: blur(1.4px); }
.fc-dot.fc-e1::before { border-width: 2px; filter: blur(0.9px); }
.fc-dot.fc-e2::before { border-width: 1.6px; filter: blur(0.55px); }
.fc-dot.fc-e3::before { border-width: 1.2px; filter: blur(0.25px); }
.fc-axis { gap: 2px; }
.fc-ax { flex: none; width: 20px; height: 18px; padding: 0; border-radius: 4px;
  border: 1px solid var(--line); background: transparent; color: var(--dim);
  font-size: var(--fs-small); line-height: 1; cursor: pointer; }
.fc-ax:hover:not(:disabled) { border-color: var(--accent); color: var(--text); }
.fc-ax.on { background: var(--panel2); border-color: var(--text-2); color: var(--text); }
.fc-ax:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.fc-ax:disabled { cursor: default; }

/* ---- the images cell ----------------------------------------------------
   What there is, at a glance; the gallery is a dialog. The thumbnails are
   CONTAINED in a fixed box, so a portrait phone photograph and a landscape
   camera still both fit whole — neither cropped nor stretched. */
.fc-imgs { padding: 0 4px; }
/* It reads as a control AT REST. It used to be transparent until hover, which
   left the only route to captions and removal invisible — the owner could not
   find it on his own tab. The resting treatment is the thumbnail's own, so the
   cell still reads as pictures rather than as a button with pictures in it. */
.fc-imgbtn { display: flex; align-items: center; gap: 3px; width: 100%; min-height: 26px;
  padding: 2px 4px; background: var(--panel2); border: 1px solid var(--line-soft); border-radius: 4px;
  color: var(--dim); font-size: var(--fs-mini); cursor: pointer; }
.fc-imgbtn:hover { border-color: var(--accent); color: var(--text); background: var(--surface-2); }
.fc-imgbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.fc-thumb { width: 22px; height: 22px; object-fit: contain; border-radius: 3px;
  background: var(--panel2); border: 1px solid var(--line-soft); }
.fc-imgmore { font-family: var(--mono); }
.fc-imgnone { font-size: var(--fs-small); }

/* ---- the gallery dialog -------------------------------------------------- */
.fci-modal { width: min(680px, 94vw); }
.fci-sub { margin: -4px 0 10px; font-family: var(--mono); font-size: var(--fs-small); }
.fci-list { display: flex; flex-direction: column; gap: 8px; max-height: 56vh; overflow-y: auto; }
.fci-file { display: flex; align-items: center; gap: 10px; padding: 6px;
  border: 1px solid var(--line-soft); border-radius: 6px; }
.fci-thumb { width: 72px; height: 54px; object-fit: contain; border-radius: 4px;
  background: var(--panel2); border: 1px solid var(--line-soft); }
.fci-meta { display: flex; flex-direction: column; gap: 3px; flex: 1; min-width: 0; }
.fci-cap { width: 100%; font-size: var(--fs-small); }
.fci-none { padding: 10px 2px; }
.fci-foot { display: flex; align-items: center; gap: 8px; margin-top: 12px; }

/* ==========================================================================
   FOCUS CHART — the sheet somebody carries up the ladder (owner 2026-08-18)
   --------------------------------------------------------------------------
   One device per border, in the house card grammar: the swept corner law
   (rounded upper-right and lower-left, squared the other two) and the black
   identity block, both shared with the VLAN card, the spec sheet and the rack
   sheets. It reuses `.vc-card` for the frame so the corners can never drift
   from the rest of the paperwork.

   PRINT INK ONLY. Everything here is black on white by design: these rules are
   scoped under `.rpt-page`, which is a printed document and never takes a
   screen voice.
   ========================================================================== */
.rpt-page .fcr-card { padding: 0 0 5px; margin-bottom: 11px; }

/* THE BAND: two identity blocks, Device ID upper left and Channel upper right,
   with nothing between them.

   AT THE DEVICE SPECIFICATION'S SIZE (owner 2026-08-18: "match the width of the
   Device Specifications"). 92px fixed, the swept 2px/10px corner, black with a
   gray caption — the same construction as `.spec-typebox` and `.bomr-typebox`,
   so one device's three documents carry the same mark. A FIXED width, not a
   min-width, for the same reason the spec box states: as a minimum, one long
   value would widen the box past the others and undo the match. It wraps
   instead, and the type steps down to fit (fcrBlkFit in app.js). */
/* THE SIDE GAPS MATCH THE TOP GAP (owner 2026-08-18) — the blocks sit as far
   from the frame's sides as from its top. The same ruling the VLAN card
   carries, and the same 10px. */
.rpt-page .fcr-band { display: flex; align-items: stretch; gap: 16px; padding: 10px 10px 4px; }
.rpt-page .fcr-blk { width: 132px; flex: none; background: #000; color: #fff;
  border-radius: 2px 10px 2px 10px; padding: 4px 8px; text-align: center;
  display: flex; flex-direction: column; }
.rpt-page .fcr-blk-r { margin-left: auto; }
.rpt-page .fcr-blk > span { font-size: 7px; text-transform: uppercase;
  letter-spacing: .08em; color: #bbb; line-height: 1.2; }
.rpt-page .fcr-blk .n { flex: 1; display: flex; align-items: center; justify-content: center;
  font-weight: 800; line-height: 1.05; font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere; }
/* The value is as large as it can be and still fit the block. 132px is sized
   for FIVE DIGITS at the full 32px (owner 2026-08-18: "Channel could be 5
   digits"), which is wider than the Device Specification's 92px because that
   box holds "A1" and this one holds a channel somebody calls across a room. */
.rpt-page .fcr-blk .n-xl { font-size: 32px; }
.rpt-page .fcr-blk .n-lg { font-size: 24px; }
.rpt-page .fcr-blk .n-md { font-size: 17px; }
.rpt-page .fcr-blk .n-sm { font-size: 12px; line-height: 1.15; }
.rpt-page .fcr-blk .n-xs { font-size: 10px; line-height: 1.15; }

/* LOCATION AND PURPOSE RIDE THE BAND, with pill labels, the way the spec sheet
   carries Manufacturer. Block, not inline-block: an inline-block pill sits ON a
   text line and the line's half-leading holds it below its row — the exact
   misalignment the owner pointed at on the spec sheet. */
.rpt-page .fcr-bandmid { flex: 1; min-width: 0; display: flex; flex-direction: column;
  justify-content: center; gap: 5px; }
.rpt-page .fcr-bandmid .pill-label { display: block; width: fit-content; margin: 0 0 1px; }
.rpt-page .fcr-pillv { font-size: 13px; font-weight: 700; line-height: 1.2; overflow-wrap: anywhere; }

/* THREE COLUMNS: what it is, what it is wearing, what it is plugged
   into (owner's grouping). Equal columns rather than content-sized, so the
   same fields land in the same place on every card and the reader's eye can
   go straight to the one it wants. */
/* COL_PAD and COL_GAP in focusRptHtml() derive the picture's width from these
   two numbers so it comes out exactly one column wide. Change either and change
   both, or the picture stops lining up with the section above it. */
.rpt-page .fcr-top { display: grid; grid-template-columns: 1fr 1fr 1fr; column-gap: 20px;
  padding: 5px 16px 3px; align-items: start; }
.rpt-page .fcr-kv { display: flex; justify-content: space-between; gap: 12px;
  border-bottom: 1px solid #eee; padding: 1.5px 0; font-size: 10.5px; }
.rpt-page .fcr-kv > span { color: #555; text-transform: uppercase; font-size: 9px;
  letter-spacing: .06em; padding-top: 2px; flex: none; max-width: 52%; }
.rpt-page .fcr-kv > strong { font-weight: 700; text-align: right; overflow-wrap: anywhere; }
.rpt-page .fcr-none { color: #999; }

/* WHAT A PERSON WROTE, FULL WIDTH, label above the words. A key/value row
   ranges its value right, which reads as a measurement rather than as a
   sentence, and leaves the ragged edge on the side that is harder to read
   down. */
.rpt-page .fcr-access { padding: 3px 16px 0; }
/* THE WORDS COME BEFORE THE READINGS THEY EXPLAIN, and are written for two or
   three lines each (owner 2026-08-18), so the pair holds its height whether a
   fixture has been described at length or not yet at all. Without a floor, a
   card with empty notes drew a different shape from the one beside it and the
   picture stretched to match, so no two cards on a sheet agreed. */
.rpt-page .fcr-state .fcr-said-b:first-child { padding-top: 0; }
.rpt-page .fcr-state .fcr-said-b > p { min-height: 2.6em; }
/* A GAP between the notes and the first reading (owner 2026-08-18, twice):
   what somebody wrote and what the instrument is set to are two different kinds
   of statement and must not run together. Padding rather than margin — the
   readings block's first child carries a border, and a margin there collapses
   into it and disappears, which is why the first attempt at 6px did not show. */
.rpt-page .fcr-readings { padding-top: 10px; }

/* Edge and Focus share a line: the two shortest readings, paired. */
/* THE FOUR READINGS, ON COLUMNS THAT ARE ACTUALLY COLUMNS (owner 2026-08-18:
   "align these items", then "stack AXIS directly above FOCUS"). Beam over Edge
   in the first column, Axis over Focus in the third, the beam's angle between
   them. It was two independent rows, each sizing its own label, and the dots
   started eight pixels apart; as a grid the alignment is structural and cannot
   drift when a label changes length. */
.rpt-page .fcr-rgrid { display: grid; grid-template-columns: minmax(0, 1fr) 30px minmax(0, 1fr);
  column-gap: 8px; row-gap: 2px; align-items: center;
  padding-bottom: 3px; border-bottom: 1px solid #eee; }
.rpt-page .fcr-rgrid .fcr-scale { border-bottom: 0; padding: 0; }
.rpt-page .fcr-said-b { border-bottom: 1px solid #eee; padding: 1.5px 0; }
.rpt-page .fcr-said-b > span { display: block; color: #555; text-transform: uppercase;
  font-size: 9px; letter-spacing: .06em; line-height: 1.25; }
.rpt-page .fcr-said-b > p { margin: 0; font-size: 11px; font-weight: 600; line-height: 1.3; }

/* THE PICTURE BESIDE THE DRAWN STATE. Two columns, and the picture is a fixed
   box with the image CONTAINED, so a portrait phone photograph and a landscape
   camera still both print whole rather than one cropped and one stretched. */
/* TWO COLUMNS (owner 2026-08-18): the picture, and everything about the focus
   itself. A grid rather than arithmetic — computing the picture's width from
   the padding and gutters landed 2px out on every paper, because the printable
   width the report is handed and the width a column resolves to are not the
   same number to the pixel. As a column it is exact by construction. */
.rpt-page .fcr-mid { display: grid; grid-template-columns: 1fr 1fr; column-gap: 20px;
  align-items: stretch; padding: 3px 16px 0; }
/* border-box, so the 1px frame is INSIDE the column width rather than 2px
   wider than it — the point of the size is that it lines up with the column
   above. */
/* A COLUMN WIDE, AND AS TALL AS WHAT SITS BESIDE IT. A fixed aspect ratio made
   the picture the tallest thing on the card and pushed every fixture onto a
   page of its own; stretched, it fills the row the readings already need and
   costs no extra height. The image is CONTAINED, so a portrait phone shot and
   a landscape camera still both print whole. */
.rpt-page .fcr-shot { box-sizing: border-box; min-height: 118px;
  border: 1px solid #bbb; border-radius: 2px 10px 2px 10px;
  display: flex; align-items: center; justify-content: center; overflow: hidden; background: #fafafa; }
.rpt-page .fcr-shot img { max-width: 100%; max-height: 100%; object-fit: contain; }
.rpt-page .fcr-noshot { font-size: 9px; text-transform: uppercase; letter-spacing: .06em; color: #999; }


/* Sf . + . Hd, drawn the way the paper charts draw it. A scale nobody has read
   has NO dot filled, which is a different statement from one read as dead
   centre — so an empty row still prints, and still says nothing. */
/* SIZED TO ITS COLUMN, AND THAT IS A BUDGET, NOT A PREFERENCE. At Letter
   portrait the column is 144pt; label 32 + two 17pt ends + five 10pt slots +
   seven 3pt gaps = 137. Growing the dots for the beam's grading took the row
   to 173 and it spilled over the axis beside it and off the sheet — caught by
   the owner, not by the sweep, because the sweep ran before the dots grew.
   Anything added to this row has to come out of the same 144. */
.rpt-page .fcr-scale { display: flex; align-items: center; gap: 3px; padding: 1.5px 0;
  border-bottom: 1px solid #eee; font-size: 10px; }
/* ONE label width and ONE end width for every scale on the card, Axis included.
   They are what the grid's columns are made of: give the paired scales narrower
   labels, as a first cut did, and Beam and Edge stop lining up. */
.rpt-page .fcr-sl { flex: none; width: 32px; color: #555; text-transform: uppercase;
  font-size: 9px; letter-spacing: .06em; }
.rpt-page .fcr-sends { flex: none; width: 17px; text-align: center; color: #555; font-size: 9px; }
/* A FIXED SLOT holding a circle, so the five stay evenly spaced whatever they
   draw. The beam's grow from spot to flood; the edge's soften from hard to
   soft. Both are gentler on paper than on screen — ink spreads, and a printed
   9px ring blurred as hard as a screen one reads as a smudge rather than as a
   soft edge. */
.rpt-page .fcr-dot { flex: none; width: 10px; height: 10px; display: flex;
  align-items: center; justify-content: center; }
/* UNSELECTED IS LIGHTER THAN CHOSEN (owner 2026-08-18). The five are a scale
   and one of them is the answer; drawn in the same ink, the reader has to find
   the filled one among four equals. */
.rpt-page .fcr-dot::before { content: ''; display: block; width: 7px; height: 7px;
  box-sizing: border-box; border: 1px solid #999; border-radius: 50%; }
.rpt-page .fcr-dot.on::before { background: #111; border-color: #111; }
/* The middle of the Focus scale is a square (owner 2026-08-18). */
.rpt-page .fcr-dot.fcr-sq::before { border-radius: 1px; }
.rpt-page .fcr-dot.fcr-d0::before { width: 5px; height: 5px; }
.rpt-page .fcr-dot.fcr-d1::before { width: 6px; height: 6px; }
.rpt-page .fcr-dot.fcr-d2::before { width: 7px; height: 7px; }
.rpt-page .fcr-dot.fcr-d3::before { width: 8.5px; height: 8.5px; }
.rpt-page .fcr-dot.fcr-d4::before { width: 10px; height: 10px; }
.rpt-page .fcr-dot.fcr-e0::before { border-width: 2px; filter: blur(0.8px); }
.rpt-page .fcr-dot.fcr-e1::before { border-width: 1.6px; filter: blur(0.5px); }
.rpt-page .fcr-dot.fcr-e2::before { border-width: 1.3px; filter: blur(0.3px); }
.rpt-page .fcr-dot.fcr-e3::before { border-width: 1.1px; filter: blur(0.15px); }
/* A FIXED SLOT, even when empty: without one the axis slides left on every
   fixture whose beam angle is not recorded, and no two cards on a sheet line
   up. */
/* A COLUMN OF ITS OWN, even when empty: without one the axis slides left on
   every fixture whose beam angle is not recorded, and no two cards on a sheet
   line up. */
.rpt-page .fcr-deg { text-align: right; font-size: 11px;
  font-weight: 800; font-variant-numeric: tabular-nums; }
/* Beam, its angle, and the Axis on one line. The beam scale takes the slack,
   the degree sits between the two, and the axis is only as wide as its four
   glyphs. */

.rpt-page .fcr-ax { flex: none; width: 13px; height: 12px; border: 1px solid #ccc; border-radius: 2px;
  text-align: center; line-height: 12px; font-size: 10px; color: #999; }
.rpt-page .fcr-ax.on { border-color: #111; color: #111; font-weight: 800; }

/* The four blades of the shutter barrel, as fields. Two by two, because Top
   over Bottom and Left beside Right is how the barrel is arranged.
   A PLAIN HEADING, not a pill (owner 2026-08-18). A pill is a badge, and this
   is a section title over four rows; it takes the same voice as every other
   label on the card. */
.rpt-page .fcr-cuts { padding-top: 4px; }
.rpt-page .fcr-cuth { display: block; color: #555; text-transform: uppercase;
  font-size: 9px; font-weight: 700; letter-spacing: .06em; padding-bottom: 1px;
  border-bottom: 1px solid #ddd; margin-bottom: 2px; }
.rpt-page .fcr-cutgrid { display: grid; grid-template-columns: 1fr 1fr; column-gap: 18px; }
.rpt-page .fcr-cut { display: flex; justify-content: space-between; gap: 10px;
  border-bottom: 1px solid #eee; padding: 1.5px 0; font-size: 10.5px; }
.rpt-page .fcr-cut > span { color: #555; text-transform: uppercase; font-size: 9px;
  letter-spacing: .06em; flex: none; }
.rpt-page .fcr-cut > strong { text-align: right; overflow-wrap: anywhere; }

/* THE NARROWEST SHEET THIS APP OFFERS IS A4 PORTRAIT, and the two-column
   sections were checked on it rather than assumed: a first cut of these rules
   collapsed the layout on `.paper-a5`, a paper size print-doc.js does not have
   (the list is Letter, Legal, Tabloid, A4, A3). A rule keyed to a class nothing
   ever carries is not a safeguard, it is a comment that looks like one. The
   picture is sized from the MEASURED content width instead, so it follows the
   sheet on every combination rather than needing a rule per paper. */

/* ---- documents a device is expected to hold ------------------------------ */
/* Three states on an empty card, and only one of them is a problem. Colour is
   spent on the exception, not the rule: a missing REQUIRED document reads warm,
   an excused one reads as settled, and a slot nobody demands looks exactly as
   it always did. */
.da-card.da-missing { border-color: var(--warn); }
.da-missing-name { color: var(--warn); font-weight: 600; }
.da-card.da-excused { opacity: .72; }
.da-card.da-excused .da-ico-empty { color: var(--faint); }

/* ---- QC Requirements ------------------------------------------------------ */
/* The same decision in three places, so one look. Ticked is the ordinary state
   and carries no decoration; what draws the eye is a box that is CLEAR. */
.dev-c.dev-chk .dev-qcreq { margin: 0 auto; display: block; }
.dev-qcreq:disabled { opacity: .45; }

/* On a card: small, beside the slot name, never competing with it. */
.da-label { display: flex; align-items: center; gap: 8px; }
.da-req { display: inline-flex; align-items: center; gap: 4px; margin-left: auto;
  font-size: var(--fs-micro); color: var(--faint); cursor: pointer; white-space: nowrap; }
.da-req input { margin: 0; }
.da-req input:disabled { opacity: .45; }
.da-req input:disabled + span { opacity: .6; }

/* THE PRODUCT NAME, BOLD (owner 2026-08-23: "easier to read for someone who has
   to drop hundreds of pdfs in the right place"). The code beside it is already
   a chip; this is the string the eye is actually hunting for. */
.da-ident { font-weight: 600; color: var(--text); }

/* In Device Category Definition: three boxes on the row, at the category level. */
.dt-reqs { display: inline-flex; align-items: center; gap: 12px; flex: none; padding: 0 10px; }
.dt-req { display: inline-flex; align-items: center; gap: 4px;
  font-size: var(--fs-micro); color: var(--dim); cursor: pointer; white-space: nowrap; }
.dt-req input { margin: 0; }
.ao-hcell.ao-hreq { flex: none; width: 240px; }

/* ---------------------------------------------------------------------------
   LIFE CYCLE — a device type's maintenance tasks and the things it uses up.
   A list, not a column, so the cell shows a count and opens this.
   --------------------------------------------------------------------------- */
.dev-lc { width: 100%; background: none; border: 0; color: var(--text); font: inherit;
  font-size: var(--fs-mini); text-align: left; padding: 2px 4px; cursor: pointer;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border-radius: 4px; }
.dev-lc:hover { background: var(--panel2); }
.dev-lc:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.dev-lc-when { color: var(--dim); }
.dev-lc-none { opacity: .35; }

.lc-pop { position: absolute; z-index: 70; width: 470px; max-width: calc(100vw - 16px);
  background: var(--panel); border: 1px solid var(--line-strong); border-radius: 10px;
  box-shadow: 0 12px 34px rgba(0,0,0,.34); padding: 10px; }
.lc-head { display: flex; flex-direction: column; gap: 1px; padding-bottom: 8px;
  margin-bottom: 8px; border-bottom: 1px solid var(--line-soft); }
.lc-head span { font-size: var(--fs-mini); color: var(--dim); }
.lc-rows { display: flex; flex-direction: column; gap: 6px; max-height: 46vh; overflow-y: auto; }
.lc-row { display: flex; align-items: center; gap: 6px; }
.lc-label { flex: 1; min-width: 0; }
.lc-every { display: flex; align-items: center; gap: 4px; flex: none; }
.lc-every-l { font-size: var(--fs-mini); color: var(--dim); }
.lc-n { width: 52px; text-align: right; }
.lc-u { width: 84px; }
.lc-x { flex: none; width: 24px; height: 24px; border-radius: 6px; border: 1px solid var(--line);
  background: var(--panel2); color: var(--dim); cursor: pointer; line-height: 1; }
.lc-x:hover { color: var(--bad); border-color: var(--bad); }
.lc-none { margin: 2px 0 6px; font-size: var(--fs-mini); color: var(--dim); }
.lc-add { display: flex; gap: 6px; margin-top: 9px; padding-top: 9px; border-top: 1px solid var(--line-soft); }
.lc-new { flex: 1; min-width: 0; }
/* Consumables rows carry a fourth control (the linked Consumable device), so
   their popup is wider and the select gets a fixed lane. */
/* READINESS: the project's dependency graph, drawn. SVG inherits the theme
   variables, so both themes come along without a second palette. */
.rg-svg { width: 100%; max-width: 980px; height: auto; display: block; margin: 6px 0 4px; }
.rg-node { cursor: pointer; }
.rg-node rect { fill: var(--panel2); stroke: var(--line-strong); stroke-width: 1; }
.rg-node:hover rect { stroke: var(--accent); }
.rg-node .rg-t { fill: var(--text); font-size: 13px; font-weight: 600; }
.rg-node .rg-w { fill: var(--dim); font-size: 10px; font-weight: 400; }
/* The Operations record lives in the other app: a solid node with a dashed
   ring, so it reads as real data arriving from elsewhere. */
.rg-node-ext rect { stroke-dasharray: 5 3; }
.rg-edge-ext { stroke-dasharray: 5 3; }
.rg-bad .rg-dot { fill: var(--bad); }
span.rg-dot.rg-bad { background: var(--bad); }
.rg-node .rg-dot { stroke: none; }
.rg-ok .rg-dot { fill: var(--ok); }
.rg-todo .rg-dot { fill: var(--warn); }
.rg-quiet .rg-dot { fill: var(--dim); }
.rg-edge { fill: none; stroke: var(--line-strong); stroke-width: 1.5; }
.rg-edge-derived { stroke-dasharray: 4 4; }
.rg-derived rect { fill: transparent; stroke: var(--line); stroke-dasharray: 3 3; }
.rg-derived { cursor: pointer; }
.rg-derived text { fill: var(--dim); font-size: 11px; }
.rg-derived .rg-w { font-size: 9px; }
.rg-derived:hover rect { stroke: var(--accent); }
/* The words under the picture: one row per station, clickable. */
.rg-rows { display: flex; flex-direction: column; gap: 2px; margin-top: 8px; }
.rg-row { display: flex; align-items: baseline; gap: 10px; text-align: left; border: none;
  background: transparent; color: var(--text); padding: 4px 6px; border-radius: 6px; cursor: pointer; }
.rg-row:hover { background: var(--panel2); }
.rg-row .rg-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; align-self: center; }
span.rg-dot.rg-ok { background: var(--ok); }
span.rg-dot.rg-todo { background: var(--warn); }
span.rg-dot.rg-quiet { background: var(--dim); }
.rg-row-l { font-weight: 600; flex: none; width: 150px; }
.rg-row-w { color: var(--dim); font-size: var(--fs-mini); flex: none; width: 230px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rg-row-s { color: var(--dim); font-size: var(--fs-body-s); }
.rg-legend { display: flex; flex-direction: column; gap: 3px; margin-top: 12px; font-size: var(--fs-mini); color: var(--dim); }
.rg-legend .rg-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
.rg-note { font-size: var(--fs-mini); margin: 8px 0 0; }
/* The backward reading at the top of the history drawer: what points at this
   row, before anybody changes it. */
.hist-usedby { margin: 0; padding: 6px 14px; font-size: var(--fs-mini); color: var(--dim);
  border-bottom: 1px solid var(--line-soft); }

/* An empty reference picker's jump: a quiet link-shaped control that names
   the tab where the missing thing is made. Sized to sit inside grid cells. */
.ref-jump { border: 1px dashed var(--line-strong); background: transparent; color: var(--dim);
  border-radius: 6px; padding: 2px 8px; font-size: var(--fs-mini); cursor: pointer;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ref-jump:hover { color: var(--accent); border-color: var(--accent); }
.ref-jump-mini { padding: 1px 6px; }
.lc-cons-none { color: var(--dim); font-size: var(--fs-mini); align-self: center; width: 150px; }
.lc-copy { margin-top: 7px; }
.lc-copy .lc-copyfrom { font-size: var(--fs-mini); color: var(--dim); max-width: 220px; }

.lc-pop-cons { width: 540px; }
.lc-cons { flex: none; width: 150px; }

/* The Device Category's default Kind: the picker sits between the category
   name and the QC boxes, sized so the longest label ("Power & distribution")
   is readable without pushing the QC band off the row. */
/* Width comes from the grid track now; see .dt-grid. */

/* QC bands: one per question, so turning checks on reads as a set of decisions
   rather than a list of fifteen. */
.qc-group { margin-bottom: 18px; }
/* The whole band is the control, not a chevron somebody has to aim at. */
.qc-group-h { display: flex; align-items: baseline; gap: 10px; width: 100%; text-align: left;
  background: none; border: 0; border-bottom: 1px solid var(--line-soft);
  font: inherit; font-size: var(--fs-mini);
  text-transform: uppercase; letter-spacing: .07em; font-weight: 600; color: var(--dim);
  padding: 0 2px 6px; margin-bottom: 8px; cursor: pointer; }
.qc-group-h:hover { color: var(--text); }
.qc-group-h:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.qc-chev { font-size: 9px; opacity: .8; width: 9px; flex: none; }
/* Indented under their band, so a heading reads as a heading. */
.qc-group-b { padding-left: 18px; display: flex; flex-direction: column; gap: 8px; }
.qc-group.shut { margin-bottom: 10px; }
.qc-group-n { text-transform: none; letter-spacing: 0; font-weight: 400; opacity: .7; }
.qc-set-u { font-family: var(--mono); }

/* ---------------------------------------------------------------------------
   LIFE CYCLE REPORTS (lcr-*, own prefix per the namespace rule)
   Built on the VLAN card family — the same band, the same continued heading —
   because they are read the same way: one subject per card, scanned down.
   --------------------------------------------------------------------------- */
/* THE SAME INSET THE OTHER CARDS HAVE (owner 2026-08-23: "adjust the gaps
   around the blocks to match the other reports"). .vc-card carries no side
   padding because the VLAN report's own band supplies it; the spec band does
   not, because the spec SHEET supplies it. Borrowing the band without the
   inset left every block flush against the card's border. */
.rpt-page .lcr-card { margin-bottom: 10px; padding: 8px 10px 9px; }
/* The Device Specification sheet's own identity band, reused. The photograph
   closes the band opposite the Device Type box: the same 92px square, the same
   sweep, so the row is bracketed by two blocks with the words between them
   (owner 2026-08-23). Always drawn, empty or not -- the frame is part of the
   form, the same ruling the spec sheet's own photo block carries. */
.rpt-page .lcr-band { margin-bottom: 6px; }
.rpt-page .lcr-photo { width: 92px; flex: none; align-self: stretch;
  border: 2px solid #111; border-radius: 2px 10px 2px 10px; background: #fff;
  overflow: hidden; display: flex; align-items: center; justify-content: center; }
.rpt-page .lcr-photo img { width: 100%; height: 100%; object-fit: contain; display: block; }

.rpt-page .lcr-table { width: 100%; border-collapse: collapse; font-size: 11px; margin-top: 4px; }
/* The "Where used" zone line on a consumable's card (SLS 3.8.3.g). */
.rpt-page .lcr-zones { font-size: 10px; margin-top: 3px; }
.rpt-page .lcr-table th { text-align: left; font-size: 8.5px; text-transform: uppercase; letter-spacing: .07em;
  color: #666; font-weight: 700; padding: 4px 6px; border-bottom: 1.5px solid #111; }
.rpt-page .lcr-table td { padding: 3.5px 6px; border-bottom: 1px solid #ddd; vertical-align: top; }
.rpt-page .lcr-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.rpt-page .lcr-table tr:last-child td { border-bottom: 0; }
/* The obsolescence sheet is one long table rather than cards, so its header
   repeats on every page the way a schedule's does. */
.rpt-page .lcr-wide thead { display: table-header-group; }

/* The one figure the maintenance sheet exists to produce. */
.rpt-page .lcr-foot { border: 2px solid #111; border-radius: 2px 14px 2px 14px; padding: 10px 14px; margin-top: 12px; }
.rpt-page .lcr-foot-n { display: flex; align-items: baseline; gap: 8px; margin-top: 2px; }
.rpt-page .lcr-foot-n b { font-size: 22px; font-weight: 800; }
.rpt-page .lcr-foot-n span { font-size: 11px; color: #444; }
.rpt-page .lcr-foot-note { margin: 4px 0 0; font-size: 9.5px; color: #666; }

/* Obsolescence and Limited-Life read as a statement rather than a row: one
   date and one plan, side by side, with room for the plan to be a sentence. */
.rpt-page .lcr-obs { display: flex; gap: 18px; align-items: flex-start; margin-top: 4px; }
/* Wide enough that the pill above it never wraps to a second line. */
.rpt-page .lcr-obs-year { flex: none; width: 130px; }
/* Sized like the Device Type code's neighbours rather than like a headline:
   at 26px the year shouted over the plan beside it, which is the part somebody
   has to act on (owner 2026-08-23: "the year is too large"). */
.rpt-page .lcr-obs-year b { display: block; font-size: 18px; font-weight: 800; line-height: 1.15; margin-top: 2px;
  font-variant-numeric: tabular-nums; }
.rpt-page .lcr-obs-plan { flex: 1; min-width: 0; }
.rpt-page .lcr-obs-plan p { margin: 2px 0 0; font-size: 11.5px; line-height: 1.35; }
.rpt-page .lcr-obs-plan em { color: #666; }
.rpt-page .lcr-obs-wide { flex: 1; }
/* Consumables Usage: the linked device's identity under the item name --
   procurement's line, quieter than the label above it. */
.rpt-page .lcr-conslink { font-size: 10.5px; color: #555; margin-top: 1px; }
/* Consumables List: the per-consumable facts (lead time, SDS, totals, and on
   the priced variant Estimated Cost) as one row of key-values over the table. */
.rpt-page .lcr-consfacts { display: flex; flex-wrap: wrap; gap: 4px 22px; margin: 6px 0 8px; }
.rpt-page .lcr-consfacts .vc-kv { display: flex; gap: 6px; align-items: baseline; }
.rpt-page .lcr-consfacts .vc-kv span { font-size: 9.5px; text-transform: uppercase; letter-spacing: .04em; color: #666; }
.rpt-page .lcr-consfacts .vc-kv strong { font-size: 11.5px; }

/* Facility Interface: five named sections of key-values, because the client
   asks for it "by element" with those headings. Two columns so a value has
   room to be a sentence; a single-column section for the ones that always are. */
.rpt-page .fi-why { display: flex; align-items: baseline; gap: 8px; margin: 2px 0 8px; font-size: 10.5px; }
/* TIGHT, like the specification sheet (owner 2026-08-23). Its rows are
   2.5px of padding at 11.5px type; ours were 5px of grid gap plus a pill
   carrying 8px of its own margin, which is most of a line per row. The pill's
   margin is overridden here rather than globally: it is right everywhere else.
   Two columns, so a value still has room to be a sentence. */
.rpt-page .fi-sec { margin-top: 6px; }
.rpt-page .fi-sec-h { font-size: 9px; text-transform: uppercase; letter-spacing: .08em; font-weight: 700;
  border-bottom: 1.5px solid #111; padding-bottom: 2px; margin-bottom: 2px; }
.rpt-page .fi-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 18px; }
.rpt-page .fi-grid-1 { grid-template-columns: 1fr; }
.rpt-page .fi-kv { min-width: 0; padding: 2.5px 0; border-bottom: 1px solid #ddd; }
.rpt-page .fi-kv .pill-label { margin: 0 0 1px; }
.rpt-page .fi-v { font-size: 11.5px; line-height: 1.3; }
/* The maintenance access value stands alone: the section heading already asks
   the question, so a second label under it says it twice. */
.rpt-page .fi-bare { padding: 3px 0 1px; }

/* The Element heading a facility interface is organised under: the spec asks
   for the spreadsheet "by element", and on a show lighting scope an Element is
   an area of the park, not a product. */
.rpt-page .fi-el { display: flex; align-items: baseline; gap: 10px; font-size: 13px; font-weight: 800;
  border-bottom: 2px solid #111; padding-bottom: 3px; margin: 10px 0 6px; }
.rpt-page .fi-el:first-child { margin-top: 0; }
.rpt-page .fi-el-n { margin-left: auto; font-size: 10px; font-weight: 400; color: #555; }
/* The instance this sheet is about, above the product it happens to be. */
.rpt-page .fi-inst { display: grid; grid-template-columns: 1fr 1.4fr 1.2fr; gap: 0 16px;
  border-bottom: 1px solid #ddd; padding-bottom: 4px; margin-bottom: 5px; }
.rpt-page .fi-inst .pill-label { margin: 0 0 1px; }
.rpt-page .fi-inst-v { font-size: 12px; font-weight: 600; }
.rpt-page .fi-inst-id .fi-inst-v { font-family: var(--mono); font-size: 13px; }

/* ---- Starting a project (owner 2026-08-24): the decision map -------------
   Green chip = change anytime; amber = the cost of changing grows once data
   is filed under it. The same chips ride the Levels and Device ID section
   headers, so the consequence is visible where the decision is made, not
   only on day one. */
.st-acts { display: flex; align-items: center; gap: 10px; margin: 4px 0 14px; flex-wrap: wrap; }
.st-rows { display: flex; flex-direction: column; }
.st-row { display: flex; align-items: center; gap: 12px; padding: 9px 4px; border-top: 1px solid var(--line-soft); }
.st-row:first-child { border-top: none; }
.st-dot { width: 20px; height: 20px; flex: none; border-radius: var(--r-ctl); border: 1.5px solid var(--line-strong);
  display: inline-flex; align-items: center; justify-content: center; font-size: 12px; color: var(--ok); }
.st-dot.done { border-color: var(--ok); }
.st-name { font-weight: 600; min-width: 150px; }
.st-what { color: var(--dim); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.st-chip { flex: none; font-size: var(--fs-small); padding: 2px 10px; border-radius: var(--r-ctl); white-space: nowrap; cursor: help; }
.st-chip.ok { color: var(--ok); background: color-mix(in srgb, var(--ok) 14%, transparent); }
.st-chip.warn { color: var(--warn, #d9a93f); background: color-mix(in srgb, var(--warn, #d9a93f) 14%, transparent); }
.ws-panel-h .st-chip { margin-left: 6px; vertical-align: 1px; }
.st-foot { margin: 12px 0 0; }
/* Copy Setup dialog rides the lc-pop furniture. */
.st-pop { width: 430px; }
.st-src { width: 100%; margin: 10px 0 12px; }
.st-picks { display: flex; flex-direction: column; gap: 7px; }
.st-pick { display: flex; align-items: center; gap: 9px; }
.st-pick.st-held { opacity: .55; }
.st-note { margin: 12px 0 4px; }

/* ---- BOM readability (owner 2026-08-24: "It's crowded") -------------------
   Scoped to .bom-lines (the BOM tab's own table): the other grids that share
   bom-* classes keep their left-rail actions. Columns stay aligned like a
   spreadsheet; hierarchy lives in a fixed-width GUTTER (category lines carry
   an indent rail) and in a quiet wash on sub-rows, never in shifted cells. */
.bom-lines .bom-gut { flex: none; width: 26px; align-self: stretch; }
.bom-lines .bom-gut-sub { border-left: 2px solid var(--line-strong); margin-left: 12px; width: 14px; }
.bom-lines .bom-sub-row { background: color-mix(in srgb, var(--surface-2) 45%, transparent); }
/* Actions ride the far right, quiet until the row is under the pointer or
   holds focus — the Device List's own pattern. */
.bom-lines .bom-actions { width: 84px; border-right: none; border-left: 1px solid var(--line-soft);
  justify-content: flex-end; opacity: 0; transition: opacity .12s; }
.bom-lines .bom-row:hover .bom-actions, .bom-lines .bom-row:focus-within .bom-actions { opacity: 1; }
.bom-lines .bom-head .bom-actions { border-left: none; }
/* The category header aligns to the rail it owns and breathes a little. */
.bom-lines .bom-cathdr { padding: 5px 8px 5px 14px; }
.bom-lines .bom-cathdr .bom-catname { letter-spacing: .02em; }

/* BOM readability, rounds two + three (owner 2026-08-24): the folder tabs
   share ONE width so the panel spine is straight; the tab is TWO lines —
   code + the Device List's description, then category left and the parts
   count right-aligned (money just before it); ＋ Line sits OUTSIDE the tab.
   Sub-rows indent inside the FIRST column, so every other column stays
   spreadsheet-aligned. */
.bom-lines .bom-devpanel .bom-band { width: max-content; min-width: 0; background: transparent;
  border: none; padding: 0; align-items: flex-end; gap: 10px; }
.bom-lines .bom-tab { width: 560px; max-width: 100%; background: var(--surface-3);
  border-radius: 7px 7px 0 0; padding: 6px 12px 5px; display: flex; flex-direction: column; gap: 1px; }
.bom-lines .bom-tab-l1 { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
.bom-lines .bom-devdesc { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bom-lines .bom-tab-l2 { display: flex; align-items: baseline; gap: 12px; font-size: var(--fs-small); }
.bom-lines .bom-tab-cat { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bom-lines .bom-tab-l2 .bom-devtotal { margin-left: 0; flex: none; }
.bom-lines .bom-tab-l2 .bom-count { flex: none; }
.bom-lines .bom-band .bom-add { margin-bottom: 4px; }
.bom-lines .bom-sub-row > .bom-c:first-of-type { padding-left: 22px; }
.bom-lines .bom-cathdr { padding-left: 26px; }

/* BOM round four (owner 2026-08-24): the Device Type reads first, the
   category HEADER sits at Primary Component's level, and the repeated
   per-line category text steps in beneath it, quieter — header names the
   group, lines defer to it. */
.bom-lines .bom-tab .bom-devcode { font-size: 16px; line-height: 1.15; }
.bom-lines .bom-cathdr { padding-left: 10px; }
.bom-lines .bom-sub-row > .bom-c:first-of-type { padding-left: 34px; }
.bom-lines .bom-sub-row > .bom-c:first-of-type .bom-f { color: var(--dim); }

/* Round five (owner 2026-08-24): "Primary Component and Software are at the
   same level. They should be the same font style." The category header name
   takes Primary Component's exact voice (sans, 600, text-2) instead of the
   mono band style; its line count stays muted beside it. */
/* var(--sans) EXPLICITLY, not inherit: .bom-cathdr itself is set mono with
   the other bands (line ~3197), so inherit kept the mono face — the exact
   mismatch the owner screenshotted twice. */
.bom-lines .bom-cathdr .bom-catname { font-family: var(--sans); font-weight: 600;  color: var(--text-2); font-size: var(--fs-small); }


/* ---- Acceptance Testing (TESTING tab, owner 2026-08-26) ------------------ */
/* Hierarchy is drawn with fill and INDENT, never borders: heading rows sit on
   the nested surface and fold; sections step in once; Physical/Operational
   sub-heads and their items step in again. */
.tst-wrap { display: flex; flex-direction: column; gap: 12px; }
.tst-camps { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.tst-camp { border-radius: var(--r-ctl); }
.tst-camp.active { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.tst-addbtn { border-style: dashed; color: var(--dim); }
.tst-statusseg { display: inline-flex; background: var(--surface-2); border-radius: var(--r-ctl); padding: 3px; gap: 2px; }
.tst-statusseg .tst-status { border: 0; background: transparent; color: var(--dim); border-radius: var(--r-ctl); padding: 4px 12px; }
.tst-statusseg .tst-status.on { background: var(--accent); color: var(--on-accent); }
.tst-figs { display: flex; gap: 18px; align-items: center; flex-wrap: wrap; padding: 2px 4px; }
.tst-fig b { display: block; font: 600 17px/1.1 var(--mono); color: var(--text-strong); font-variant-numeric: tabular-nums; }
.tst-fig small { font: 10.5px/1.5 var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--faint); }
.tst-fig.ok b { color: var(--ok); } .tst-fig.warn b { color: var(--warn); } .tst-fig.bad b { color: var(--bad); }
.tst-fig.dim b { color: var(--faint); }
.tst-frozen { font: 11px/1 var(--mono); letter-spacing: .06em; color: var(--warn);
  background: color-mix(in srgb, var(--warn) 12%, var(--surface-2)); border-radius: var(--r-ctl); padding: 5px 12px; }
.tst-h3 { display: flex; align-items: center; gap: 10px; }
.tst-h3 .tst-tools { margin-left: auto; display: flex; gap: 8px; }
.tst-startrow { display: flex; gap: 10px; margin-top: 10px; }

.tst-sessrow { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; }
.tst-sess { background: var(--surface-2); border-radius: var(--r-card); padding: 10px 12px;
  border-top: 1px solid var(--edge-hi-soft); display: flex; flex-direction: column; gap: 8px; }
.tst-sess.stamping { background: color-mix(in srgb, var(--accent) 8%, var(--surface-2)); }
.tst-sesshead { display: flex; gap: 8px; align-items: center; }
.tst-sid { font: 11px/1 var(--mono); color: var(--text-2); background: var(--surface-3); border-radius: var(--r-ctl); padding: 4px 9px; flex: none; }
.tst-sloc { flex: 1; min-width: 0; font-weight: 600; }
.tst-sessgrid { display: flex; flex-direction: column; gap: 4px; }
.tst-sessgrid label { display: flex; align-items: center; gap: 8px; font-size: var(--fs-small); color: var(--dim); }
.tst-dr { display: flex; gap: 6px; flex: 1; }
.tst-dr input { flex: 1; min-width: 0; }
.tst-hrs { width: 76px; }
.tst-ppl { display: flex; flex-direction: column; gap: 3px; }
.tst-p { display: flex; gap: 6px; align-items: center; }
.tst-p input { flex: 1; min-width: 0; }
.tst-porg { color: var(--dim); }
.tst-pmv { display: flex; gap: 1px; opacity: 0; transition: opacity .12s; }
.tst-p:hover .tst-pmv { opacity: 1; }
.tst-pmv button { border: 0; background: transparent; color: var(--faint); padding: 2px 4px; cursor: pointer; }
.tst-pmv button:hover { color: var(--text); }
.tst-pmv button:disabled { opacity: .3; }
.tst-sessfoot { display: flex; align-items: center; gap: 8px; margin-top: auto; }
.tst-stampinto { font-size: var(--fs-small); color: var(--dim); display: flex; align-items: center; gap: 6px; }
.tst-sadd { border: 1px dashed var(--line-strong); border-radius: var(--r-card); background: transparent;
  color: var(--dim); min-height: 90px; }

.tst-head { margin-top: 8px; }
.tst-fold { display: flex; width: 100%; align-items: center; gap: 10px; text-align: left;
  background: var(--surface-2); border: 0; border-radius: var(--r-ctl); padding: 7px 14px; cursor: pointer; }
.tst-fold:hover { background: var(--surface-3); }
.tst-tri { color: var(--faint); font-size: 10px; }
.tst-headname { font: 11px/1.3 var(--mono); letter-spacing: .1em; text-transform: uppercase; color: var(--text-2); }
.tst-headcount { margin-left: auto; font: 10.5px/1 var(--mono); color: var(--faint); font-variant-numeric: tabular-nums; }
.tst-headcount .bad { color: var(--bad); }
.tst-sec { font-size: var(--fs-small); font-weight: 600; color: var(--text-2); padding: 8px 4px 2px 22px; }
.tst-run { font: 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; color: var(--faint); padding: 5px 4px 2px 38px; }
.tst-item { display: flex; gap: 8px; align-items: center; padding: 2px 4px 2px 54px; }
.tst-item.deferred .tst-itext { color: var(--faint); }
.tst-itext { flex: 1; min-width: 220px; }
.tst-inote { width: 24%; min-width: 140px; color: var(--dim); }
.tst-stamp { font: 10px/1.4 var(--mono); color: var(--faint); white-space: nowrap; min-width: 150px; }
.tst-faulttag { font: 9.5px/1 var(--mono); letter-spacing: .07em; text-transform: uppercase; color: var(--warn);
  background: color-mix(in srgb, var(--warn) 12%, var(--surface-2)); border-radius: var(--r-ctl); padding: 3px 8px; flex: none; }
.tst-defertag { font: 10px/1 var(--mono); letter-spacing: .05em; color: var(--warn);
  background: color-mix(in srgb, var(--warn) 12%, var(--surface-2)); border-radius: var(--r-ctl); padding: 4px 10px; white-space: nowrap; }
.tst-faultrow { display: flex; gap: 14px; padding: 0 4px 4px 70px; }
.tst-faultrow label { display: flex; align-items: center; gap: 6px; flex: 1; font-size: var(--fs-micro); color: var(--faint); }
.tst-faultrow input { flex: 1; min-width: 0; font-size: var(--fs-small); }
.tst-res { display: inline-flex; background: var(--surface-2); border-radius: var(--r-ctl); padding: 2px; gap: 1px; flex: none; }
.tst-res button { border: 0; background: transparent; font: 10px/1 var(--mono); color: var(--faint);
  border-radius: var(--r-ctl); padding: 4px 8px; cursor: pointer; }
.tst-res button:hover:not(:disabled) { color: var(--text); }
.tst-res .tst-r-pass.on { background: color-mix(in srgb, var(--ok) 20%, var(--surface-2)); color: var(--ok); }
.tst-res .tst-r-fail.on { background: color-mix(in srgb, var(--bad) 20%, var(--surface-2)); color: var(--bad); }
.tst-res .tst-r-cond.on { background: color-mix(in srgb, var(--warn) 20%, var(--surface-2)); color: var(--warn); }
.tst-res .tst-r-na.on { background: var(--surface-3); color: var(--text-2); }
.tst-units { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; padding: 2px 4px 8px 70px; }
.tst-unit { border: 0; font: 10.5px/1 var(--mono); border-radius: var(--r-ctl); padding: 5px 10px; cursor: pointer;
  background: var(--surface-3); color: var(--faint); }
.tst-u-pass { background: color-mix(in srgb, var(--ok) 16%, var(--surface-2)); color: var(--ok); }
.tst-u-fail { background: color-mix(in srgb, var(--bad) 18%, var(--surface-2)); color: var(--bad); }
.tst-u-cond { background: color-mix(in srgb, var(--warn) 16%, var(--surface-2)); color: var(--warn); }
.tst-u-na { background: var(--surface-3); color: var(--text-2); }
.tst-pu, .tst-defer, .tst-undefer { font-size: var(--fs-micro); }

/* ---- the printed sheets (.rpt-page grammar) ------------------------------ */
.rpt-page .tstr-band { display: flex; gap: 16px; align-items: stretch; min-height: 80px; margin: 10px 0 6px; }
.rpt-page .tstr-typebox { background: #000; color: #fff; border-radius: 2px 10px 2px 10px; padding: 4px 8px;
  width: 92px; flex: none; text-align: center; display: flex; flex-direction: column; }
.rpt-page .tstr-typebox span { font-size: 7px; text-transform: uppercase; letter-spacing: .08em; color: #bbb; }
.rpt-page .tstr-typebox .code { flex: 1; display: flex; align-items: center; justify-content: center; font-size: 28px; font-weight: 800; }
.rpt-page .tstr-typebox .cat { font-size: 7.5px; text-transform: uppercase; letter-spacing: .04em; color: #ccc; line-height: 1.2; }
.rpt-page .tstr-mid { flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: space-between; gap: 2px; }
.rpt-page .tstr-mid .pill-label { display: block; width: fit-content; margin: 0 0 1px; }
.rpt-page .tstr-midrow { display: flex; gap: 26px; flex-wrap: wrap; }
.rpt-page .tstr-v { font-size: 10.5px; line-height: 1.3; font-weight: 600; }
.rpt-page .tstr-statbox { position: relative; border: 2px solid #111; border-radius: 2px 10px 2px 10px;
  width: 96px; flex: none; padding: 2px 6px 4px; text-align: center; display: flex; flex-direction: column; }
.rpt-page .tstr-statbox span { font-size: 7px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; color: #666; }
.rpt-page .tstr-statbox .qty { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-size: 15px; font-weight: 800; font-variant-numeric: tabular-nums; }
.rpt-page .tstr-progress { font-size: 10px; color: #555; margin: 2px 0 0; }
.rpt-page .tstr-h2 { margin-top: 16px; }
.rpt-page .tstr-h2 .tstr-dim { font-weight: 400; font-size: 10.5px; color: #555; }
.rpt-page .tstr-tbl { margin-top: 6px; }
.rpt-page .tstr-tbl tr { break-inside: avoid; }
.rpt-page .tstr-h td { background: #e4e8eb; font-size: 9.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; }
.rpt-page .tstr-s td { background: #f3f5f6; font-weight: 700; font-size: 10.5px; padding-left: 14px; }
.rpt-page .tstr-r td { background: #fafbfb; font-size: 9px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .08em; color: #666; padding-left: 22px; }
.rpt-page .tstr-tbl td:first-child { padding-left: 22px; }
.rpt-page .tstr-h td:first-child, .rpt-page .tstr-s td:first-child { padding-left: 7px; }
.rpt-page .tstr-s + tr td:first-child, .rpt-page .tstr-follow td:first-child { padding-left: 7px; }
.rpt-page .tstr-pill { display: inline-block; font-size: 8.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .06em; border-radius: var(--r-ctl); padding: 2px 9px; white-space: nowrap; }
.rpt-page .tstr-pass { background: #e2f3e9; color: #1e7a48; }
.rpt-page .tstr-fail { background: #fde3e3; color: #c22e30; }
.rpt-page .tstr-cond { background: #fdeed7; color: #b06a12; }
.rpt-page .tstr-na { background: #eceff1; color: #5c6870; }
.rpt-page .tstr-open { background: #fff; color: #98a2ab; box-shadow: inset 0 0 0 1px #c6ccd1; }
.rpt-page .tstr-defer { background: #e8edf4; color: #32627e; }
.rpt-page .tstr-by { font-family: var(--mono); font-size: 8.5px; color: #666; white-space: nowrap; }
.rpt-page .tstr-fault { font-size: 9px; color: #555; margin-top: 2px; }
.rpt-page .tstr-units { margin-top: 3px; display: flex; flex-wrap: wrap; gap: 3px; }
.rpt-page .tstr-u { display: inline-block; font-family: var(--mono); font-size: 8px; border-radius: var(--r-ctl);
  padding: 1.5px 7px; background: #eceff1; color: #5c6870; }
.rpt-page .tstr-u.tstr-pass { background: #e2f3e9; color: #1e7a48; }
.rpt-page .tstr-u.tstr-fail { background: #fde3e3; color: #c22e30; }
.rpt-page .tstr-u.tstr-cond { background: #fdeed7; color: #b06a12; }
.rpt-page .tstr-u.tstr-open { background: #fff; box-shadow: inset 0 0 0 1px #d8dde1; color: #98a2ab; }
.rpt-page .tstr-blank { min-width: 60px; }
.rpt-page .tstr-note { font-size: 9.5px; color: #555; margin-top: 8px; }

/* ---- Testing readability pass (owner 2026-08-26, first read) ------------- */
/* The rows were "a little hard to read": every input drew its own pill and
   the page read as an outline farm. Item and note inputs now sit QUIET on
   the row (no border, no fill) and earn their outline on hover and focus;
   the row itself answers hover with the nested surface. */
.tst-item { padding-top: 3px; padding-bottom: 3px; border-radius: var(--r-soft); }
.tst-item:hover { background: var(--surface-2); }
.tst-item .tst-itext, .tst-item .tst-inote {
  background: transparent; border-color: transparent; }
.tst-item:hover .tst-itext, .tst-item:hover .tst-inote { border-color: var(--line); }
.tst-item .tst-itext:focus, .tst-item .tst-inote:focus {
  background: var(--surface-2); border-color: var(--accent); }
/* Three fold levels, one grammar: heading, Components, Physical/Operational.
   Buttons, reset flat; hierarchy is drawn by indent alone. */
.tst-run, .tst-grp, .tst-sec.tst-fold2 { border: 0; background: transparent; width: 100%;
  text-align: left; display: flex; align-items: center; gap: 8px; cursor: pointer; }
/* Lifted categories (owner 2026-08-26): in the General and Processes bands
   the sections ARE the first level, so they sit at heading depth and their
   items step in one. */
.tst-sec.lift { padding-left: 8px; }
.tst-run.lite { padding-left: 22px; }
.tst-item.lite { padding-left: 38px; }
.tst-faultrow.lite { padding-left: 54px; }
.tst-units.lite { padding-left: 54px; }
.tst-grp { font-size: var(--fs-small); font-weight: 600; color: var(--text-2);
  padding: 8px 4px 2px 22px; }
.tst-grp:hover, .tst-run:hover { color: var(--text); }
.tst-runcount { font: 10px/1 var(--mono); color: var(--faint); font-variant-numeric: tabular-nums; }
.tst-sec.deep { padding-left: 38px; }
.tst-run.deep { padding-left: 54px; }
.tst-item.deep { padding-left: 70px; }
.tst-faultrow.deep { padding-left: 86px; }
.tst-units.deep { padding-left: 86px; }
/* Printed sheets: component sections indent under their Components band. */
.rpt-page .tstr-compband td { padding-left: 14px; }
.rpt-page .tstr-s.tstr-comp td { padding-left: 22px; }
.rpt-page .tstr-comp + tr td:first-child { padding-left: 7px; }

/* The band tier (owner 2026-08-26): General / Equipment / Processes above the
   headings. Stronger voice than a heading, still fill-and-indent only. */
.tst-band { border: 0; background: transparent; width: 100%; text-align: left;
  display: flex; align-items: center; gap: 10px; cursor: pointer;
  font: 12.5px/1 var(--mono); letter-spacing: .14em; text-transform: uppercase;
  color: var(--text-strong); padding: 14px 4px 4px; }
.tst-band:hover { color: var(--accent); }
.rpt-page .tstr-bandttl { display: block; min-height: 0; margin: 14px 0 2px;
  font-size: 13px; letter-spacing: .04em; text-transform: uppercase;
  border-bottom: 1.5px solid #111; padding-bottom: 2px; }

/* ---- Testing hierarchy pass (owner 2026-08-26, screenshot round) --------- */
/* "The device label SR01 looks to be the most important thing on the page."
   The tiers, loudest first: the BAND titles the page; each rack is a
   collapsing CARD; sections and runs live inside on indent alone. The
   checklist sits on the page ground (no outer panel), so a card never
   nests inside a card. */
.tst-cklist { display: flex; flex-direction: column; gap: 4px; }
.tst-cklist .tst-h3 { font: 12px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase;
  color: var(--text-2); margin: 0 0 6px; padding-bottom: 8px; border-bottom: 1px solid var(--line); }
.tst-band { font: 700 15px/1.2 var(--sans); letter-spacing: .02em; text-transform: none;
  color: var(--text-strong); padding: 18px 4px 6px; }
.tst-band .tst-runcount { font-size: 11px; }
.tst-card { background: var(--surface-1); border-radius: var(--r-card);
  border-top: 1px solid var(--edge-hi-soft); margin: 4px 0; }
.tst-cardhead { border: 0; background: transparent; width: 100%; text-align: left;
  display: flex; align-items: center; gap: 10px; cursor: pointer; padding: 11px 14px; }
.tst-cardhead:hover { background: var(--surface-2); border-radius: var(--r-card); }
.tst-cardname { font: 600 13.5px/1.2 var(--sans); color: var(--text-strong); }
.tst-cardbody { padding: 2px 10px 10px; }
/* The result pills STACK in one column whatever the indent (owner: "Pass /
   Fail / Conditional / N/A columns should be stacked. Currently, they
   shift."): the row is a grid, the text absorbs the indent, and every
   control column is fixed. */
.tst-item { display: grid; grid-template-columns: minmax(200px, 1fr) 176px minmax(140px, 300px) 150px 168px;
  gap: 8px; align-items: center; }
.tst-tcell { display: flex; gap: 8px; align-items: center; min-width: 0; }
.tst-tcell .tst-itext { flex: 1; min-width: 0; }
.tst-item .tst-inote { width: auto; min-width: 0; }
.tst-item .tst-stamp { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.tst-item .row-tools { justify-self: end; }
.tst-res { justify-self: start; }
.tst-defertag { max-width: 176px; overflow: hidden; text-overflow: ellipsis; }

/* ---- Testing calm pass (owner 2026-08-26: "It might be used in a stressful
   situation. Let's be sure the form isn't adding to the confusion.") ------- */
/* One logical row = item + its fault facts + its unit chips, wrapped so the
   hairline and the hover cover all of it. The hairline is what lets the eye
   carry a long row from its words to its pills without drifting a line. */
.tst-row { border-bottom: 1px solid var(--line-soft); }
.tst-cardbody .tst-row:last-child { border-bottom: 0; }
.tst-row:hover, .tst-row:focus-within { background: var(--surface-2); }
.tst-item:hover { background: transparent; }
.tst-row:hover .tst-itext, .tst-row:hover .tst-inote { border-color: var(--line); }
/* The occasional acts (per unit, defer, remove) stay quiet until the row is
   the one being worked; the frequent act (the result) is always drawn.
   focus-within keeps them reachable by keyboard. */
.tst-item .row-tools { opacity: 0; transition: opacity .12s; display: flex;
  flex-wrap: nowrap; gap: 6px; align-items: center; }
.tst-row:hover .row-tools, .tst-row:focus-within .row-tools { opacity: 1; }
/* Room for "per unit · defer · remove" on ONE line: the tools column grows
   and never wraps (owner: "right side choices force devices onto two rows"). */
.tst-item { grid-template-columns: minmax(200px, 1fr) 176px minmax(140px, 300px) 150px 216px; }
/* The FAULT tag leads the label, clear of the result pills. */
.tst-tcell .tst-faulttag { flex: none; }
/* Fault facts: each on its own indented line, label right-aligned to a
   fixed rail so the two inputs align. */
.tst-faultrow { display: flex; gap: 10px; align-items: center; padding-top: 1px; padding-bottom: 3px; }
.tst-flbl { font: 10px/1.4 var(--mono); letter-spacing: .06em; text-transform: uppercase;
  color: var(--faint); width: 118px; flex: none; text-align: right; }
.tst-faultrow input { flex: 1; min-width: 0; max-width: 560px; font-size: var(--fs-small);
  background: transparent; border-color: transparent; }
.tst-row:hover .tst-faultrow input { border-color: var(--line); }
.tst-faultrow input:focus { background: var(--surface-2); border-color: var(--accent); }
/* Placeholders whisper. */
.tst-if::placeholder, .tst-sf::placeholder, .tst-pf::placeholder { color: var(--faint); }

/* Building codes (owner 2026-08-26): the zone cell is a preview that opens a
   multiline editor. Shaped like the inputs beside it; the empty state names
   itself the way a placeholder does. */
.zn-attr-long { display: inline-flex; align-items: center; gap: 4px; max-width: 220px;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  border: 1px solid var(--line); border-radius: var(--r-ctl); background: transparent;
  color: var(--text); font-size: var(--fs-small); padding: 4px 12px; cursor: pointer; }
.zn-attr-long:hover { background: var(--surface-2); }
.zn-attr-long.empty { color: var(--faint); }
.zn-attr-long small { color: var(--faint); }
.zn-attr-long.ro { border-color: transparent; cursor: default; }

/* ---- Cable catalog (SLS Table 1-4) ---------------------------------------
   ONE grid per band, heading and rows together, for the reason the Device
   Categories table is built that way: two content-sized tracks can only agree
   on a width if every row is measured in the same grid. Cable rows carry a
   maximum length and a terminations list; terminations and surge devices do
   not, so they use the short template. */
/* Description was taking the lion's share and left the notes cramped (owner
   2026-08-28). It now grows at the same rate as Notes rather than half again
   faster, and the part number keeps a real floor because the ordering codes
   are long ("FLWLCLCAD-JEFxxx"). */
.cb-table { display: grid; gap: 6px 8px; align-items: center;
  grid-template-columns: 120px minmax(180px, 1fr) 110px minmax(170px, 1fr) 72px 92px minmax(150px, 1fr) minmax(180px, 1fr) max-content; }
/* The short template is SIX columns, and its rows now emit six cells. It was
   seven against nine: the rows carried a length, a by-length box and a
   terminations slot that a termination cannot have, so every row started two
   columns further along than the row above it. A grid does not complain about
   that; it just staircases (owner 2026-08-28). */
.cb-table:has(.cb-grid-short) { grid-template-columns: 120px minmax(180px, 1fr) 110px minmax(170px, 1fr) minmax(180px, 1fr) max-content; }
.cb-grid { display: contents; }
.cb-head > span { color: var(--dim); font-size: var(--fs-mini); text-transform: uppercase;
  letter-spacing: .04em; align-self: end; }
.cb-row .cb-f { height: 32px; box-sizing: border-box; width: 100%; min-width: 0;
  padding: 4px 10px; font-size: var(--fs-body-s); }
.cb-desig { display: flex; align-items: center; gap: 5px; min-width: 0; }
.cb-desig .cb-d { font-family: var(--mono); font-weight: 600; }
/* Terminations are a LIST, and it has to look like one: chips that can be
   removed one at a time, and a picker offering only what is left. A single text
   box held the same data and told nobody it could hold more than one value. */
.cb-terms { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; min-width: 0; }
.cb-chip { display: inline-flex; align-items: center; gap: 2px; font-family: var(--mono);
  font-size: var(--fs-small); background: var(--panel2); border: 1px solid var(--line);
  border-radius: var(--r-ctl); padding: 1px 4px 1px 8px; }
.cb-chipx { border: none; background: none; color: var(--dim); cursor: pointer;
  padding: 0 3px; font-size: 13px; line-height: 1; }
.cb-chipx:hover { color: var(--bad); }
.cb-termadd { font-size: var(--fs-small); padding: 2px 4px; max-width: 84px; }
.cb-none { font-size: var(--fs-small); }
/* The length-code answer, in a column so every row states it. */
.cb-chk { display: flex; justify-content: center; }
.cb-chk input { margin: 0; }
.cb-btns { display: flex; align-items: center; gap: 6px; }
.cb-del { border: none; background: none; color: var(--dim); cursor: pointer; padding: 2px 6px; }
.cb-del:hover { color: var(--bad); }
.cb-band { margin-bottom: 18px; }
/* THE THREE SECTIONS READ AS ONE THING WITH THREE PARTS, and they fold the way
   everything else in the suite folds: `.vw-band` + `.vw-gbody`, the grouped-
   band shape from lib-views-ui, with the section at depth 0 and any grouping
   inside it at depth 1. This tab had grown its own underlined full-width
   header instead, which is the generic look the house style exists to avoid
   (owner 2026-08-28: "The collapsable parts don't match how we do it other
   places... the style looks like standard AI apps"). Only the section's own
   weight is added here; every other line comes from the shared rules. */
.cb-sec { margin-top: 14px; }
.cb-sec strong { font-size: var(--fs-body); }

/* A COLUMN GRID DOES NOT INDENT ITS ROWS. The suite's grouped bands indent
   the body one step per level (style.css .vw-gbody: a margin, a rail and a
   padding), which is right for a list of cards and wrong for any grid whose
   heading is a separate row: every cell under a band moved 24px right per
   level while the heading stayed put, and at two levels a checkbox sat under
   the next column's name (owner 2026-09-18: "columns lose alignment with
   their headings. please fix this..... again"). In a scrolling grid the BAND
   still indents, which is what says what is under what; the rows under it
   sit in their columns. */
.ws-scroll .vw-gbody { margin-left: 0; padding-left: 0; border-left: 0; }

/* ---- Cable rules ----------------------------------------------------------
   The sentences that print at the end of a cable document. One grid, heading
   and rows together, for the reason the catalog is built that way: two
   content-sized tracks agree on a width only when they are measured in the
   same grid. Five cells per row, five columns, and a test that says so. */
.cr-table { display: grid; gap: 6px 8px; align-items: start;
  grid-template-columns: 46px minmax(280px, 1fr) 170px 130px max-content; }
.cr-grid { display: contents; }
.cr-head > span { color: var(--dim); font-size: var(--fs-mini); text-transform: uppercase;
  letter-spacing: .04em; align-self: end; }
/* A rule is a SENTENCE, so its box is a sentence's shape: it wraps, it grows
   downward, and it never becomes a pill (the shape law: only controls that
   hold one short value are pill-shaped). */
.cr-row .cr-text { width: 100%; min-width: 0; resize: vertical; min-height: 46px;
  border-radius: var(--r-soft); padding: 6px 10px; font-size: var(--fs-body-s);
  line-height: 1.4; font-family: inherit; }
.cr-row .cr-f:not(.cr-text) { height: 32px; box-sizing: border-box; width: 100%; min-width: 0;
  padding: 4px 10px; font-size: var(--fs-body-s); }
.cr-row .cr-scope { width: 100%; min-width: 0; height: 32px; font-size: var(--fs-body-s); }
.cr-chk { display: flex; justify-content: center; padding-top: 8px; }
.cr-chk input { margin: 0; }
.cr-btns { display: flex; align-items: center; gap: 2px; padding-top: 4px; }
.cr-btns button { border: none; background: none; color: var(--dim); cursor: pointer; padding: 2px 5px; }
.cr-btns button:hover:not(:disabled) { color: var(--text); }
.cr-btns .cr-del:hover { color: var(--bad); }
.cr-btns button:disabled { opacity: .3; cursor: default; }
.cr-band { margin-bottom: 18px; }

/* The printed rules: a numbered list per category, the source set quietly to
   the right of the sentence it belongs to. Nothing is added that nobody asked
   for -- no "generated by", no counts. */
.crp-cat { margin: 10px 0 4px; font-size: 11pt; font-weight: 700; }
.crp-list { margin: 0 0 8px; padding-left: 20px; }
.crp-list li { margin: 0 0 3px; line-height: 1.35; }
.crp-src { color: #555; font-size: 8.5pt; margin-left: 6px; white-space: nowrap; }
.crp-scope { color: #555; font-size: 8.5pt; margin-left: 6px; }

/* ---- Marking up a document (owner 2026-08-28) -----------------------------
   A full-window workbench, because annotating a datasheet is a session, not a
   glance: the page is as large as the screen allows and everything else gets
   out of the way. There is no Save button anywhere in here on purpose — every
   mark is written as it is drawn. */
body.mk-locked { overflow: hidden; }
.mk-wrap { position: fixed; inset: 0; z-index: 900; background: var(--bg);
  display: flex; flex-direction: column; }
.mk-bar { display: flex; align-items: center; gap: 14px; padding: 8px 14px;
  border-bottom: 1px solid var(--line); background: var(--panel); flex-wrap: wrap; }
.mk-id { font-weight: 600; }
.mk-toolset { display: flex; gap: 4px; }
.mk-tool { font-size: var(--fs-small); padding: 4px 11px; border-radius: var(--r-ctl);
  border: 1px solid var(--line); background: var(--panel2); color: var(--dim); cursor: pointer; }
.mk-tool:hover { color: var(--text); }
.mk-tool.on { border-color: var(--accent); color: var(--accent); background: var(--accent-soft, transparent); font-weight: 600; }
.mk-nav, .mk-zoom { display: flex; align-items: center; gap: 6px; font-size: var(--fs-small); color: var(--dim); }
.mk-pg { border: 1px solid var(--line); background: var(--panel2); color: var(--text);
  border-radius: var(--r-ctl); padding: 2px 9px; cursor: pointer; font-size: var(--fs-small); }
.mk-pg:disabled { opacity: .35; cursor: default; }
.mk-pgno, .mk-zval { font-variant-numeric: tabular-nums; }
/* The one thing on this screen that answers "is my work safe?", so it is a
   plain sentence rather than an icon, and it never says "saved" while a write
   is still in flight. */
.mk-status { margin-left: auto; font-size: var(--fs-small); color: var(--dim); }
.mk-status.mk-saving { color: var(--accent); }
.mk-status.mk-failed { color: var(--bad); font-weight: 600; }
.mk-stale { padding: 7px 14px; background: color-mix(in srgb, var(--warn) 12%, var(--panel));
  border-bottom: 1px solid var(--line); font-size: var(--fs-small); color: var(--text); }
.mk-stage { flex: 1; overflow: auto; display: grid; place-items: center; padding: 24px; }
.mk-page { position: relative; box-shadow: 0 10px 40px rgba(0,0,0,.35); background: #fff; }
.mk-page canvas { display: block; }
/* THE CURSOR SAYS WHAT WILL HAPPEN (owner 2026-08-28). A crosshair means "a
   drag here draws"; in Select it is the ordinary pointer, and over a mark it is
   an open hand that closes while dragging. A crosshair everywhere told somebody
   about to move a rectangle that they were about to draw another one. */
.mk-layer { position: absolute; inset: 0; cursor: default; touch-action: none; }
.mk-layer[data-tool="rect"], .mk-layer[data-tool="underline"] { cursor: crosshair; }
.mk-layer[data-tool="note"] { cursor: cell; }
.mk-wrap.mk-grabbing, .mk-wrap.mk-grabbing * { cursor: grabbing !important; }
.mk-loading { color: var(--dim); }

/* A mark. Red, outlined, never filled — it has to survive being printed in
   black and white, and the specification wants circled or underlined, never
   highlighted. */
.mk-m { position: absolute; cursor: grab; }
/* A handle for each corner a kind can be pulled by, drawn ON the edge so the
   thing under your finger is the thing that moves. */
.mk-h { position: absolute; width: 11px; height: 11px; border-radius: 2px;
  background: var(--panel); border: 1.5px solid var(--accent); box-sizing: border-box; }
.mk-h-nw { left: -6px; top: -6px; cursor: nwse-resize; }
.mk-h-ne { right: -6px; top: -6px; cursor: nesw-resize; }
.mk-h-se { right: -6px; bottom: -6px; cursor: nwse-resize; }
.mk-h-sw { left: -6px; bottom: -6px; cursor: nesw-resize; }
.mk-h-w { left: -6px; bottom: -6px; cursor: ew-resize; }
.mk-h-e { right: -6px; bottom: -6px; cursor: ew-resize; }
/* The weight is set per mark and written inline, in points of the finished
   page: these only supply the color and the style. */
.mk-m-rect { border: 1.5px solid #c62828; border-radius: 2px; }
.mk-m-underline { border-bottom: 1.5px solid #c62828; }
/* A STRIKE IS A LINE ACROSS ITS BOX, on whichever diagonal it was drawn. CSS
   has an edge for every side of a rectangle and none for its diagonal, so the
   box carries an SVG and the box itself is invisible. */
.mk-m-strike > svg { width: 100%; height: 100%; display: block; overflow: visible; }
/* A COVER HIDES WHAT IS UNDER IT, so it is filled and has no outline of its
   own. Black because that is what a reader reads as "deliberately taken out" —
   a white box would look like the datasheet was always blank there, and this
   one is honest about having been done on purpose. */
.mk-m-cover { background: #000; }
/* While it is being drawn the fill would hide the page the author is aiming
   at, so the ghost is an outline like every other. */
.mk-m-cover.mk-ghosting { background: rgba(0, 0, 0, .35); border: 1.5px dashed #c62828; }
.mk-pen { display: flex; gap: 4px; }
/* A note is a BOX of the author's chosen width, and its text wraps inside it.
   The size is set inline, in points of the page times the zoom, so the lines
   break on screen where they break on paper. */
/* Size, padding, gap and the dot all come from LTCMarkup.MK_NOTE as inline
   styles, so this file cannot disagree with the PDF drawer about them. What is
   left here is colour, shape and behaviour.
   BORDER-BOX because the stored width is the box's OUTER width — what somebody
   dragged the edge to — and the wrapping width is derived from it identically
   on both sides. */
.mk-m-note { display: flex; align-items: center; transform: translateY(-50%); }
.mk-note-dot { border-radius: 50%; background: #c62828; flex: none; }

/* TWO DOWNLOAD BUTTONS, ON THE ANNOTATE LINE (owner 2026-08-28). They were one
   segmented pill, which held them together but was wide enough to push the row
   onto a second line. Short labels and the arrow doing the work of the verb fit
   all three controls across; a.small.ghost in style.css already gives them the
   shape, so only the icon is set here. */
.da-dl .da-dlico { margin: 0 -1px 0 -1px; opacity: .7; flex: none; }
.da-dl:hover { color: var(--accent); }
.da-dl:hover .da-dlico { opacity: 1; }
/* A DELIBERATE BREAK, so the row never wraps somewhere arbitrary. What you do
   WITH the document sits on the first line; what CHANGES the document sits on
   the second. Left to itself the row broke between Replace and Remove, which
   grouped nothing and left one button stranded. */
.da-actbreak { flex-basis: 100%; height: 0; margin: 0; }
/* The note IS the editor. Selected, it takes a caret; the weight of its border
   is the pen, set inline like every other mark's. */
.mk-note-text { background: #fff; color: #111; border: 1px solid #c62828; border-radius: var(--r-soft);
  box-sizing: border-box; white-space: pre-wrap; overflow-wrap: anywhere; }
.mk-note-text.mk-editing { outline: 2px solid var(--accent); outline-offset: 2px; cursor: text;
  caret-color: #c62828; }
.mk-note-text.mk-editing:empty::before { content: 'Type the note'; color: #999; }
.mk-note-dot { cursor: grab; }
.mk-hint { font-size: 11px; color: var(--dim); background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-ctl); padding: 2px 8px; white-space: nowrap; }
/* The width handle, on the edge it changes. Only on the selected note, so a
   page of notes is not a page of handles. */
.mk-note-grip { width: 8px; align-self: stretch; margin-left: -3px; cursor: ew-resize;
  border-right: 3px solid var(--accent); border-radius: 2px; flex: none; }
.mk-m-sel { outline: 2px solid var(--accent); outline-offset: 2px; }
.mk-ghosting { border-style: dashed; pointer-events: none; }
/* A mark from a release this one has not caught up with: shown where it is,
   plainly not ours to edit, and never dropped. */
.mk-m-unknown { border: 1.5px dotted var(--dim); border-radius: 2px; background: rgba(128,128,128,.1);
  cursor: help; }
/* A mark drawn on an earlier version of this file: shown, never moved for you. */
.mk-m-stale { border-color: var(--warn); border-style: dashed; }
.mk-m-stale.mk-m-note .mk-note-dot { background: var(--warn); }
.mk-seltools { position: absolute; display: flex; gap: 6px; align-items: center; z-index: 2; }
.mk-notein { font-size: 13px; padding: 4px 8px; border-radius: var(--r-soft); line-height: 1.35;
  border: 1px solid var(--accent); background: var(--panel); color: var(--text);
  min-width: 260px; resize: vertical; font-family: inherit; }
.mk-del { font-size: 11px; padding: 3px 9px; border-radius: var(--r-ctl); cursor: pointer;
  border: 1px solid var(--line); background: var(--panel); color: var(--bad); }

/* ---- Manufacturers & Reps: the project roster (2026-08-29) ----------------
   Derived from the Device Types tab; what a person does here is DECIDE. */
.mfr-table td { vertical-align: top; }
.mfr-note { max-width: 34ch; white-space: normal; }
/* Taken from the manufacturer's own record and overridable here — the same
   blue the rest of the app uses for a borrowed value. */
.mfr-borrowed { color: var(--calc); }
/* A DECISION, not an absence. It has to look different from an empty cell,
   because "we chose not to name one" and "there is nobody" are the two things
   this column exists to tell apart. */
.mfr-none { color: var(--dim); font-style: italic; }
.mfr-switch { display: inline-flex; align-items: center; gap: 8px; }
.mfr-foot { margin: 10px 2px 0; max-width: 80ch; }

/* ---- the Manufacturers & Reps report --------------------------------------
   A company block: identity, address, contacts, people. Drawn the same way
   for a manufacturer and for a rep, at two sizes, because they are the same
   block — the rep's is simply smaller and labelled. */
.cob-id { display: flex; align-items: flex-start; gap: 10px; }
.cob-logo { max-height: 34px; max-width: 120px; object-fit: contain; }
.cob-name { font-weight: 650; font-size: 15px; }
.cob-brand { font-size: var(--fs-small); color: var(--dim); }
.cob-addr { margin-top: 8px; line-height: 1.45; }
.cob-rows { margin-top: 8px; display: grid; grid-template-columns: auto 1fr; gap: 2px 10px; }
.cob-row { display: contents; }
.cob-k { color: var(--faint); font-size: var(--fs-small); }
.cob-people { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 12px; }
.cob-person { min-width: 18ch; }
.cob-title { color: var(--dim); font-size: var(--fs-small); margin-left: 6px; }
.cob-small .cob-name { font-size: 13px; }
.cob-small .cob-logo { max-height: 24px; max-width: 88px; }

/* The reps sit UNDER the manufacturer, side by side. Three blocks degrade to
   two and to one without the page looking like something failed to load. */
.mfr-card .mfr-reps { display: grid; grid-template-columns: 1fr 1fr;
  gap: 12px; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); }
.mfr-cardnote { margin-top: 12px; }
.mfr-cardnote p { margin: 4px 0 0; max-width: 80ch; }
/* Address on the left, contacts and people on the right: a company block is
   short and the page is wide, and a single column wastes half the sheet. */
.cob-body { display: grid; grid-template-columns: minmax(18ch, 1fr) 1.4fr; gap: 8px 28px; margin-top: 8px; }
.cob-body .cob-addr, .cob-body .cob-rows { margin-top: 0; }
@media (max-width: 620px) { .cob-body { grid-template-columns: 1fr; } }
/* A REPRESENTATIVE'S BLOCK STACKS. It has half the page beside its twin, and
   half of half is a column too narrow for a support address (owner
   2026-08-29). The manufacturer keeps its two columns: it has the full width. */
.cob-small .cob-body { grid-template-columns: 1fr; gap: 6px; }
/* The rep slots and the note are edited in the cell, so the controls sit quiet
   until touched: a table of 16 rows with 48 boxed inputs reads as a form, not
   a list. Same reasoning as the borrowed-cell inputs elsewhere. */
.mfr-table .mfr-slot, .mfr-table .mfr-noteb {
  width: 100%; font: inherit; font-size: var(--fs-small); color: var(--text);
  background: transparent; border: 1px solid transparent; border-radius: 5px;
  padding: 3px 5px; }
.mfr-table .mfr-slot:hover, .mfr-table .mfr-noteb:hover { border-color: var(--line); }
.mfr-table .mfr-slot:focus, .mfr-table .mfr-noteb:focus {
  outline: none; border-color: var(--accent); background: var(--panel); }
.mfr-table .mfr-noteb::placeholder { color: var(--faint); }

/* ---- a cell whose save was refused ---------------------------------------
 *
 * Owner 2026-08-30: *"agreed."* The typed text stays, because losing what
 * somebody typed is the worse of the two failures. What changes is that the
 * cell stops looking like a stored value.
 *
 * IT DRAWS IN A CHANNEL NOTHING ELSE USES, and that took two attempts.
 *
 * Every other mark a grid cell can carry is a box-shadow ring: `dev-code-dup`
 * (red, a duplicate Device ID), `dev-code-badtype` (amber, a letter naming no
 * category) and the focus ring itself. The first version of this rule used a
 * ring too, and lost — both of those rings are `!important`, so a refused save
 * on a cell that was ALSO invalid showed the amber ring and no sign that
 * nothing had been stored. Verified in the browser, not reasoned about: a
 * refused edit of a device code is exactly the case where both apply.
 *
 * So the mark is a background gradient: a 2px rule along the bottom edge, where
 * no other rule draws. It composes with all three rings instead of fighting
 * them, and a cell can now say "this letter names no category" and "and it was
 * not saved" at the same time, which is the truth.
 *
 * Deliberately quieter than `dev-code-dup`: a duplicate Device ID is a fault in
 * the data; this is a value that has not landed yet.
 */
.ws-unsaved,
.dev-cell.ws-unsaved,
.dev-a.ws-unsaved,
input.ws-unsaved,
select.ws-unsaved,
textarea.ws-unsaved {
  background-image: linear-gradient(to top, var(--bad) 0 2px, transparent 2px);
  background-color: color-mix(in srgb, var(--bad) 9%, transparent);
}
/* Standing IN the refused cell is the likeliest case, so the tint sits over the
   focus fill rather than replacing it, and the focus ring is left alone. */
.dev-cell.ws-unsaved:focus,
.dev-a.ws-unsaved:focus,
input.ws-unsaved:focus,
select.ws-unsaved:focus,
textarea.ws-unsaved:focus {
  background-color: color-mix(in srgb, var(--bad) 9%, var(--panel2));
}

/* ---- columns people add themselves --------------------------------------
   An added column looks like every other column, because it is one. What
   marks it is the menu button in its heading, and that only appears on the
   ones somebody added: the built-in headings are unchanged. */
.uf-head { display: inline-flex; align-items: center; gap: 4px; }
/* The label gives way, never the button: a heading too narrow for both should
   truncate the name rather than push the menu out of reach. */
.uf-head .uf-lbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  min-width: 0; flex: 1 1 auto; }
/* A REAL TARGET, not a decoration. This was three characters at 11px with two
   pixels of padding, which is under 20px square and misses as often as it hits.
   Owner, trying it 2026-09-06: "the ... is too small to click". 26px square is
   still modest beside a 140px column and is a size a finger can find. */
.uf-menu-btn { appearance: none; background: transparent; border: 0; color: var(--dim);
  font: inherit; line-height: 1; cursor: pointer; border-radius: 4px;
  min-width: 26px; height: 26px; display: inline-flex; align-items: center;
  justify-content: center; flex: none; font-size: 15px; margin-right: 2px; }
.uf-menu-btn:hover { color: var(--text); background: var(--surface-3); }
.uf-menu-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.uf-menu-wrap { position: fixed; inset: 0; z-index: 60; }
.uf-menu { position: fixed; min-width: 200px; max-width: 260px;
  max-height: calc(100vh - 16px); overflow-y: auto; background: var(--surface-2);
  border: 1px solid var(--line-strong); border-radius: var(--r-card); padding: 5px;
  box-shadow: 0 10px 30px rgba(0,0,0,.35); display: flex; flex-direction: column; }
.uf-menu button { appearance: none; background: transparent; border: 0; color: var(--text);
  font: inherit; font-size: var(--fs-body-s); text-align: left; padding: 7px 10px;
  border-radius: 5px; cursor: pointer; }
.uf-menu button:hover { background: var(--surface-3); }
.uf-menu button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.uf-menu-foot { border-top: 1px solid var(--line); margin-top: 4px; padding: 7px 10px 3px;
  font-size: var(--fs-small); color: var(--dim); }

.uf-bar { display: inline-flex; align-items: center; gap: 8px; margin-left: 10px; }
/* A value the server refused. The cell keeps what was typed rather than
   reverting it: that text is somebody's work, and silently replacing it is how
   the correction gets lost along with the mistake. */
.uf-in.uf-bad { outline: 1px solid var(--bad); background: color-mix(in srgb, var(--bad) 10%, transparent); }

.uf-away { display: flex; flex-direction: column; gap: 6px; margin: 10px 0 4px; }
.uf-away-row { display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 7px 10px; background: var(--surface-2); border-radius: var(--r-card); }

/* Dragging an added column. The marker sits on the heading the cursor is over,
   on the side the column would land, so what you see is the answer rather than
   a guess. */
/* touch-action: none on the handle, or a touch drag becomes a page scroll
   before the gesture is ever seen. Only the handle suppresses the browser's
   own behaviour; doing it wider would be the text-selection bug again. */
.uf-head.uf-drag { cursor: grab; touch-action: none; }
.uf-head.uf-dragging { opacity: .45; cursor: grabbing; }
/* The marker sits on whatever heading the pointer is over, added or built-in:
   an added column may land anywhere along the grid, so every heading has to be
   able to show that it is the one being aimed at.

   TWO LANDINGS, TWO MARKS (owner 2026-09-07: "there should be a different
   indicator for dropping a column INTO a group vs before or after"). They are
   genuinely different outcomes -- one leaves the column folding away with
   nothing, the other signs it up to fold away with a band from then on -- so
   they are drawn differently, and the second one names the band by lighting it
   up in the strip above. */

/* BETWEEN: a plain rule on the edge it will land against. */
.bom-head .uf-drop-out-before { box-shadow: inset 3px 0 0 0 var(--accent); }
.bom-head .uf-drop-out-after { box-shadow: inset -3px 0 0 0 var(--accent); }

/* INTO A BAND: the same edge, and the heading tinted, so the column reads as
   going INSIDE something rather than beside it. */
.bom-head .uf-drop-in-before,
.bom-head .uf-drop-in-after {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border-radius: 3px;
}
.bom-head .uf-drop-in-before { box-shadow: inset 3px 0 0 0 var(--accent); }
.bom-head .uf-drop-in-after { box-shadow: inset -3px 0 0 0 var(--accent); }

/* And the band it would join says so, which is the part that answers "into
   WHAT". A collapsed band lights up the same way. */
.sched-bandhead .uf-band-target {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  box-shadow: inset 0 0 0 2px var(--accent);
  border-radius: 4px;
}
/* The grip keeps the right-hand edge: a drag that starts there is a resize. */
.uf-head .dev-grip { cursor: col-resize; }

/* ---- the Columns window ---------------------------------------------------
   Every column the grid can draw, in the order it draws them. It exists to show
   what the grid cannot: the hidden ones, and the ones folded inside a shut
   band. A tall list on a 103 column grid, so it scrolls inside the dialog
   rather than pushing the buttons off the bottom of the screen. */
/* THE WHOLE DIALOG IS THE LIST, so it takes the height the screen allows rather
   than a polite fraction of it. The enclosing .modal caps at 84vh and scrolls,
   which on a 42 row list meant scrolling a dialog INSIDE a scrolling list
   (owner 2026-09-07: "can this window be re-sizable to allow for a taller view
   of the columns?"). The dialog is now a column: heading and footer hold their
   height, the list takes the rest, and it is draggable taller or shorter from
   its bottom edge for anybody who wants a different balance. */
/* THE RESIZE HANDLE MUST NOT FIGHT THE FLEX BOX.
   `resize: vertical` sets an inline height on the list; `flex: 1 1 auto` inside
   a dialog capped at 84vh then shrinks it back; the handle is still down, so it
   sets the height again. That is a layout loop, and the page stops answering
   while the pointer is anywhere near the bottom of the window -- which is
   exactly where the handle is (owner 2026-09-08: "the failure is occurring
   before I even click on Close. Just when i drag my mouse to the lower part of
   the pop-up").
   `flex: none` ends it: the list is the height it is given, whether that came
   from the default or from somebody dragging, and nothing argues back. The
   dialog scrolls if a person drags it taller than the screen, rather than
   clipping its own buttons. */
.cw-modal { max-width: 460px; display: flex; flex-direction: column;
  max-height: 84vh; overflow-y: auto; }
.cw-list { flex: none; height: min(62vh, 560px); min-height: 140px; max-height: none;
  resize: vertical; overflow-y: auto; margin: 12px 0 4px;
  border: 1px solid var(--line); border-radius: var(--r-card); background: var(--surface-2); }
.cw-band + .cw-band { border-top: 1px solid var(--line); }
.cw-band-hd { display: flex; align-items: center; gap: 8px; padding: 7px 12px;
  font-size: var(--fs-small); font-weight: 600; color: var(--dim);
  text-transform: uppercase; letter-spacing: .06em;
  position: sticky; top: 0; background: var(--surface-3); }
.cw-row { display: flex; align-items: center; gap: 8px; padding: 5px 12px; }
.cw-grip { width: 12px; color: var(--dim); cursor: grab; touch-action: none; user-select: none;
  font-size: var(--fs-small); flex: none; }
.cw-row.cw-dragging { opacity: .45; }
.cw-row.cw-dragging .cw-grip { cursor: grabbing; }
/* The line sits on the edge the column would land against, the same grammar the
   header drag uses on the grid. */
.cw-drop-before { box-shadow: inset 0 2px 0 0 var(--accent); }
.cw-drop-after { box-shadow: inset 0 -2px 0 0 var(--accent); }
.cw-pick { display: flex; align-items: center; gap: 8px; cursor: pointer; flex: 1; min-width: 0; }
.cw-pick span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cw-pick input[disabled] { cursor: default; }
.cw-off .cw-pick span { color: var(--dim); }
.cw-tag { font-size: var(--fs-small); color: var(--dim); white-space: nowrap; }
/* The band controls. Only drawn for somebody who may use them, so they need no
   disabled state: absent, not greyed, which is this app's rule. */
.cw-band-hd { gap: 8px; }
.cw-band-lbl { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.cw-band-tools { display: inline-flex; gap: 4px; margin-left: auto; }
.cw-bandbtn { appearance: none; background: transparent; border: 1px solid var(--line);
  color: var(--dim); border-radius: 3px; padding: 0 6px; font: inherit;
  font-size: var(--fs-small); text-transform: none; letter-spacing: 0; cursor: pointer;
  min-height: 20px; }
.cw-bandbtn:hover { color: var(--text); border-color: var(--line-strong); }
.cw-bandbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Starting a band is a per-column action, so it hides until the row is under
   the pointer or the keyboard: 113 of them all shouting would be unreadable. */
.cw-split { opacity: 0; margin-left: auto; }
.cw-row:hover .cw-split, .cw-split:focus-visible { opacity: 1; }
.cw-tag.cw-lock { border: 1px solid var(--line); border-radius: 3px; padding: 0 5px; }
.cw-foot { margin-top: 10px; }

/* ---- the photograph viewer (lib-imgview.js, owner 2026-09-09) -------------
   A survey photograph exists to be examined, and the side panel is sized for
   the notes beside it. This is the same picture with the room to read it.

   z-index sits above the report window (120) because it opens from inside a
   panel that may already be over one, and below nothing else: it is the
   frontmost thing in the app while it is open. */
.iv-wrap { position: fixed; inset: 0; z-index: 200; display: flex; flex-direction: column;
  background: rgba(6, 9, 12, .93); outline: none; }
.iv-bar { display: flex; align-items: center; gap: 8px; padding: 10px 14px;
  border-bottom: 1px solid var(--line); background: var(--panel); flex: none; }
.iv-title { font-weight: 600; max-width: 46ch; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
/* Tabular figures so the percentage does not jitter the toolbar as it counts. */
.iv-pct { color: var(--dim); font-size: var(--fs-small); font-variant-numeric: tabular-nums;
  min-width: 5ch; }
.iv-spacer { flex: 1; }
.iv-stage { flex: 1; position: relative; overflow: hidden; min-height: 0;
  display: block; touch-action: none; }
/* The cursor is the affordance, and only when there is somewhere to drag TO. */
.iv-stage.iv-grab { cursor: grab; }
.iv-stage.iv-grabbing { cursor: grabbing; }
.iv-img { position: absolute; top: 0; left: 0; transform-origin: 0 0;
  image-orientation: from-image; user-select: none; -webkit-user-drag: none;
  max-width: none; max-height: none; }
/* THE MAP. Bottom right, out of the way of the toolbar, small enough to ignore
   and large enough to read. It appears only when part of the picture is off
   screen -- at fit the box would be the whole thumbnail and say nothing. */
.iv-nav { position: absolute; right: 16px; bottom: 16px; width: 168px;
  border: 1px solid var(--line); border-radius: var(--r-soft); overflow: hidden;
  background: var(--panel); box-shadow: 0 10px 30px rgba(0, 0, 0, .55); cursor: crosshair;
  line-height: 0; }
.iv-nav-img { width: 100%; height: auto; display: block; opacity: .65; user-select: none;
  -webkit-user-drag: none; }
.iv-nav-box { position: absolute; border: 2px solid var(--accent); background: rgba(255, 255, 255, .10);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .55); pointer-events: none; }
.iv-hint { position: absolute; left: 16px; bottom: 16px; margin: 0; color: var(--dim);
  font-size: var(--fs-small); background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-soft); padding: 5px 9px; }
@media (max-width: 700px) {
  .iv-nav { width: 108px; right: 10px; bottom: 10px; }
  .iv-hint { display: none; }
  .iv-title { max-width: 18ch; }
}

/* The side-panel photograph says it opens, rather than leaving it to be
   discovered. A clip keeps its own click, which is play and pause. */
.sc-detail-media .sc-openable { cursor: zoom-in; }
.sc-detail-media .sc-openable:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* A photograph whose file is gone says so, in the middle of the frame where
   the picture would have been (2026-09-09). */
.iv-gone { position: absolute; inset: 0; margin: 0; display: flex; align-items: center;
  justify-content: center; text-align: center; padding: 0 12%; color: var(--text-2);
  font-size: var(--fs-small); line-height: 1.5; }
