/* ============================================================================
   UV.CSS — what this module adds, and nothing else.
   ----------------------------------------------------------------------------
   Four stylesheets load before this one and none of them is edited here:

     theme.css   copied byte-for-byte from the live Pulse frontend — the tokens
     nordic.css  copied byte-for-byte — the .cd-* component library
     tokens.css  this module's :root, which re-points the names those two read
     shell.css   the 60px header + 264px left panel + mobile dock, copied from
                 mt3prototypes/hr/pulse-v2.css (itself goods-in v2), hr- → uv-

   So every button is a .cd-btn, every badge a .cd-badge, every table a
   .cd-table, every modal a .cd-modal, every form control a .cd-form-*. This
   file holds only what this module genuinely adds — the components no other
   Pulse module needs, because no other module renders a press plate:

     the pocket map · the QA five-pill strip · the release/confidence line ·
     the conservation rail · the build stage timeline · the wizard rail ·
     the bucket fill bar · the touch layer

   THE TOUCH LAYER IS THE REASON MOST SIZES HERE ARE BIGGER THAN PULSE'S.
   Half the sessions on these screens are a gloved finger on a wall panel
   beside a flatbed press. --tap (48px) is a floor, not a suggestion, and
   nothing in this file hides information behind :hover — every hover state
   has a tap or focus equivalent, because a touch screen has no hover.
   ========================================================================== */

/* ---- 1. THERE IS NO LONGER A READABILITY LAYER, AND THAT IS THE POINT -----

   This section used to hold twelve rules forking Pulse's type scale under
   [data-density="comfortable"], plus two more setting an unconditional 48px
   floor on .cd-btn. All fourteen are gone, and nothing replaced them.

   They existed because nordic.css hardcodes font-size on .cd-table td/th,
   .cd-btn and .cd-badge and reads no type token at all, so a scoped override
   was the only way this module's scale could reach Pulse's components. Since
   30 Aug this module owns its component layer (css/app.css re-implements every
   .cd-* name at our token values, procurement's position), so the sizes are
   set in tokens.css and arrive at the components directly.

   IT WAS ALSO SILENTLY BROKEN, which is worth recording rather than deleting.
   `[data-density="comfortable"] .cd-btn { min-height: var(--h-ctl) }` is an
   attribute selector plus a class; the unconditional `.cd-btn { min-height:
   var(--tap) }` below it was a class alone. The density rule out-specified the
   floor, so every button in the module rendered at 40px and every .cd-btn-sm
   at 34 — under the 48px touch floor this module's whole case rests on, on
   every page, including the one the audit reads first. Nobody would have found
   that by reading either rule; it needed the two together.

   The lesson is in tokens.css at --h-ctl: a rule enforced by an override can
   lose a cascade fight. A rule carried in the token cannot.
   -------------------------------------------------------------------------- */

/* (The body's font-size/line-height are set once, in app.css's base block, at
   the same token values — a restatement here was deleted 30 Aug as pure
   duplication: two rules, one value, and the second could only ever drift.) */

/* ---- 2. PAGE FURNITURE --------------------------------------------------- */
.uv-crumbs { font-size: var(--t-meta); color: var(--muted); margin-bottom: 10px;
  display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.uv-crumbs a { color: var(--muted); text-decoration: none; }
.uv-crumbs a:hover { color: var(--accent); text-decoration: underline; }
/* --line-strong is #cbd5e1: a BORDER colour, 1.36:1 as ink on the page ground.
   The ❯ between crumbs was in the DOM and not on the screen, so a trail read
   "Sheets Laptop Batch 349639". --ink-4 is quieter than the crumb text it
   separates and is actually visible. */
.uv-crumbs-sep { color: var(--ink-4); }

.uv-section { margin-top: var(--s6); }
.uv-section-head { display: flex; justify-content: space-between; align-items: flex-end;
  gap: 16px; margin-bottom: 12px; flex-wrap: wrap; }
.uv-section-title { font-size: var(--t-node); font-weight: var(--w-bold); color: var(--ink);
  letter-spacing: -.01em; }
.uv-section-sub { font-size: var(--t-meta); color: var(--muted); margin-top: 3px; }

.uv-r { text-align: right; }
.uv-muted { color: var(--muted); }
.uv-mono { font-family: var(--mono); font-variant-numeric: tabular-nums;
  font-size: .95em; letter-spacing: -.01em; }
.uv-sort { margin-left: 6px; font-size: 10px; color: var(--nordic-sort-active); }
.uv-link { color: var(--link); text-decoration: none; font-weight: var(--w-semi); }
.uv-link:hover { color: var(--link-hover); }
.uv-link:hover { text-decoration: underline; }
/* --faint (2.56:1) is for a placeholder — text nobody has to read. A hint is
   read: "One per line, or separated by commas or spaces". --ink-3 is 4.76:1
   and still visibly secondary. Same reason for .uv-stat-s and .uv-rule-m
   below; DEVICE_TEST_CHECKLIST §6 is the room this is read in. */
.uv-hint { font-size: var(--t-micro); font-weight: var(--w-reg); color: var(--ink-3); }
.uv-req { color: var(--bad); font-weight: var(--w-semi); font-size: var(--t-micro); }
.uv-confirm-msg { font-size: var(--t-body); line-height: var(--lh-prose); color: var(--ink-2); }

/* UNKNOWN RENDERS AS UNKNOWN. A missing value never renders blank and never
   renders a plausible substitute — po-planning printed "INCOTERMS: FOB" on a
   domestic order for months because a NULL fell through to a default. dash()
   is "legitimately not applicable"; gap() is "missing and it matters". */
 /* --ink-5 (#cbd5e1) on white is 1.48:1. The em-dash was there in the DOM and
    gone on the screen — 298 of them across the registers on 30 Aug, so "not
    applicable" rendered as blank, which is the exact reading rule 8 forbids.
    --ink-3 is the same grey the table HEADERS use: quiet, and actually there. */
.uv-dash { color: var(--ink-3); }
.uv-gap { display: inline-flex; align-items: center; gap: 5px; padding: 1px 7px;
  border-radius: var(--r-badge); font-size: var(--t-micro); font-weight: var(--w-semi);
  color: var(--warn-fg); background: repeating-linear-gradient(135deg,
    #fef3c7 0 5px, #fde68a 5px 10px); border: 1px solid var(--warn-line); }

/* ---- 3. BANNERS — say why, and offer the ONE action that fixes it -------- */
.uv-banner { display: flex; align-items: flex-start; gap: 11px; padding: 13px 15px;
  border: 1px solid var(--line); border-radius: var(--r); background: var(--surface);
  font-size: var(--t-body); line-height: var(--lh-body); }
/* The three ink literals are the 900-stops one step darker than the --*-fg
   tokens (which are 800s tuned for 12px badge text). A banner is a SENTENCE
   on a tinted ground and reads better a step darker; the token scale carries
   no 900 stop, so the literals are stated here — the one place they occur. */
.uv-banner.is-block { background: var(--bad-bg); border-color: var(--bad-line); color: #7f1d1d; }
.uv-banner.is-warn  { background: var(--warn-bg); border-color: var(--warn-line); color: #78350f; }
.uv-banner.is-info  { background: var(--info-bg); border-color: var(--info-line); color: #1e3a8a; }
.uv-banner.is-ok    { background: var(--ok-bg); border-color: var(--ok-line); color: var(--ok-fg); }
.uv-banner-ic { flex: none; margin-top: 1px; }
.uv-banner-text { flex: 1; min-width: 0; }
.uv-banner-act { flex: none; }

/* ---- 4. STAT STRIP — one bordered surface, hairline-divided cells.
   Not a row of floating cards: seven shadowed KPI tiles read as seven
   separate objects when they are one reading (goods-in v2's ruling).

   The values here are DELIBERATELY LARGE (--t-num, 34px, weight 800). On a
   floor panel the number is the interface; a 23px stat is a desk stat. ---- */
.uv-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r);
  overflow: hidden; margin-bottom: var(--s5); }
/* Hairlines drawn with box-shadow, not border-right, and the parent's
   overflow:hidden trims them at the edges. A wrapping auto-fit grid with
   border-right leaves a stray rule hanging off the last cell of every row but
   the last — which is what this did before 30 Aug, on every screen with more
   than four stat cells. (The technique is hr.css:161's, via procurement.) */
.uv-stat { padding: 16px 18px; min-width: 0;
  box-shadow: 1px 0 0 0 var(--line), 0 1px 0 0 var(--line); }
.uv-stat-l { display: flex; align-items: center; gap: 7px; font-size: var(--t-micro);
  font-weight: var(--w-semi); text-transform: uppercase; letter-spacing: var(--tr-caps);
  color: var(--muted); }
.uv-stat-l::before { content: ''; width: 8px; height: 8px; border-radius: 3px;
  background: var(--line-3); flex: none; }
.uv-stat-v { font-size: var(--t-num); font-weight: var(--w-black); margin-top: 8px;
  letter-spacing: var(--tr-num); color: var(--ink); font-variant-numeric: tabular-nums;
  line-height: 1.05; }
/* A stat value that is a WORD, not a number — see statStrip() in shell.js. */
.uv-stat-v.is-prose { font-size: var(--t-sub); font-weight: var(--w-bold);
  line-height: var(--lh-cell); }
/* The line UNDER the number, on every stat cell on every page — "53 items
   waiting for HP to batch them", "0 blocked · 4 need a reason". It is the
   sentence that says what the number means, and it was at 2.56:1. */
.uv-stat-s { font-size: var(--t-meta); color: var(--ink-3); margin-top: 5px; font-weight: var(--w-med); }
.uv-stat.is-bad  .uv-stat-l::before { background: var(--bad-dot); }
.uv-stat.is-bad  .uv-stat-v { color: var(--bad); }
.uv-stat.is-warn .uv-stat-l::before { background: var(--warn-dot); }
.uv-stat.is-warn .uv-stat-v { color: var(--warn); }
.uv-stat.is-good .uv-stat-l::before { background: var(--ok-dot); }
.uv-stat.is-good .uv-stat-v { color: var(--ok); }
.uv-stat.is-click { text-decoration: none; display: block; transition: background var(--d1); }
.uv-stat.is-click:hover, .uv-stat.is-click:focus-visible { background: var(--row-hover); }
/* Every stat cell that has a href is an anchor, so it is keyboard-reachable
   and openable in a new tab like every other link on the page. */

/* The one headline number — the wizard's savings line, the queue's "ready to
   build". Read across a room, so it gets its own step. */
.uv-headline { font-size: var(--t-num-lg); font-weight: var(--w-black);
  letter-spacing: var(--tr-num); line-height: 1.05; font-variant-numeric: tabular-nums; }

/* ---- 5. FILTER CHIPS — one row of chip-dropdowns, not a grid of selects.
   The <select> sits invisibly over the chip so the native picker (which is
   the searchable, keyboard-driven, touch-friendly one on every platform) is
   what actually opens. ---------------------------------------------------- */
.uv-chips { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 14px; }
.uv-chip { position: relative; display: inline-flex; align-items: center; gap: 7px;
  min-height: var(--h-ctl); padding: 0 12px; border-radius: var(--r-pill);
  border: 1px solid var(--line-2); background: var(--surface); color: var(--ink-2);
  font-size: var(--t-meta); font-weight: var(--w-med); cursor: pointer; }
.uv-chip:hover { border-color: var(--line-3); }
/* #3730a3 (indigo-800) is the readable INK for text sitting ON --accent-tint —
   the accent itself is 3.5:1 there. Also used by .uv-step.is-on below; kept a
   literal because it is an ink-on-tint pairing, not a brand stop. */
.uv-chip.is-active { background: var(--accent-tint); border-color: var(--accent-line); color: #3730a3; }
.uv-chip-select { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%; }
.uv-chip-clear { background: none; border: none; font-size: var(--t-meta);
  font-weight: var(--w-semi); color: var(--accent); cursor: pointer; min-height: var(--tap);
  padding: 0 8px; }

/* ---- 6. TABS — URL-backed (?tab=), and big enough to hit ---------------- */
.uv-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--line);
  margin-bottom: var(--s5); overflow-x: auto; scrollbar-width: none; }
.uv-tabs::-webkit-scrollbar { display: none; }
/* A HIDDEN SCROLLBAR IS NOT AN AFFORDANCE. At 375 the sheet's six tabs came out
   as four, with Batches and Activity off the right edge and nothing on screen
   saying they existed — the same failure the checklist writes down for a wizard
   rail ("wraps rather than scrolls off"). Every tab is already >=48px, so they
   wrap onto a second line instead. The container's rule then sits under the
   last row, which is where a tab underline belongs. */
@media (max-width: 768px) {
  .uv-tabs { flex-wrap: wrap; overflow-x: visible; }
}
.uv-tab { display: inline-flex; align-items: center; gap: 8px; min-height: var(--tap);
  padding: 0 16px; border: none; background: none; cursor: pointer; white-space: nowrap;
  font-size: var(--t-body); font-weight: var(--w-semi); color: var(--muted);
  border-bottom: 3px solid transparent; text-decoration: none; }
.uv-tab:hover { color: var(--ink); }
.uv-tab.is-on { color: var(--accent); border-bottom-color: var(--accent); }
.uv-tab-n { font-size: var(--t-micro); font-weight: var(--w-bold); padding: 2px 7px;
  border-radius: var(--r-pill); background: var(--grey-bg); color: var(--muted); }
.uv-tab.is-on .uv-tab-n { background: var(--accent-tint); color: var(--accent); }

/* ---- 7. KEY/VALUE — the node itself lives in PAGE PRIMITIVES below --------
   This section held a second .uv-node definition (at --r, 8px) that silently
   LOST to the PAGE PRIMITIVES copy lower in the file (--r-surface, 12px):
   same specificity, later source order. Every node has rendered at 12px since
   shell.css retired, so the loser was deleted 30 Aug rather than left to
   trap the next reader into editing the rule that does nothing. */
.uv-kv { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 14px 20px; }
.uv-kv-k { font-size: var(--t-micro); font-weight: var(--w-semi); text-transform: uppercase;
  letter-spacing: var(--tr-caps); color: var(--muted); margin-bottom: 4px; }
.uv-kv-v { font-size: var(--t-lead); font-weight: var(--w-semi); color: var(--ink); word-break: break-word; }

/* ---- 8. THE QA FIVE-PILL STRIP -----------------------------------------
   The single most-read object in the module: five counts that say whether a
   2.4 m plate is safe to print.

   SKIP IS HATCHED. Not decoration — on 29 Aug fifty-five packages carried six
   skips each and every one of their pages read as fine, because grey and green
   at 11px are the same pill at arm's length. The hatch survives greyscale,
   photocopying and a dirty screen. -------------------------------------- */
.uv-qa { display: inline-flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.uv-qa-pill { display: inline-flex; align-items: baseline; gap: 6px; padding: 5px 11px;
  border-radius: var(--r-badge); border: 1px solid transparent;
  font-size: var(--t-micro); font-weight: var(--w-semi); }
.uv-qa-pill b { font-size: var(--t-cell); font-weight: var(--w-bold);
  font-variant-numeric: tabular-nums; }
.uv-qa-pill.is-pass { color: var(--qa-pass-fg); background: var(--qa-pass-bg); border-color: var(--qa-pass-line); }
.uv-qa-pill.is-warn { color: var(--qa-warn-fg); background: var(--qa-warn-bg); border-color: var(--qa-warn-line); }
.uv-qa-pill.is-fail { color: var(--qa-fail-fg); background: var(--qa-fail-bg); border-color: var(--qa-fail-line); }
.uv-qa-pill.is-skip { color: var(--qa-skip-fg); background: var(--qa-skip-hatch); border-color: var(--qa-skip-line); }
.uv-qa-pill.is-info { color: var(--qa-info-fg); background: var(--qa-info-bg); border-color: var(--qa-info-line); }
.uv-qa-pill.is-zero { opacity: .45; }

/* One QA check, expanded on the sheet's QA tab. */
.uv-check { border: 1px solid var(--line); border-radius: var(--r); background: var(--surface);
  border-left-width: 4px; }
.uv-check + .uv-check { margin-top: 8px; }
.uv-check.is-pass { border-left-color: var(--qa-pass-line); }
/* #fffdf6 / #fffafa: washes far fainter than --warn-bg / --bad-bg — a card
   BODY holding readable prose, not a badge. No token stop sits between
   --surface and the tone tints, so the two washes are literals, here only. */
.uv-check.is-warn { border-left-color: var(--warn-dot); background: #fffdf6; }
.uv-check.is-fail { border-left-color: var(--bad-dot); background: #fffafa; }
.uv-check.is-skip { border-left-color: var(--mute-dot); }
.uv-check.is-info { border-left-color: var(--info-dot); }
.uv-check-head { display: flex; align-items: center; gap: 11px; padding: 13px 15px;
  cursor: pointer; min-height: var(--tap); flex-wrap: wrap; }
.uv-check-name { font-family: var(--mono); font-size: var(--t-cell); font-weight: var(--w-semi);
  color: var(--ink); }
.uv-check-msg { flex: 1; min-width: 200px; font-size: var(--t-cell); color: var(--ink-2); }
.uv-check-body { padding: 0 15px 15px 15px; }
.uv-check-json { font-family: var(--mono); font-size: var(--t-meta); background: var(--sunken);
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 11px 13px;
  white-space: pre-wrap; word-break: break-word; color: var(--ink-2); }

/* The skip-heavy banner. Six or more skips means the sheet was barely tested,
   and "ok" on that page has meant nothing since 29 Aug. */
.uv-skipwarn { display: flex; align-items: center; gap: 10px; padding: 12px 15px;
  border-radius: var(--r); margin-bottom: 14px; font-size: var(--t-body);
  color: var(--ink-2); background: var(--qa-skip-hatch); border: 1px solid var(--mute-line); }

/* ---- 9. THE RELEASE LINE + CONFIDENCE -----------------------------------
   One line on every sheet, and it is the gate's own sentence:
     Self-test: 12 pass · 1 warn · confidence 97% (High) — released by Manuj: "…"
   The override reason is quoted verbatim and never truncated: it is the record
   of somebody's decision to put a warned plate on a press. ---------------- */
.uv-release { display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 13px 16px; border-radius: var(--r); border: 1px solid var(--line);
  background: var(--surface); font-size: var(--t-body); }
.uv-release.is-clean     { background: var(--rel-clean-bg);     border-color: var(--ok-line);   color: var(--rel-clean-fg); }
.uv-release.is-attention { background: var(--rel-attention-bg); border-color: var(--warn-line); color: var(--rel-attention-fg); }
.uv-release.is-blocked   { background: var(--rel-blocked-bg);   border-color: var(--bad-line);  color: var(--rel-blocked-fg); }
.uv-release.is-released  { background: var(--rel-released-bg);  border-color: var(--odd-line);  color: var(--rel-released-fg); }
.uv-release-reason { font-style: italic; }

.uv-conf { display: inline-flex; align-items: center; gap: 8px; padding: 4px 11px;
  border-radius: var(--r-pill); font-size: var(--t-micro); font-weight: var(--w-semi); }
.uv-conf b { font-size: var(--t-cell); font-weight: var(--w-bold); font-variant-numeric: tabular-nums; }
.uv-conf.is-high { color: var(--conf-high-fg); background: var(--conf-high-bg); }
.uv-conf.is-med  { color: var(--conf-med-fg);  background: var(--conf-med-bg); }
.uv-conf.is-low  { color: var(--conf-low-fg);  background: var(--conf-low-bg); }
/* No score at all is NOT a low score: an older sheet built before the score
   was persisted has nothing to say, and saying "0%" about it would be a lie. */
.uv-conf.is-none { color: var(--muted); background: var(--grey-bg); }

/* ---- 10. THE POCKET MAP -------------------------------------------------
   A to-scale plan of the press bed. The container is width-driven and the
   drawing is absolutely positioned in percentages, so it scales from a
   1600px desktop to a 360px phone without a second layout.

   ORIGIN is drawn top-left on every map. The alignment guide in the package
   is the jig authority and the map says so out loud — the team's hand-made
   files sit 0.85 mm higher than ours, so an offset calibrated by habit
   misplaces the sheet. -------------------------------------------------- */
/* The ORIGIN flag used to be INSIDE .uv-map, absolutely placed at its top-left
   corner, which is exactly where slot 1 is. At 1440 it clipped the first
   pocket's border; at 375 it covered the number outright. It now sits in an
   18px strip above the bed, still pointing at the top-left corner, still drawn
   (rule 1 of pocket-map.js), and no longer covering the pocket it names. */
.uv-map-wrap { position: relative; padding-top: 18px; }
.uv-map { position: relative; width: 100%; background: var(--map-bed);
  border: 2px solid var(--map-bed-line); border-radius: var(--r-sm); overflow: hidden; }
.uv-map-slot { position: absolute; box-sizing: border-box; border: 1px solid var(--map-slot-line);
  background: var(--map-slot); border-radius: 2px; }
.uv-pocket { position: absolute; box-sizing: border-box; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 2px; overflow: hidden;
  border: 2px solid var(--map-filled-line); background: var(--map-filled);
  border-radius: 3px; cursor: pointer; text-decoration: none; color: var(--ink);
  transition: box-shadow var(--d1), transform var(--d1); }
.uv-pocket:hover, .uv-pocket:focus-visible { box-shadow: 0 0 0 3px var(--accent-ring);
  z-index: 2; outline: none; }
.uv-pocket.is-sel { background: var(--map-sel); border-color: var(--map-sel-line); z-index: 3;
  box-shadow: 0 0 0 4px var(--map-sel-ring); }
.uv-pocket.is-low { background: var(--map-low); border-color: var(--map-low-line); }
/* A placement HP shipped with no artwork at all. It is hatched, never blank —
   a blank rectangle on a plan of a press bed reads as "nothing to print here",
   which is exactly the wrong reading. */
.uv-pocket.is-blank { background: var(--map-blank); border-style: dashed; cursor: default; }
.uv-pocket.is-fail  { border-color: var(--bad-dot); box-shadow: inset 0 0 0 2px var(--bad-bg); }
.uv-pocket-n { font-size: var(--t-cell); font-weight: var(--w-black); line-height: 1;
  font-variant-numeric: tabular-nums; }
/* (.uv-pocket-t — artwork inside a map pocket — was styled here but
   pocket-map.js never emits it; the map draws number + SKU only, and the
   thumbnail lives in the pocket CARDS beside it. Deleted 30 Aug as dead.) */
.uv-pocket-s { position: relative; font-size: 10px; font-weight: var(--w-semi);
  color: var(--ink-2); max-width: 92%; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
.uv-pocket-c { position: absolute; top: 2px; right: 3px; font-size: 9px;
  font-weight: var(--w-bold); color: var(--warn-fg); }
.uv-map-die { position: absolute; box-sizing: border-box; pointer-events: none;
  border: 2px dashed var(--map-die); border-radius: 3px; }
.uv-map-origin { position: absolute; top: 0; left: 0; display: flex; align-items: center;
  gap: 4px; padding: 2px 8px; background: var(--map-origin); color: #fff; font-size: 10px;
  font-weight: var(--w-bold); letter-spacing: var(--tr-caps);
  border-radius: 4px 4px 0 0; pointer-events: none; z-index: 4; }
.uv-map-legend { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 10px;
  font-size: var(--t-micro); color: var(--muted); }
.uv-map-legend span { display: inline-flex; align-items: center; gap: 6px; }
.uv-map-legend i { width: 14px; height: 14px; border-radius: 3px; border: 1px solid var(--line-3);
  display: inline-block; }
.uv-map-note { margin-top: 8px; font-size: var(--t-meta); color: var(--muted); }

/* The mini map on a plan card / merge preview — same component, no labels. */
.uv-map.is-mini .uv-pocket-s, .uv-map.is-mini .uv-map-origin { display: none; }
.uv-map.is-mini .uv-pocket-n { font-size: 9px; }

/* ---- 11. BUILD STAGE TIMELINE ------------------------------------------
   Eight stages with real durations. A failed stage expands with the verbatim
   error, and `stalled` is a first-class red state rather than a spinner that
   never stops — the 45-minute HP download stall is a real pathology and a
   build that is stuck must say so. ------------------------------------- */
.uv-stages { display: flex; flex-direction: column; gap: 2px; }
.uv-stage { display: grid; grid-template-columns: 26px 150px 1fr 84px; align-items: center;
  gap: 12px; padding: 11px 12px; border-radius: var(--r-sm); min-height: var(--tap); }
.uv-stage:hover { background: var(--row-hover); }
.uv-stage-dot { width: 12px; height: 12px; border-radius: 50%; background: var(--line-3);
  justify-self: center; }
.uv-stage.is-done    .uv-stage-dot { background: var(--ok-dot); }
.uv-stage.is-running .uv-stage-dot { background: var(--info-dot); animation: uv-pulse 1.4s infinite; }
.uv-stage.is-failed  .uv-stage-dot { background: var(--bad-dot); }
.uv-stage.is-stalled .uv-stage-dot { background: var(--bad-dot); animation: uv-pulse 1.4s infinite; }
.uv-stage-name { font-weight: var(--w-semi); font-size: var(--t-cell); }
.uv-stage-bar { height: 10px; border-radius: var(--r-pill); background: var(--grey-bg); overflow: hidden; }
.uv-stage-fill { display: block; height: 100%; background: var(--accent); border-radius: var(--r-pill); }
.uv-stage.is-failed .uv-stage-fill, .uv-stage.is-stalled .uv-stage-fill { background: var(--bad-dot); }
.uv-stage-t { text-align: right; font-variant-numeric: tabular-nums; font-weight: var(--w-semi);
  font-size: var(--t-cell); color: var(--ink-2); }
.uv-stage-err { grid-column: 2 / -1; font-family: var(--mono); font-size: var(--t-meta);
  color: var(--bad-fg); background: var(--bad-bg); border-radius: var(--r-sm); padding: 10px 12px;
  white-space: pre-wrap; }
@keyframes uv-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

/* The six-segment micro-bar in a Builds row. */
.uv-microbar { display: inline-flex; gap: 2px; }
.uv-microbar i { width: 12px; height: 8px; border-radius: 2px; background: var(--grey-bg);
  display: inline-block; }
.uv-microbar i.is-done { background: var(--ok-dot); }
.uv-microbar i.is-running { background: var(--info-dot); }
.uv-microbar i.is-failed { background: var(--bad-dot); }

/* ---- 12. THE CONSERVATION RAIL -----------------------------------------
   Selected = Placed + Unplaced, always on screen while merging, and a nonzero
   Unplaced blocks Build and names the culprits. This is the rail that stops a
   item being silently dropped off a merged sheet. --------------------- */
.uv-cons { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; padding: 14px 18px;
  border-radius: var(--r); border: 1px solid var(--ok-line); background: var(--ok-bg); }
.uv-cons.is-bad { border-color: var(--bad-line); background: var(--bad-bg); }
.uv-cons-n { font-size: var(--t-sub); font-weight: var(--w-black); font-variant-numeric: tabular-nums; }
.uv-cons-l { font-size: var(--t-micro); font-weight: var(--w-semi); text-transform: uppercase;
  letter-spacing: var(--tr-caps); color: var(--ink-2); }

/* ---- 13. WIZARD RAIL — print run, manual upload, impose ---------------- */
.uv-steps { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: var(--s5); }
.uv-step { display: flex; align-items: center; gap: 10px; padding: 10px 16px;
  border-radius: var(--r-pill); border: 1px solid var(--line-2); background: var(--surface);
  font-size: var(--t-meta); font-weight: var(--w-semi); color: var(--muted); min-height: var(--tap); }
.uv-step.is-on { border-color: var(--accent); background: var(--accent-tint); color: #3730a3; }
.uv-step.is-done { border-color: var(--ok-line); background: var(--ok-bg); color: var(--ok-fg); }
.uv-step-n { width: 24px; height: 24px; border-radius: 50%; background: var(--grey-bg);
  display: inline-flex; align-items: center; justify-content: center; font-weight: var(--w-bold);
  font-size: var(--t-micro); flex: none; }
.uv-step.is-on .uv-step-n { background: var(--accent); color: #fff; }
.uv-step.is-done .uv-step-n { background: var(--ok-dot); color: #fff; }

/* ---- 14. CARD GRIDS — sheets, plans, buckets, case types --------------- */
/* align-items:start, not the grid default of stretch. Two merge-opportunity
   cards of 39 and 3 buckets sat side by side on buckets.html and the short one
   was stretched to 700px, most of it empty white — a card the eye reads as
   broken rather than as short. A card is as tall as what is in it. */
.uv-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 14px; align-items: start; }
.uv-card { display: block; text-decoration: none; color: inherit; background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--r); overflow: hidden;
  transition: border-color var(--d1), box-shadow var(--d1); }
.uv-card:hover, .uv-card:focus-visible { border-color: var(--accent-line);
  box-shadow: var(--sh-pop); outline: none; }
.uv-card-head { padding: 14px 16px 10px; }
.uv-card-t { font-size: var(--t-lead); font-weight: var(--w-bold); color: var(--ink);
  line-height: var(--lh-cell); }
.uv-card-s { font-size: var(--t-meta); color: var(--muted); margin-top: 4px; }
.uv-card-body { padding: 0 16px 14px; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.uv-card-foot { padding: 11px 16px; border-top: 1px solid var(--line); background: var(--sunken);
  display: flex; gap: 10px; align-items: center; flex-wrap: wrap; font-size: var(--t-meta);
  color: var(--muted); }

/* ---- 15. FILL BAR — bucket fill against threshold, slot fill on a sheet.
   Under-filled is the module's whole economic case (58% of 6UP batches close
   at 5.12/6), so the bar states the fraction rather than only drawing it. -- */
.uv-fill { display: inline-flex; align-items: center; gap: 10px; }
.uv-fill-track { display: inline-block; width: 110px; height: 12px; border-radius: var(--r-pill);
  background: var(--grey-bg); overflow: hidden; flex: none; }
.uv-fill-bar { display: block; height: 100%; border-radius: var(--r-pill); background: var(--accent); }
.uv-fill-bar.is-low  { background: var(--warn-dot); }
.uv-fill-bar.is-full { background: var(--ok-dot); }
.uv-fill-n { font-size: var(--t-cell); font-weight: var(--w-bold); font-variant-numeric: tabular-nums; }

/* ---- 16. COLUMN TOGGLE (inside the .cd-modal) ------------------------- */
.uv-coltoggle { display: flex; flex-direction: column; gap: 2px; max-height: 52vh; overflow-y: auto; }
.uv-coltoggle-row { display: flex; align-items: center; gap: 10px; padding: 10px 8px;
  border-radius: var(--r-sm); cursor: pointer; min-height: var(--tap); font-size: var(--t-body); }
.uv-coltoggle-row:hover { background: var(--row-hover); }
.uv-coltoggle-row.is-locked { opacity: .65; cursor: default; }
.uv-lock-note { margin-left: auto; font-size: var(--t-micro); color: var(--faint); }

/* ---- 17. THE HEADER MENU PANELS — what still belongs to this module -------

   This section used to hold an avatar menu, a mobile dock and their responsive
   rules. They are gone: since 30 Aug the chrome is live Pulse's own, in
   app.css (`.app-header-menu`, `.top-nav`, `.mobile-nav`, `.mt3-dock`), and a
   rule with no markup behind it is worse than a missing one — the next person
   to read it believes the module still draws its own dock.

   What survives is the PANEL BODY: shell.js still fills the user menu and the
   "Start print ▾" menu with these rows, because their content is ours (the
   shell themes, the two ways to start a print run) even though the box around
   them is Pulse's.
   -------------------------------------------------------------------------- */
/* Only the ROW classes survive here. shell.js emits .uv-menu-item / -tick /
   -t / -s inside boxes that are somebody else's (.uv-primary-menu below, and
   Pulse's own .app-header-menu__dropdown). The .uv-menu container itself, its
   -sec / -div / -foot furniture and the .is-on state were emitted by nobody —
   the appearance picker they served now lives in Pulse's dropdown with Pulse's
   own .app-header-menu__appearance-btn — and were deleted 30 Aug as dead. */
.uv-menu-item { display: flex; align-items: flex-start; gap: 10px; width: 100%; padding: 10px;
  border: none; background: none; text-align: left; cursor: pointer; border-radius: var(--r-ctl);
  min-height: var(--tap); color: var(--ink); text-decoration: none; }
.uv-menu-item:hover { background: var(--row-hover); }
.uv-menu-tick { flex: none; width: 18px; color: var(--accent); }
.uv-menu-t { display: block; font-size: var(--t-body); font-weight: var(--w-semi); }
.uv-menu-s { display: block; font-size: var(--t-meta); color: var(--muted); margin-top: 2px; }

/* ---- 18. THE PRIMARY-ACTION MENU --------------------------------------- */
/* This section also carried a full .uv-actionbar block, ported here while
   shell.css's hr- → uv- rename was stuck. shell.css has since retired and the
   PAGE PRIMITIVES section at the foot of this file owns the action bar — and
   because that copy is LATER in the file at the same specificity, it won every
   declaration and this one was a twin that could only lose. Deleted 30 Aug;
   the surviving copy also carries min-height: var(--h-actionbar), which this
   one never had. */
.uv-primary { position: relative; }
.uv-primary-menu { display: none; position: absolute; top: calc(100% + 6px); right: 0;
  width: 260px; background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r); box-shadow: var(--sh-pop); padding: 6px; z-index: var(--z-popover); }
.uv-primary.open .uv-primary-menu { display: block; }

/* The user menu's OPERATOR section — our content inside Pulse's dropdown box
   (the rows themselves are .app-header-menu__dropdown-item, so they read as
   the menu they sit in; only the radio dot and the section's spacing are
   ours). Shared-terminal model: switching operator changes who audit rows
   name, never the login — see Printing.ACCOUNT in print.js. */
.uv-op-sect { padding: 8px 0 4px; }
.uv-op-sect > .app-header-menu__appearance-label { padding: 0 16px; }
.uv-op-dot { flex: none; width: 16px; height: 16px; border-radius: 50%;
  border: 2px solid var(--line-2); background: transparent; }
.uv-op-row.is-on .uv-op-dot { border-color: var(--accent); background: var(--accent);
  /* the white ring that makes a filled circle read as a chosen radio */
  box-shadow: inset 0 0 0 3px var(--surface); }
.uv-op-row.is-on { background: var(--accent-tint); }

/* ---- 19. ACTIVITY / AUDIT FEED --------------------------------------- */
.uv-feed { display: flex; flex-direction: column; }
.uv-feed-row { display: flex; gap: 12px; padding: 13px 4px; border-bottom: 1px solid var(--line); }
.uv-feed-row:last-child { border-bottom: none; }
.uv-feed-ic { flex: none; width: 30px; height: 30px; border-radius: 50%; background: var(--grey-bg);
  display: flex; align-items: center; justify-content: center; color: var(--muted); }
.uv-feed-b { flex: 1; min-width: 0; }
.uv-feed-t { font-size: var(--t-body); color: var(--ink); }
.uv-feed-m { font-size: var(--t-meta); color: var(--muted); margin-top: 3px; }
.uv-feed-day { font-size: var(--t-micro); font-weight: var(--w-bold); text-transform: uppercase;
  letter-spacing: var(--tr-caps); color: var(--ink-3); padding: 16px 4px 6px; }

/* ---- 20. RESPONSIVE + MOTION ----------------------------------------- */
@media (max-width: 900px) {
  .uv-stage { grid-template-columns: 22px 1fr 70px; }
  .uv-stage-bar { display: none; }
}
@media (max-width: 768px) {
  body { padding-bottom: var(--h-dock-pad); }
  .uv-stats { grid-template-columns: repeat(2, 1fr); }
  .uv-stat { border-bottom: 1px solid var(--line); }
  .uv-stat-v { font-size: 28px; }
  .uv-kv { grid-template-columns: 1fr; }
  .uv-cards { grid-template-columns: 1fr; }
}
/* prefers-reduced-motion lives in app.css's RESPONSIVE / PRINT / MOTION block
   (which also covers ::before/::after and scroll-behavior). A second copy here
   restated two of its three declarations at a different duration — .001ms
   against .01ms, both indistinguishable from zero — and was deleted 30 Aug;
   its one addition, animation-iteration-count: 1, moved into app.css's rule. */

/* Focus is visible everywhere. A keyboard operator and a touch operator are
   the same person at different hours of the same shift. */
a:focus-visible, button:focus-visible, input:focus-visible,
select:focus-visible, textarea:focus-visible, [tabindex]:focus-visible {
  outline: 3px solid var(--accent-ring); outline-offset: 2px; border-radius: var(--r-sm);
}

/* Print: the pocket map and the QA list are the two things anybody prints off
   these screens, and they print onto a job bag that goes to the press. */
@media print {
  .app-header-menu, .mobile-nav, .mt3-dock, .app-footer,
  .uv-actionbar, .cd-table-toolbar, .uv-chips { display: none !important; }
  .uv-main { margin: 0 !important; padding: 0 !important; }
  .uv-map { break-inside: avoid; }
}

/* ---- 21. COPY TARGET — an id with no page behind it. Styled as a control so
   it is obviously clickable, and NOT as a link so it is obviously not a
   destination. --------------------------------------------------------- */
.uv-copy { border: 1px dashed var(--line-2); background: none; color: var(--ink-2);
  border-radius: var(--r-sm); padding: 3px 8px; cursor: pointer; font-size: var(--t-meta);
  min-height: 32px; }
.uv-copy:hover { border-color: var(--accent-line); background: var(--accent-tint); color: var(--accent); }
/* 32px is a DESKTOP decision — a copy target inline in a twenty-column
   register, where every pixel of row height costs a row of real work. It is
   not a decision a phone gets to inherit: measured at 375px on 30 Aug there
   were 126 of these on the module's own screens at 122 x 32, and rule 6 has no
   width exception. Three pages already restated the floor one at a time
   (.uv-item-act, .uv-item-kv, .uv-idcell); this states it once, for the
   widths where a finger is the pointer. */
@media (max-width: 768px) {
  .uv-copy { min-height: var(--tap); }
}

/* The pocket card the URL anchors to. Same amber as the map's selection, so
   "the one I tapped" reads the same in both halves of the screen. */
.uv-card.is-sel { border-color: var(--map-sel-line); box-shadow: 0 0 0 3px var(--map-sel-ring); }

/* ---- 22. THE PAGE SEARCH BAR -------------------------------------------
   The register's dominant control, and the ONE control every not-found page
   has to offer. Added in this wave because /skus is the first list screen and
   a bad ?sku= / ?barcode= must land on a page with a way forward rather than
   an empty column (rule 9). The input carries .cd-form-input for its nordic
   skin; everything set here is on the .uv- class, so no .cd-* rule is
   overridden — uv.css loads last, so equal specificity resolves our way. -- */
.uv-searchbar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
  margin-bottom: 14px; }
/* Two classes, deliberately: the input carries .cd-form-input for its skin and
   .uv-search-input for OUR sizing, so no .cd-* rule is the subject of a rule
   here (the layer rule). The min-height restates the floor --h-ctl already
   carries — harmless, and it survives even if a density mode caps .cd-form-*.
   (An earlier comment here described a [data-density="comfortable"] fork at
   the top of this file out-specifying the floor; that fork was deleted — see
   §1 — and the height decision now lives in the token.) */
.uv-searchbar .uv-search-input { flex: 1 1 240px; min-width: 0; min-height: var(--tap); }
.uv-searchbar-note { flex: 1 1 100%; font-size: var(--t-meta); color: var(--muted); }

/* ---- 23. THE ITEM PAGE — the label QR target, drawn phone-first -------
   Every other screen in this module is a desktop screen that survives a
   phone. This one is the opposite: it is what opens when somebody standing at
   a press scans the QR on a label, so the phone is the design target and the
   desktop is the widening. Hence: one column by default, the identity (the
   barcode) at a size readable at arm's length under a plate, the thumbnail
   big enough to compare against the case in the hand, and the two-column
   split only from 760 px up. ------------------------------------------- */
.uv-item-hero { display: grid; gap: var(--s4); background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--r); padding: 16px; }
.uv-item-id { min-width: 0; }
.uv-item-eyebrow { font-size: var(--t-micro); font-weight: var(--w-bold);
  text-transform: uppercase; letter-spacing: var(--tr-caps); color: var(--ink-3);
  margin-bottom: 6px; }
/* The barcode is this page's <h1>. It carries no typography of its own — the
   copy button inside it does — so the heading is reset to nothing but a box.
   Keeping it a real heading is what makes the screen-reader outline agree with
   what the eye sees. */
.uv-item-h1 { margin: 0; font: inherit; }

/* The barcode IS the page, and it is a copy target rather than a link: there
   is nowhere else for it to go. Sized in vw so it fills a 375 px screen and
   stops growing on a monitor; break-all because a scan that half-read the code
   must still show every character it did read. */
.uv-item-code { display: block; width: 100%; text-align: left; cursor: pointer;
  font-family: var(--mono); font-weight: var(--w-black);
  font-size: clamp(28px, 9vw, 42px); line-height: 1.05; letter-spacing: .035em;
  color: var(--ink); word-break: break-all; padding: 10px 12px; min-height: var(--tap);
  background: var(--sunken); border: 1px dashed var(--line-3); border-radius: var(--r); }
.uv-item-code:hover, .uv-item-code:focus-visible { border-color: var(--accent-line);
  background: var(--accent-tint); color: var(--accent); }
.uv-item-code-hint { font-size: var(--t-meta); color: var(--muted); margin-top: 6px; }

/* The thumbnail. In the app this is mt3_uv_thumbs (never the expiring HP URL);
   here it is a marked gap, because a plausible grey square is exactly the kind
   of substitute rule 8 exists to forbid. */
.uv-item-thumb { position: relative; width: 100%; max-width: 360px; aspect-ratio: 4 / 3;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 8px; padding: 14px; text-align: center; color: var(--muted);
  background: var(--sunken); border: 1px dashed var(--line-3); border-radius: var(--r); }
.uv-item-thumb-note { font-size: var(--t-meta); max-width: 30ch; line-height: var(--lh-cell); }
/* 4/3 of a 343px phone column is 257px of dashed grey saying "there is no
   picture", and on the one page that exists to be opened by a label scan that
   is 257px between the barcode and the answer. Below 760 (where the thumbnail
   is a stacked block rather than its own column) it keeps its shape only if
   there is something in it. */
@media (max-width: 759px) {
  .uv-item-thumb { aspect-ratio: auto; min-height: 0; padding: var(--s3);
    flex-direction: row; text-align: left; }
}

.uv-item-chips { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }

/* Actions stretch to the full width of a phone and sit in a row from 560 px.
   A grid rather than a flex row so the children stretch without this file
   having to reach into .cd-btn to say so. */
/* grid-auto-rows restates the touch floor on the one page that is only ever
   used in gloves. The [data-density="comfortable"] fork this once defended
   against is gone (§1 tells that story — the floor now rides in --h-ctl
   itself), but the guarantee stays local: whatever a future density mode does
   to .cd-btn, the item page's actions hold 48 px. */
.uv-item-act { display: grid; gap: 10px; grid-auto-rows: minmax(var(--tap), auto); }
.uv-item-act .uv-copy { min-height: var(--tap); font-size: var(--t-cell); }
/* Rule 6 has no exception on the one page that is only ever read on a phone:
   the copy targets in the lineage grid get the full 48 px too. uv.css §21
   sizes .uv-copy at 32 px for table cells, which is a desktop decision. */
.uv-item-kv .uv-copy { min-height: var(--tap); font-size: var(--t-cell); }
/* WAS grid-auto-flow: column, and it scrolled the whole page sideways by 38px
   at 768 — three auto columns cannot shrink below their min-content, and 768 is
   exactly where the hero has just split in two and the id column is down to
   ~360px. A wrapping flex row keeps the desktop pose and drops the third button
   onto a second line when it does not fit. */
@media (min-width: 560px) {
  .uv-item-act { display: flex; flex-wrap: wrap; align-items: stretch; }
  .uv-item-act > * { min-height: var(--tap); }
}
@media (min-width: 760px) {
  .uv-item-hero { grid-template-columns: 360px 1fr; align-items: start; }
  /* The code comes FIRST in the DOM (a phone reads it before it sees the
     picture); on a desktop the picture takes the left column. Order, not a
     second block of markup. */
  .uv-item-pic { order: -1; }
}

/* ---- 24. THE REST OF THE BUILD TIMELINE — added with Builds and build
   detail. Nothing here overrides a .cd-* rule, and nothing duplicates a class
   another page already added: the sheet register's preview cell reuses
   `.uv-thumb.is-none` from the thumbnail block above rather than defining a
   second thumbnail. ------------------------------------------------------- */

/* §11's micro-bar knew done/running/failed. A queue that also holds stalled,
   pending and cancelled segments needs all six, and STALLED IS NOT A SLOWER
   RUNNING: same red as failed, and it pulses so a glance down the column finds
   it. Pending stays the empty track it already was. */
.uv-microbar i.is-stalled { background: var(--bad-dot); animation: uv-pulse 1.4s infinite; }
.uv-microbar i.is-cancelled { background: var(--line-3); }

/* A stage that has not run yet must not read like one that passed. */
/* --faint (#94a3b8) on white is 2.56:1 and a stage NAME is content, not a
   placeholder — "plate", "guide", "qa" are what the operator is reading down.
   --ink-3 is 4.76:1 and still visibly quieter than a finished stage. */
.uv-stage.is-pending .uv-stage-name, .uv-stage.is-pending .uv-stage-t { color: var(--ink-3); }
.uv-stage.is-cancelled .uv-stage-dot { background: var(--line-3); }

/* ?stage=<name> is the target of a failure notification, so the anchor has to
   be VISIBLE on arrival — scrolling to a row that looks like every other row is
   not landing anywhere. Same amber as `.uv-hit` and the pocket map's selection:
   "the one I was sent to" reads the same everywhere in the module. A stage row
   is a grid div rather than a <tr>, so it cannot use `.uv-hit` itself. */
.uv-stage.is-anchor { background: var(--map-sel); box-shadow: inset 3px 0 0 var(--map-sel-line); }

/* The measured facts a stage carries — bytes and source on download, dpi and
   workers on render. Its own grid row, like .uv-stage-err, because the stage is
   a four-column grid and a fifth child would collapse the bar. */
.uv-stage-detail { grid-column: 2 / -1; font-size: var(--t-meta); color: var(--muted);
  display: flex; flex-wrap: wrap; gap: 4px 16px; padding: 2px 0 4px; }
.uv-stage-detail b { color: var(--ink-2); font-weight: var(--w-semi); font-variant-numeric: tabular-nums; }

/* The one quiet line that keeps an apportioned number from reading as a
   measurement. Deliberately plain: it is a caveat, not an alarm. */
.uv-derived { font-size: var(--t-meta); color: var(--muted); padding: 10px 12px;
  border-left: 3px solid var(--line-2); margin: 10px 0; line-height: var(--lh-prose); }


/* ---- 25. THE THREE-PANE PLANNER ---------------------------------------
   Only the merge planner needs this: eligible batches | plan preview |
   conservation rail, side by side on a desk and stacked read-only on a phone.
   It is the one screen in the module that is explicitly desktop work, and it
   says so on itself rather than pretending a phone can do it.

   Nothing here is drag-only. The pane is a list of ≥48px rows that tap-select,
   and every destination is a real <button> — a merge planner that needs a
   mouse is a merge planner the floor cannot use when the desk is occupied. */
.uv-panes { display: grid; grid-template-columns: minmax(300px, 1fr) minmax(380px, 1.7fr)
  minmax(280px, .95fr); gap: 14px; align-items: start; }
.uv-pane { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r);
  overflow: hidden; min-width: 0; }
.uv-pane-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 13px 15px; border-bottom: 1px solid var(--line); background: var(--sunken); }
/* White on --ink-4 is 2.56:1 — the step number in a three-pane wizard was the
   least legible glyph on the screen. --ink-3 takes it to 5.0:1. */
.uv-pane-n { width: 22px; height: 22px; border-radius: 50%; flex: none; background: var(--ink-3);
  color: #fff; font-size: var(--t-micro); font-weight: var(--w-black);
  display: inline-flex; align-items: center; justify-content: center; }
.uv-pane-t { font-size: var(--t-node); font-weight: var(--w-bold); color: var(--ink); }
.uv-pane-s { flex-basis: 100%; font-size: var(--t-meta); color: var(--muted); }
.uv-pane-body { padding: 12px 13px; display: flex; flex-direction: column; gap: 12px; }

/* One destination group in pane 1: the imposition or the bedline family that a
   tapped batch can be moved into. The whole header is the drop target and it
   is a button, so tap-destination and click-destination are one control. */
.uv-group { border: 1px solid var(--line); border-radius: var(--r-sm); overflow: hidden; }
.uv-group-head { width: 100%; text-align: left; display: flex; align-items: center; gap: 9px;
  flex-wrap: wrap; padding: 10px 12px; min-height: var(--tap); border: none; cursor: default;
  background: var(--sunken); font-size: var(--t-cell); font-weight: var(--w-bold); color: var(--ink); }
.uv-group.is-target .uv-group-head { cursor: pointer; background: var(--accent-tint);
  border-bottom: 1px dashed var(--accent-line); }
/* WAS var(--accent-lo) — the PRESSED-BUTTON fill, #4a56c8. The head keeps its
   --ink text on hover, and #0f172a on #4a56c8 is ~1.9:1: the drop target went
   near-illegible at the exact moment a finger hovered it. --accent-line is the
   next tint stop up from the resting --accent-tint, so hover still deepens. */
.uv-group.is-target .uv-group-head:hover { background: var(--accent-line); }
.uv-group-n { margin-left: auto; font-size: var(--t-meta); font-weight: var(--w-semi); color: var(--muted); }

/* A batch offered to the plan. is-on = in the plan; is-picked = tapped and
   waiting for a destination; is-refused = greyed WITH ITS REASON and never
   removed from the list, because a batch that disappears is a lost item. */
.uv-brow { display: flex; align-items: stretch; border-top: 1px solid var(--line);
  background: var(--surface); }
.uv-brow:first-child { border-top: none; }
.uv-brow.is-on { background: var(--accent-tint); }
.uv-brow.is-picked { background: var(--map-sel); box-shadow: inset 3px 0 0 var(--map-sel-line); }
.uv-brow.is-refused { background: var(--grey-bg); color: var(--muted); }
/* Select and Move are two separate real buttons rather than one row with a
   secret gesture. A nested <button> is invalid and unreachable by keyboard,
   and a long-press is a control nobody can see. */
.uv-brow-hit { flex: 1; min-width: 0; display: flex; align-items: center; gap: 11px;
  text-align: left; padding: 9px 12px; min-height: var(--tap); background: none; border: none;
  cursor: pointer; font: inherit; color: inherit; }
.uv-brow-hit:hover { background: var(--row-hover); }
.uv-brow.is-refused .uv-brow-hit { cursor: not-allowed; }
.uv-brow-move { flex: none; min-height: var(--tap); padding: 0 12px; border: none;
  border-left: 1px dashed var(--line-2); background: none; cursor: pointer;
  font-size: var(--t-micro); font-weight: var(--w-bold); text-transform: uppercase;
  letter-spacing: var(--tr-caps); color: var(--accent); }
.uv-brow-move:hover { background: var(--accent-tint); }
.uv-brow-tick { width: 20px; height: 20px; flex: none; border-radius: var(--r-sm);
  border: 2px solid var(--line-strong); display: inline-flex; align-items: center;
  justify-content: center; color: #fff; }
.uv-brow.is-on .uv-brow-tick { background: var(--accent); border-color: var(--accent); }
.uv-brow-b { flex: 1; min-width: 0; }
/* display:block, and it is not a tidy-up. Both of these were inline spans, so
   the batch number and the SKU under it rendered on ONE line with no space
   between them: "360082LUX-17-P · Lux Case", 29 rows of it. The margin-top the
   second one already carried says the author meant them stacked. (2px was also
   off the 4px grid; --s1.) */
.uv-brow-t { display: block; font-size: var(--t-cell); font-weight: var(--w-semi); }
.uv-brow-m { display: block; font-size: var(--t-micro); color: var(--muted);
  margin-top: var(--s1); }
.uv-brow-q { font-size: var(--t-cell); font-weight: var(--w-bold); font-variant-numeric: tabular-nums; }

/* The refusal sentence itself. It is never a tooltip: an operator on a wall
   panel has no hover, and "why is this one grey" is the question the pane
   exists to answer. */
.uv-refuse { display: block; font-size: var(--t-micro); font-weight: var(--w-semi);
  color: var(--bad-fg); margin-top: 3px; }
.uv-refuse.is-soft { color: var(--warn-fg); }

/* The rail is sticky in its own pane so "Selected = Placed + Unplaced" is on
   screen while you scroll a long batch list — the requirement is *always
   visible*, not *present somewhere on the page*. */
.uv-cons.is-rail { position: sticky; top: calc(var(--h-header) + 10px); z-index: var(--z-stickycol); }
.uv-culprits { margin: 0; padding: 10px 0 0 18px; font-size: var(--t-meta); color: var(--bad-fg); }
.uv-culprits li { margin-bottom: 5px; }

/* The move banner: which batch is picked up, and how to put it down. */
.uv-picked { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 11px 14px; border-radius: var(--r); border: 1px solid var(--map-sel-line);
  background: var(--map-sel); font-size: var(--t-body); }

/* THE CONSERVATION RAIL HAS TO BE VISIBLE. IA_SIGNOFF §1 row 10 says so, and
   it was not: pane 1 holds every offered batch in an unbounded column — 233 of
   them on 30 Aug — so the rail measured at y=8465 at 1024px, y=8391 at 768 and
   y=9481 at 375. Eight thousand pixels below the one number that says whether
   the merge lost an item. Two halves, one per layout:
     wide   — the rail sticks under the header and the list scrolls past it;
     narrow — the list gets its own scroller, so the whole planner is one
              screenful and the rail is on it.
   --h-header + --s3 is the sticky header's own height plus the page gutter. */
.uv-pane-3 { position: sticky; top: calc(var(--h-header) + var(--s3)); }

@media (max-width: 1240px) {
  .uv-panes { grid-template-columns: 1fr 1fr; }
  .uv-pane-3 { grid-column: 1 / -1; position: static; }
  /* Bounded here, not above: at full width the two lists are beside the rail
     and a page scroll shows all three. Below 1240 they are stacked, and an
     unbounded one buries everything under it. */
  .uv-pane-1 > .uv-pane-body,
  .uv-pane-2 > .uv-pane-body { max-height: 62vh; overflow-y: auto;
    overscroll-behavior: contain; }
  /* flex:0 0 auto, and it is the same scar app.css's nav cards carry: the body
     is a flex COLUMN, so the moment it gains a max-height every child shrinks
     to make the set fit, and .uv-group's overflow:hidden then clips the rows
     inside it. Twenty-nine batch rows rendered as blank grey bars. A group is
     as tall as its rows; the COLUMN is what scrolls. */
  .uv-pane-1 > .uv-pane-body > *,
  .uv-pane-2 > .uv-pane-body > * { flex: 0 0 auto; }
}
@media (max-width: 900px) {
  .uv-panes { grid-template-columns: 1fr; }
  .uv-pane-3 { grid-column: auto; }
  /* Belt and braces: init_merge_planner also renders the read-only variant at
     this width, so the panes are never interactive-but-unusable if the JS
     media query and the CSS one ever drift apart. */
  .uv-panes.is-readonly .uv-brow-hit, .uv-panes.is-readonly .uv-brow-move,
  .uv-panes.is-readonly .uv-group-head { pointer-events: none; }
}

/* ---- 26. THE PINNED PLAN JSON ------------------------------------------
   The record a press sheet is rebuilt from, months later, shown as the bytes
   it is rather than as a rendering of them. Sorted keys, indent 1 — the exact
   shape MergePlan.to_json writes (spotfile/merge.py:273-275) — so what is on
   screen diffs clean against what the server stored. ---------------------- */
.uv-json { font-family: var(--mono); font-size: var(--t-meta); background: var(--sunken);
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 13px 15px;
  white-space: pre; overflow-x: auto; max-height: 62vh; overflow-y: auto; color: var(--ink-2);
  line-height: var(--lh-cell); }
.uv-json-bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-bottom: 10px; font-size: var(--t-meta); color: var(--muted); }

/* ---- 27. THE LABEL SHEET — greensheet reprint ---------------------------
   2 × 1 inch stock, emitted at 144 × 72 pt (docs/07-deliverables.md §4). These
   are previews of what the labels renderer produces, drawn at the real 2:1
   aspect so a sheet of them reads on screen the way it reads on the bench:
   header label first, then pocket order.

   The pocket stamp is the badge spotfile/labels.py:_stamp adds, in the same
   place — a strip down the left that cannot push the label taller. An item
   with no pocket is stamped "--", never guessed at. -------------------- */
.uv-lbls { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 10px; }
/* background #fff, not var(--surface): the card previews physical label STOCK,
   which is white paper whatever theme the shell wears. */
.uv-lbl { position: relative; aspect-ratio: 2 / 1; border: 1px solid var(--line-strong);
  border-radius: var(--r-sm); background: #fff; padding: 9px 11px 9px 54px; overflow: hidden;
  display: flex; flex-direction: column; justify-content: center; gap: 3px; }
.uv-lbl.is-header { padding-left: 11px; background: var(--sunken); border-style: dashed; }
.uv-lbl-stamp { position: absolute; left: 0; top: 0; bottom: 0; width: 44px; background: var(--ink);
  color: #fff; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.uv-lbl-cap { font-size: 8px; text-transform: uppercase; letter-spacing: var(--tr-caps); opacity: .75; }
.uv-lbl-no { font-size: 21px; font-weight: var(--w-black); line-height: 1; font-variant-numeric: tabular-nums; }
.uv-lbl-o { font-size: var(--t-lead); font-weight: var(--w-black); color: var(--ink);
  line-height: 1.1; word-break: break-all; }
.uv-lbl-m { font-size: var(--t-micro); color: var(--ink-3); word-break: break-all; }
.uv-lbl-hd { font-size: var(--t-micro); font-weight: var(--w-black); text-transform: uppercase;
  letter-spacing: var(--tr-caps); color: var(--ink-2); }
.uv-lbl-sel { position: absolute; right: 7px; top: 7px; width: 24px; height: 24px;
  border-radius: var(--r-sm); border: 2px solid var(--line-strong); display: flex;
  align-items: center; justify-content: center; color: #fff; background: #fff; }

/* THE WHOLE LABEL IS THE TARGET, not a checkbox in its corner. A 2 × 1 inch
   card is 230px wide on screen and it is what a gloved finger goes for; a 24px
   tick in the corner would be a rule-6 failure dressed up as a control. The
   corner mark is the STATE, and the button around it is the target. */
button.uv-lbl { font: inherit; text-align: left; color: inherit; cursor: pointer; width: 100%; }
button.uv-lbl:hover { border-color: var(--accent-line); }
.uv-lbl.is-on { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-tint); }
.uv-lbl.is-on .uv-lbl-sel { background: var(--accent); border-color: var(--accent); }
button.uv-lbl:disabled { cursor: default; }

/* Printing the reprint sheet: everything but the labels goes away, and the
   labels go one per 2 × 1 inch page in the order on screen. */
@media print {
  body.is-printing-labels .uv-pagehead, body.is-printing-labels .uv-stats,
  body.is-printing-labels .uv-crumbs, body.is-printing-labels .uv-section,
  body.is-printing-labels .uv-node, body.is-printing-labels .uv-banner { display: none !important; }
  body.is-printing-labels #labelsheet { display: block !important; }
  body.is-printing-labels .uv-lbls { display: block; }
  body.is-printing-labels .uv-lbl { width: 2in; height: 1in; aspect-ratio: auto;
    break-after: page; border: none; border-radius: 0; }
  body.is-printing-labels .uv-lbl-sel { display: none; }
}

/* ---- MULTI-SELECT FILTER PILLS — added with the Queue -------------------
   cdChips is ONE value per chip: a native picker behind a pill, which is the
   right control for a single choice and the wrong one for a multi-select. The
   queue's family and state filters are multi (docs/12-ui.md §1 uses '|'), and a
   multi-select inside a native picker is the one control a gloved hand on a wall
   panel beside a press cannot work at all.

   So multi-select is a row of toggle pills: every option visible without opening
   anything, every option its own 48px target, and the current selection readable
   from across the room. .uv-chip is reused deliberately — a filter chip is a
   filter chip whether it holds one value or six, and a second pill style would
   just be two vocabularies for one idea.

   THE .uv-filterrow BASE IS NOT DEFINED HERE. The admin/dashboard section lower
   down owns it (flex, wrap, gap, and the width of a .cd-form-input inside it),
   and one class defined twice in one file is exactly the rot this stylesheet is
   organised to avoid. These three rules are only what the pills add to it. --- */
.uv-filterrow-l { font-size: var(--t-micro); font-weight: var(--w-semi); text-transform: uppercase;
  letter-spacing: var(--tr-caps); color: var(--muted); min-width: 58px; }
/* --tap is the floor for anything you toggle, and these are toggled with a
   thumb. The 40px .uv-chip stays 40px everywhere it is a dropdown handle. */
.uv-filterrow .uv-chip { min-height: var(--tap); }
.uv-filter-q { flex: 1 1 220px; }

@media (max-width: 768px) {
  /* On a phone the label would eat the row; the pills say what they filter. */
  .uv-filterrow-l { min-width: 100%; }
}

/* ---- SELECT CELL — added with Buckets -----------------------------------
   Close & merge acts on a set, so the buckets table needs a per-row tick. A bare
   16px checkbox is a 16px target; this is the label around it that makes the
   whole cell one, and the box itself is drawn at 20px so it reads as ticked or
   not from standing distance. ------------------------------------------------ */
.uv-selcell { display: inline-flex; align-items: center; justify-content: center;
  min-width: var(--tap); min-height: var(--tap); cursor: pointer; }
.uv-selcell input { width: 20px; height: 20px; cursor: pointer; accent-color: var(--accent); }

/* ---- PAGE HEAD, THE ONE ITEM OF IT THAT IS OURS ------------------------
   This section briefly held a copy of shell.css's whole page-head block. That
   copy existed because shell.css had been ported from mt3prototypes/hr with an
   `hr- → uv-` rename that stopped short, so the class names shell.js emits were
   styled by nothing; shell.css has since been renamed properly and owns all of
   it again — including the `flex: 0 1 auto; min-width: 0` on the actions row
   that keeps three buttons from pushing a 390px document sideways.

   So the copy is gone and one rule survives it: on a phone the actions row gets
   the full width, because the queue's primary carries a count in its label
   ("Build 37 file-ready batches") and a wrapped primary must not end up beside
   the title fighting it for the same 120px. ---------------------------------- */
@media (max-width: 768px) {
  .uv-pagehead-actions { width: 100%; }
}

/* ---- 28. THUMBNAIL CELL — the artwork beside the barcode ----------------
   Added with the Items tab on batch detail (docs/12-ui.md §3), which is the
   first table in the module to put artwork in a row: nothing already here
   draws an inline fixed-size image, and .uv-pocket-t is absolutely positioned
   inside the bed map and cannot be reused in a cell.

   The MISSING state is a first-class variant rather than a broken <img>.
   Thumbnails are re-hosted at build time (own: mt3_uv_thumbs) precisely
   because HP's own thumbnail URLs are presigned and go dark, so "no thumb" is
   a state that will occur and it has to read as absent rather than as a
   loading failure. THUMBNAIL MISSING stays amber and retryable on the QA
   strip; here it is simply a gap, and it says so. ---------------------- */
.uv-thumb { width: 44px; height: 44px; border-radius: var(--r-sm); border: 1px solid var(--line);
  background: var(--sunken); object-fit: contain; display: inline-block; vertical-align: middle; }
.uv-thumb.is-none { display: inline-flex; align-items: center; justify-content: center;
  border-style: dashed; color: var(--faint); font-size: 9px; text-align: center;
  line-height: 1.1; padding: 2px; }

/* ---- 29. THE WIZARDS — print run + manual build -------------------------
   Four classes, and each one exists because nothing already here does its job.
   Everything else on those two pages is .cd-* or an existing .uv-*. --------- */

/* The rail's steps are NAVIGABLE on a wizard — every step is an address — so
   the .uv-step a page renders as a <button> has to look and behave like one.
   A step that is not open yet stays a <div> and says why in its title. */
button.uv-step { font: inherit; cursor: pointer; text-align: left; }
button.uv-step:hover { border-color: var(--line-3); }

/* The upload drop zone. A LABEL wrapping a hidden file input, so a tap, a
   click and a keyboard both reach the picker, and a drag reaches the same
   handler — there is no hover-only affordance anywhere in it. */
.uv-drop { display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 6px; min-height: 168px; padding: 24px; text-align: center; cursor: pointer;
  border: 2px dashed var(--line-2); border-radius: var(--r);
  background: var(--surface); color: var(--ink-2);
  transition: border-color var(--d1), background var(--d1); }
.uv-drop:hover, .uv-drop:focus-within { border-color: var(--accent-line); background: var(--accent-tint); }
.uv-drop.is-over { border-color: var(--accent); background: var(--accent-tint); }
.uv-drop-ic { color: var(--muted); }
.uv-drop-t { font-size: var(--t-lead); font-weight: var(--w-bold); color: var(--ink); }
.uv-drop-s { font-size: var(--t-meta); color: var(--muted); }

/* Per-file upload progress. Not .uv-stage-bar: that one is display:none under
   900px, where a build timeline can afford to lose it and an upload in flight
   cannot — the phone is exactly where somebody watches a 100 MB file go. */
.uv-prog { height: 10px; margin: 8px 0 4px; border-radius: var(--r-pill);
  background: var(--grey-bg); overflow: hidden; }
.uv-prog-fill { display: block; height: 100%; border-radius: var(--r-pill);
  background: var(--accent); transition: width .12s linear; }

/* A row that was resolved and then EXCLUDED — the non-UV batches on the print
   run's resolution step. Muted, never hidden: the whole point of that step is
   that nothing is dropped silently, so it is still readable and its links
   still work. Colour, not opacity, so the text keeps its contrast. */
.uv-off { color: var(--faint); }
.uv-off .uv-link { color: var(--muted); }
/* As a table ROW (cdTable rowClass): every cell quiets, including images —
   colour, not opacity, so contrast stays measurable; thumbnails desaturate so
   a non-UV row's artwork does not out-shout the UV rows around it. */
tr.uv-off > td { color: var(--faint); }
tr.uv-off .uv-link { color: var(--muted); font-weight: var(--w-med); }
tr.uv-off img { filter: grayscale(1); opacity: .55; }

/* A press file's name is long and has nowhere to break — "Batch 357485-357497
   (10 batches) iPhone 13-(27)Qty spot.pdf" pushes a 375px page sideways unless
   it is told it may break anywhere. Scoped to the two places a filename is
   rendered as a heading or a feed line, not to every mono cell. */
.uv-node-title.uv-mono, .uv-feed-t .uv-mono, .uv-card-t { overflow-wrap: anywhere; }

/* ---- 30. THE FEATURE-FLAG BANNER — a screen that is drawn but not live ---
   /impose and /impositions/:id/edit ship in the prototype and sit behind a
   flag in the app until the own-imposition engine lands (phase 4). They needed
   a banner that is NOT one of the four .uv-banner tones: those four say
   something about the DATA on the page (blocked, warn, info, ok), and this
   says something about the PAGE ITSELF.

   Three things it does that a .uv-banner does not:
     · it carries no close control, at any width. A preview screen that can be
       cleared of its own warning is a screen somebody will later swear was
       live, and there is no undoing that conversation;
     · it is hatched, so it survives greyscale, a photocopy and a dirty screen
       — the same reason the skip pill is hatched;
     · it sits above the breadcrumb rather than inside the content, because it
       is a fact about where you are, not about what you are looking at. ---- */
.uv-flag { display: flex; align-items: flex-start; gap: 12px; padding: 14px 16px;
  margin-bottom: var(--s5); border-radius: var(--r);
  border: 2px dashed var(--violet-line); color: var(--ink-2);
  background: repeating-linear-gradient(135deg,
    var(--violet-bg) 0 10px, var(--surface) 10px 20px); }
.uv-flag-ic { color: var(--violet); flex: none; margin-top: 1px; }
.uv-flag-text { font-size: var(--t-body); line-height: var(--lh-prose); }
.uv-flag-text strong { color: var(--ink); }

/* ---- 31. THE ANCHOR HIT — the row a ?param scrolled you to ---------------
   ?rule=choke_px on a case type, like ?pocket=7 on a sheet, is a URL somebody
   sends. Landing on it has to be unmistakable, or the reader scans a table of
   twenty rows looking for the one they were sent. The same amber as the pocket
   map's selection, so "the one I was sent to" reads the same everywhere. ---- */
.uv-hit > td { background: var(--map-sel); }
.uv-hit > td:first-child { box-shadow: inset 3px 0 0 var(--map-sel-line); }

/* ---- 32. THE ADMIN AND DASHBOARD LAYER ---------------------------------
   Added for dashboard.html, audit-history.html, users.html and settings.html.
   Everything here is a component no other Pulse module needs, which is the
   test for whether it belongs in this file at all:

     the hand-drawn charts   no chart library loads on a wall panel with no
                             route to the internet, and a library's bars are
                             not anchors — this dashboard's are
     the bar list            a chart row that IS a link, label and value and
                             track in one target
     the permission switch   nine of them per person, each one 48px, each one
                             one audit row
     the labelled rule row   settings' and users' shared shape: what it is,
                             what it does, where it came from, and the one
                             control that changes it
     the preselection chip   audit history arriving already filtered, visibly
     the anchor highlight    ?rule= / ?key= / ?event= landing somewhere you can
                             see rather than somewhere you have to hunt for
     the inline diff         old → new as two code spans, which is the whole
                             readable content of a permission_changed row

   No .cd-* rule is overridden here, as everywhere else in this file. -------- */

/* The inline diff and every literal identifier in prose. Not <code> bare:
   nordic.css has no opinion about code and an unstyled one reads as a typo. */
.uv-code { font-family: var(--mono); font-size: .92em; background: var(--sunken);
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 1px 6px;
  color: var(--ink-2); white-space: nowrap; }
.uv-code.is-new { background: var(--ok-bg); border-color: var(--ok-line); color: var(--ok-fg);
  font-weight: var(--w-semi); }

/* Two widgets to a row on a desk, one on a panel or a phone. */
.uv-widgets { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s5); margin-top: var(--s5); }
.uv-widgets > .uv-node { margin: 0; min-width: 0; }

/* ---- charts, drawn by hand ---- */
.uv-chart-wrap { width: 100%; overflow-x: auto; }
/* A floor, not a fixed size: below this the day labels collide. The wrapper
   scrolls rather than the page, which is the same contract the tables keep. */
.uv-chart { width: 100%; height: auto; display: block; min-width: 340px; }
.uv-chart-grid { stroke: var(--line); stroke-width: 1; }
.uv-chart-tick, .uv-chart-x { fill: var(--muted); font-size: 11px; font-family: var(--font); }
.uv-chart-area { fill: var(--accent-tint); }
.uv-chart-line { fill: none; stroke: var(--accent); stroke-width: 2.5;
  stroke-linejoin: round; stroke-linecap: round; }
.uv-chart-bar { fill: var(--map-filled); }
.uv-chart-bar.is-out { fill: var(--mute-bg); }
/* The endpoint is emphasised because "today" is what the chart is for. */
.uv-chart-bar.is-now { fill: var(--accent); }
.uv-chart-dot { fill: var(--surface); stroke: var(--accent); stroke-width: 2; }
.uv-chart-dot.is-now { fill: var(--accent); stroke: var(--surface); stroke-width: 2.5; }
/* An invisible full-height rect makes the whole column the tap target, so a
   two-pixel bar on a thin day is still reachable with a gloved finger. */
.uv-chart-zone { fill: transparent; }
.uv-chart-hit { cursor: pointer; }
.uv-chart-hit:hover .uv-chart-zone, .uv-chart-hit:focus-visible .uv-chart-zone { fill: var(--accent-tint); }
.uv-chart-hit:hover .uv-chart-bar { fill: var(--accent); }

.uv-legend { display: flex; flex-wrap: wrap; gap: var(--s4); margin-top: 10px;
  font-size: var(--t-meta); color: var(--muted); }
.uv-legend i { display: inline-block; width: 14px; height: 10px; border-radius: 2px;
  margin-right: 6px; vertical-align: middle; background: var(--map-filled); }
.uv-legend i.is-line { height: 3px; background: var(--accent); }
.uv-legend i.is-now { background: var(--accent); }

/* ---- the bar list: a chart row that is a link ---- */
.uv-bars { display: flex; flex-direction: column; gap: 4px; }
.uv-bar-row { display: block; padding: 9px 8px; border-radius: var(--r-sm);
  text-decoration: none; color: inherit; min-height: var(--tap); }
.uv-bar-row:hover, .uv-bar-row:focus-visible { background: var(--row-hover); }
.uv-bar-top { display: flex; align-items: baseline; gap: var(--s3); margin-bottom: 6px; }
.uv-bar-label { font-size: var(--t-body); color: var(--ink); font-weight: var(--w-med);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.uv-bar-val { margin-left: auto; font-size: var(--t-meta); color: var(--muted); white-space: nowrap; }
.uv-bar-val b { font-family: var(--mono); font-size: var(--t-body); color: var(--ink);
  font-weight: var(--w-bold); }
.uv-bar-track { height: 10px; border-radius: 5px; background: var(--mute-bg); overflow: hidden; }
.uv-bar-fill { display: block; height: 100%; border-radius: 5px; background: var(--accent); }
.uv-bar-fill.is-pass { background: var(--ok-dot); }
.uv-bar-fill.is-warn { background: var(--warn-dot); }
.uv-bar-fill.is-fail { background: var(--bad-dot); }
/* The 12–36 s band is drawn as a band, not described in a caption. */
.uv-bar-track.is-envelope { box-shadow: inset 0 0 0 2px var(--ok-line); background: var(--ok-bg); }

.uv-actor-cols { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s5); }
/* PRINTED / BUILT / RELEASED OVER THE GATE — the column headings on the
   dashboard's "Who did what". Headings, not placeholders; --faint was 2.35:1
   against the card's ground. */
.uv-actor-title { font-size: var(--t-label); font-weight: var(--w-bold); text-transform: uppercase;
  letter-spacing: var(--tr-caps); color: var(--ink-3); margin-bottom: 6px; }

/* ---- audit history: the preselection, the feed extras ---- */
.uv-presels { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center;
  margin: 0 0 var(--s4); }
.uv-presel { display: inline-flex; align-items: center; gap: 8px; min-height: var(--tap);
  padding: 6px 6px 6px 12px; border-radius: var(--r-pill);
  background: var(--accent-tint); border: 1px solid var(--accent-line); }
.uv-presel-k { font-size: var(--t-micro); text-transform: uppercase; letter-spacing: var(--tr-caps);
  color: var(--accent); font-weight: var(--w-bold); }
.uv-presel-v { font-size: var(--t-body); color: var(--ink); font-weight: var(--w-med); }
.uv-presel-go { font-size: var(--t-meta); }
.uv-presel-x { min-width: 34px; min-height: 34px; border: none; background: transparent;
  color: var(--accent); border-radius: 50%; cursor: pointer; font-size: 15px; }
.uv-presel-x:hover { background: var(--surface); }

/* Label above its own control, each field its own flex column. Written this way
   rather than as a row of bare controls precisely so nothing here has to reach
   into .cd-form-* to fix its spacing — the layer rule holds on this page too. */
.uv-filterrow { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--s3);
  margin-top: var(--s3); }
.uv-filterfield { display: flex; flex-direction: column; gap: 4px; min-width: 170px; }
.uv-filterfield.uv-grow { flex: 1; min-width: 240px; }

.uv-feed-ic.is-good { background: var(--ok-bg); color: var(--ok-fg); }
.uv-feed-ic.is-warn { background: var(--warn-bg); color: var(--warn-fg); }
.uv-feed-ic.is-bad  { background: var(--bad-bg);  color: var(--bad-fg); }
.uv-feed-acts { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: 8px; }
/* A typed reason, verbatim and never truncated: it is the record of somebody's
   decision to send a warned plate to a press. */
.uv-feed-reason { margin-top: 6px; padding: 8px 12px; border-left: 3px solid var(--accent-line);
  background: var(--sunken); border-radius: 0 var(--r-sm) var(--r-sm) 0;
  font-size: var(--t-body); color: var(--ink-2); line-height: var(--lh-prose); }

/* ---- the anchor highlight: ?event= / ?rule= / ?key= landing visibly ---- */
.uv-feed-row.is-hl, .uv-rule.is-hl { background: var(--accent-tint);
  box-shadow: inset 3px 0 0 var(--accent); border-radius: var(--r-sm); }
/* A highlighted TABLE row. The class goes on the <tr> cdTable rendered and the
   selector never names a .cd-* class, so this stays an addition of our own
   state rather than an override of Pulse's component. */
tr.uv-hl > td { background: var(--accent-tint); }
tr.uv-hl > td:first-child { box-shadow: inset 3px 0 0 var(--accent); }

/* ---- the labelled row with one control: settings rules, QA checks,
        connections notes and the nine permissions all share this shape ---- */
.uv-rules, .uv-checks, .uv-perms { display: flex; flex-direction: column; }
.uv-rule, .uv-perm { display: flex; gap: var(--s5); align-items: flex-start;
  padding: 14px 10px; border-bottom: 1px solid var(--line); }
.uv-rule:last-child, .uv-perm:last-child { border-bottom: none; }
.uv-rule-b, .uv-perm-b { flex: 1; min-width: 0; }
.uv-rule-k, .uv-perm-k { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-bottom: 4px; }
.uv-rule-l, .uv-perm-l { font-size: var(--t-node); font-weight: var(--w-semi); color: var(--ink); }
.uv-rule-w, .uv-perm-w { font-size: var(--t-body); color: var(--ink-2); line-height: var(--lh-prose);
  margin-top: 4px; max-width: 78ch; }
/* "What it cannot see" is the half nobody writes down, so it is set apart
   rather than run on from the half everybody does. */
.uv-rule-w.is-cannot { color: var(--ink-2); border-left: 3px solid var(--warn-line);
  padding-left: 10px; background: var(--warn-bg); border-radius: 0 var(--r-sm) var(--r-sm) 0;
  padding-top: 6px; padding-bottom: 6px; }
.uv-rule-m, .uv-perm-m { font-size: var(--t-meta); color: var(--ink-3); margin-top: 6px;
  font-family: var(--mono); }
/* flex:none meant "as wide as my content", and one content was
   "sources 14 d · packages IA at 30 d · Glacier IR at 120 d" at 34px mono —
   about 1100px. It took the whole row, overflowed the card, and squeezed the
   label column to ~30px so every word of the description sat on its own line.
   flex:0 1 auto with a cap lets it give way instead. */
.uv-rule-v { flex: 0 1 auto; display: flex; flex-direction: column; align-items: flex-end;
  gap: 8px; min-width: 140px; max-width: 46%; }
.uv-rule-val { font-family: var(--mono); font-size: var(--t-num); font-weight: var(--w-bold);
  color: var(--ink); text-align: right; overflow-wrap: anywhere; }
/* --t-num is for a NUMBER read across a room. A path, a timezone or a lifecycle
   sentence is not that, and setting it at 34px is what made the row explode in
   the first place. settings.html tags any value over 8 characters. */
.uv-rule-val.is-long { font-size: var(--t-lead); line-height: var(--lh-cell); }
.uv-rule.is-load { background: linear-gradient(90deg, var(--accent-tint), transparent 60%); }
.uv-check-n { display: inline-flex; align-items: center; justify-content: center;
  min-width: 24px; height: 24px; border-radius: 12px; background: var(--mute-bg);
  color: var(--muted); font-size: var(--t-micro); font-weight: var(--w-bold); }

/* ---- the permission switch. role="switch", 48px, and it says its own state
        in words as well as position — a colour alone is not a state. ---- */
.uv-switch { display: inline-flex; align-items: center; gap: 10px; min-height: var(--tap);
  padding: 6px 14px 6px 6px; border-radius: var(--r-pill); cursor: pointer;
  border: 1px solid var(--line-2); background: var(--sunken); color: var(--muted);
  font-size: var(--t-meta); font-weight: var(--w-semi); }
.uv-switch-knob { width: 44px; height: 26px; border-radius: 13px; background: var(--line-2);
  position: relative; flex: none; transition: background var(--ease); }
.uv-switch-knob::after { content: ''; position: absolute; top: 3px; left: 3px;
  width: 20px; height: 20px; border-radius: 50%; background: var(--surface);
  box-shadow: var(--sh-pop); transition: transform var(--ease); }
.uv-switch.is-on { border-color: var(--ok-line); background: var(--ok-bg); color: var(--ok-fg); }
.uv-switch.is-on .uv-switch-knob { background: var(--ok-dot); }
.uv-switch.is-on .uv-switch-knob::after { transform: translateX(18px); }
.uv-perm.is-on { background: linear-gradient(90deg, var(--ok-bg), transparent 55%); }
.uv-permchips { display: inline-flex; flex-wrap: wrap; gap: 4px; }
.uv-permchips a { text-decoration: none; }
/* A Cognito sub is a copy target, and on this page it is one somebody taps with
   a finger. uv.css §21 sets .uv-copy at 32px for the ones that sit inline in a
   sentence; the identity cells get the full touch floor. */
.uv-idcell .uv-copy { min-height: var(--tap); }

/* ---- HP's things and ours never look the same ---- */
.uv-node.is-hp { border-left: 4px solid var(--line-strong); }
.uv-node.is-ours { border-left: 4px solid var(--accent); }
/* The Sunday-only close is the census's whole point; it is marked where it
   appears rather than only explained above the table. */
.uv-sunday { color: var(--warn-fg); font-weight: var(--w-bold); }

@media (max-width: 1000px) {
  .uv-widgets, .uv-actor-cols { grid-template-columns: 1fr; }
}
@media (max-width: 768px) {
  .uv-rule, .uv-perm { flex-direction: column; gap: var(--s3); }
  .uv-rule-v { align-items: flex-start; width: 100%; flex-direction: row; }
  .uv-filterfield, .uv-filterfield.uv-grow { min-width: 100%; }
}


/* ============================================================================
   PAGE PRIMITIVES — moved here 30 Aug when shell.css was retired.
   ----------------------------------------------------------------------------
   shell.css held two unrelated things: the OLD chrome (`.header`, `.sidebar`,
   `.gs`, `.hamburger-btn`, copied from mt3prototypes/hr) and the page skeleton
   every screen assembles from. The chrome is gone — app.css now carries live
   Pulse's own (`.app-header-menu`, `.top-nav`, `.mobile-nav`, `.mt3-dock`) — but
   the skeleton is still what all 28 pages are built out of, so it moves here
   rather than dying with the file it happened to share.

   `.uv-main` did NOT come with it: app.css owns that now, aliased to live
   Pulse's `.app-main` + `.app-page`. shell.css's version set
   `margin-left: var(--sidebar-width)` for a sidebar that no longer exists and
   would have fought the docked panel for the same space.

   Sizes are on this module's scale rather than the 22px/12.5px/14px they
   arrived with: h1 is --t-title (32px), the sub is --t-meta, the node title is
   --t-node. Those literals were HR's desk scale, and carrying them across would
   have quietly undone the floor-panel decision on every page head in the app.
   ========================================================================== */

/* ---- page head: breadcrumb → title + actions → sub. One skeleton, every
   page, so the eye lands in the same place on all twenty-eight. ---- */
.uv-pagehead { margin-bottom: var(--s5); }
.uv-pagehead-top { display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--s5); flex-wrap: wrap; }
.uv-pagehead h1 { font-size: var(--t-title); font-weight: var(--w-bold);
  letter-spacing: var(--tr-title); line-height: var(--lh-tight); margin: 3px 0 0; color: var(--ink); }
.uv-pagehead-sub { font-size: var(--t-meta); color: var(--muted); margin-top: 6px; max-width: 82ch; }
/* flex: 0 1 auto, never flex: none — three buttons on a detail page total
   427px, and a `none` box sizes to max-content and never lets its children
   wrap, which pushed the whole document sideways at 390. */
.uv-pagehead-actions { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  flex: 0 1 auto; min-width: 0; }
/* The eyebrow names the page's section — "REGISTER", "BEFORE HP BATCHES
   ANYTHING" — at 12px uppercase. --faint is 2.56:1 there, which is under the
   3:1 floor even for large text. --ink-3, like every other secondary label. */
.uv-eyebrow { font-size: var(--t-micro); font-weight: var(--w-semi);
  letter-spacing: var(--tr-caps); text-transform: uppercase; color: var(--ink-3); }

/* ---- node: the one section surface. Borders, not shadows. ---- */
.uv-node { background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-surface); overflow: hidden; }
.uv-node-pad { padding: 18px 20px; }
.uv-node-head { display: flex; align-items: center; gap: var(--s3); padding: 14px 18px;
  border-bottom: 1px solid var(--line); flex-wrap: wrap; }
.uv-node-title { font-weight: var(--w-bold); font-size: var(--t-node); color: var(--ink);
  letter-spacing: -.01em; }
.uv-node-spacer { margin-left: auto; }

/* (Two whole primitives came across from shell.css and turned out to be
   emitted by NOTHING — grepped across all 29 pages and js/ on 30 Aug:
   the .uv-fieldgrid form grid with its cols-2/cols-3/span-2/span-all and two
   breakpoints, and the .uv-work + .uv-rail two-column workbench. The pages
   that were expected to want them build their forms out of .uv-kv, .cd-form-*
   rows and .uv-panes instead. Deleted rather than kept "for later": a layout
   system nobody exercises is a layout system that breaks silently the day
   somebody finally reaches for it.) */

/* ---- action bar: one row, bottom, primary right. One primary per view, and
   the reason a disabled primary is disabled sits beside it. ---- */
.uv-actionbar {
  position: sticky; bottom: 0; margin: var(--s5) calc(var(--pad-page) * -1) -80px;
  padding: var(--s3) var(--pad-page);
  background: rgba(255, 255, 255, .92); backdrop-filter: blur(6px);
  border-top: 1px solid var(--line); display: flex; align-items: center; gap: var(--s3);
  justify-content: flex-end; z-index: var(--z-actionbar); min-height: var(--h-actionbar);
}
.uv-actionbar-note { margin-right: auto; font-size: var(--t-meta); color: var(--muted); }
@media (max-width: 768px) {
  .uv-actionbar { margin: var(--s4) -14px -72px; padding: 10px 14px; }
  .uv-pagehead h1 { font-size: var(--t-sub); }
}
@media (max-width: 480px) {
  .uv-actionbar { flex-wrap: wrap; }
  .uv-actionbar-note { width: 100%; margin: 0 0 4px; }
}


/* ---- 33. THE HEADER PRIMARY AT PHONE WIDTH -------------------------------

   "Start print ▾" is OURS — live Pulse's header carries no primary — and at
   375px it was the thing that pushed the document sideways. Measured: page
   scrollWidth 387 against a 375 viewport, on every page, which fails the first
   line of DEVICE_TEST_CHECKLIST §1.

   The arithmetic is worth writing down, because the cause is not the button.
   The chrome ports at live Pulse's sizes — a 36px hamburger and a 38px search
   trigger — and this module raises both to the 48px tap floor. That is the
   right call for a gloved finger and it is +20px of header. Add a ~130px
   labelled primary and the row no longer fits a phone.

   So the LABEL yields, not the target: below 560 the button is the printer
   glyph and the caret, square, still 48px, still the same control with the
   same menu. The accessible name stays on the button, so a screen reader
   still hears "Start print".
   -------------------------------------------------------------------------- */
@media (max-width: 560px) {
  .uv-primary-label { display: none; }
  /* .uv-primary-btn, not `.uv-primary .cd-btn` — app.css owns the .cd-*
     library and a rule whose subject is one of its classes is an override
     however it is scoped. shell.js puts our own class on the button instead. */
  .uv-primary-btn { padding: 0 10px; gap: 4px; }
}

/* ---- 34. THE SELECTION PREVIEW — artwork of what is about to be acted on ---

   A count cannot tell you that you pasted last week's batch list. The strip
   can, so it sits under every selection that leads to an expensive action.

   It is a CONFIRMATION surface, not a gallery, and the layout says so: rows
   grouped by what was selected, a hard cap with the remainder as a real
   control, and a tile that is missing its picture drawn as missing rather than
   as a blank square. On a phone the tiles scroll inside their own row — never
   the page.
   -------------------------------------------------------------------------- */
.uv-preview { border: 1px solid var(--line); border-radius: var(--r-surface);
  background: var(--surface); overflow: hidden; }
.uv-preview.is-empty { padding: 18px 20px; color: var(--muted); font-size: var(--t-meta);
  background: var(--sunken); }
.uv-preview-head { display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap;
  padding: 12px 16px; border-bottom: 1px solid var(--line); background: var(--sunken); }
.uv-preview-t { font-size: var(--t-label); font-weight: var(--w-semi);
  text-transform: uppercase; letter-spacing: var(--tr-caps); color: var(--muted); }
.uv-preview-n { font-size: var(--t-cell); font-weight: var(--w-bold); color: var(--ink);
  font-variant-numeric: tabular-nums; }
/* Amber, and never hidden: if a third of the strip has no picture the operator
   is checking a third less than they think they are. */
.uv-preview-miss { font-size: var(--t-micro); font-weight: var(--w-semi);
  color: var(--warn-fg); background: var(--warn-bg); border: 1px solid var(--warn-line);
  border-radius: var(--r-badge); padding: 2px 8px; margin-left: auto; }

.uv-preview-group + .uv-preview-group { border-top: 1px solid var(--line); }
.uv-preview-l { display: flex; align-items: center; gap: var(--s2); padding: 10px 16px 0;
  font-size: var(--t-cell); font-weight: var(--w-semi); }
.uv-preview-x { margin-left: auto; width: var(--tap); height: var(--tap); border: none;
  background: none; color: var(--ink-4); font-size: 20px; cursor: pointer;
  border-radius: var(--r-ctl); }
.uv-preview-x:hover { background: var(--bad-bg); color: var(--bad-fg); }

.uv-tiles { display: flex; gap: var(--s2); padding: 10px 16px 14px;
  overflow-x: auto; overscroll-behavior-x: contain; }
.uv-tiles.is-inline { padding: 0; }
.uv-tile { flex: none; width: 68px; display: flex; flex-direction: column; align-items: center;
  gap: 4px; text-decoration: none; color: var(--ink-3); }
.uv-tile img { width: 68px; height: 68px; object-fit: contain; background: var(--sunken);
  border: 1px solid var(--line-2); border-radius: var(--r-ctl); }
.uv-tile:hover img, .uv-tile:focus-visible img { border-color: var(--accent); }
.uv-tile-c { font-size: 10.5px; max-width: 68px; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; font-family: var(--mono); }
/* Hatched, like every other "we could not look" in this module — the same
   visual language as a skipped self-test check, for the same reason. */
.uv-tile.is-none img, .uv-tile-gap { width: 68px; height: 68px; border-radius: var(--r-ctl);
  border: 1px dashed var(--line-3); background: var(--qa-skip-hatch);
  display: flex; align-items: center; justify-content: center;
  font-size: 10px; color: var(--muted); text-align: center; }
.uv-tile.is-more { width: 68px; height: 68px; flex: none; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 2px; border: 1px solid var(--line-2);
  border-radius: var(--r-ctl); background: var(--sunken); cursor: pointer;
  font-size: var(--t-cell); font-weight: var(--w-bold); color: var(--accent); }
.uv-tile.is-more span { font-size: 10px; font-weight: var(--w-med); color: var(--muted); }
.uv-tile.is-more.is-static { cursor: default; color: var(--muted); }
.uv-tile.is-more:hover { border-color: var(--accent); }

/* ---- 30b. TWO THINGS OF DIFFERENT KINDS, SIDE BY SIDE — added 30 Aug by the
   ("30b": eight authors numbered §30–33 twice; the b-halves keep their old
   numbers findable rather than renumbering references in js/ comments.)
   visual audit. Three defects, one shape.

   A run of bare identifiers joined by a space reads as ONE identifier. The
   item page printed "#5262804 349271 iPad" — an order, a batch and a case
   type — and there was nothing on the screen to say where one ended. The
   pocket cards printed "CS-LP-14 #5261669" in the same colour, weight and
   mono face. The fix is not more spacing: it is that each id SAYS WHAT KIND IT
   IS, quietly, and sits in its own cell so the eye can find the boundary
   without reading the characters.

   Use it wherever adjacent chips are of different kinds. Where they are the
   same kind (five batch numbers) a separator is enough and this is overkill.
   ------------------------------------------------------------------------ */
.uv-tag { display: inline-flex; align-items: baseline; gap: var(--s2);
  max-width: 100%; padding: var(--s1) var(--s2);
  border: 1px solid var(--line); border-radius: var(--r-badge);
  background: var(--sunken); line-height: var(--lh-cell); }
.uv-tag-k { flex: none; font-size: var(--t-micro); font-weight: var(--w-bold);
  text-transform: uppercase; letter-spacing: var(--tr-caps); color: var(--ink-3); }
.uv-tag > .uv-link, .uv-tag > .uv-mono { min-width: 0; overflow-wrap: anywhere; }
/* A run of them wraps as a set rather than as words. */
.uv-tags { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center; }

/* ---- 31b. A COLUMN THAT MAY NOT COLLAPSE --------------------------------
   `.cd-table` is width:100% with table-layout:auto, so a column whose text can
   break at a hyphen shrinks to nothing while the register refuses to use the
   horizontal scroller it already has. Measured on 30 Aug at 1440: the sheets
   register gave "Batch 358902-358728-358487-357472-357411-357330 iPad 10.9
   (10th Gen A16)-(20)Qty" a 116px column and drew it SIXTEEN lines tall, which
   made every other cell in that row a 16-line cell too.

   `wide: true` on a cdTable column puts this class on its <th> and every <td>.
   A floor on the cell width raises the table's own minimum width past the
   container's, and .cd-table-container scrolls — which is the documented
   behaviour for wide content and the whole reason it is a scroller.

   Named on the CELL, never on `.cd-table`: the layer rule is that uv.css may
   use a .cd-* name but never define one, so the selector is element+class. */
td.uv-wide, th.uv-wide { min-width: 210px; }
td.uv-wide-sm, th.uv-wide-sm { min-width: 140px; }

/* ---- 32b. A STACK INSIDE A TABLE CELL -----------------------------------
   Two rules in the buckets register's "Our rule" cell ran on as one sentence —
   "Close reprint buckets daily [off] [demo row] Close jig-compatible buckets
   together [off] [demo row]" — because each was inline and they were joined
   with a space. One per line, and the boundary is free. */
.uv-stack { display: flex; flex-direction: column; gap: var(--s1); }
.uv-stack > * { min-width: 0; }

/* ---- 35. THE DROP ZONE'S FOOT AND ITS DONE STATE -------------------------
   The template download used to be a full secondary BUTTON sitting under the
   file input, competing with the wizard's own primary for attention. It is a
   convenience, not a step, so it is a ghost link under the zone.

   `.is-done` is the zone after it has read a file: it stops asking for one and
   states what it got, including what it REJECTED. "Read 40" when the file held
   60 is the moment to notice, not the next screen.
   -------------------------------------------------------------------------- */
.uv-drop-foot { display: flex; justify-content: flex-end; margin-top: 6px; }
.uv-drop.is-done { cursor: default; border-style: solid; border-color: var(--ok-line);
  background: var(--ok-bg); }
.uv-drop.is-done .uv-drop-ic { color: var(--ok-dot); }
.uv-drop.is-done .uv-drop-t { color: var(--ok-fg); }
.uv-drop.is-done .uv-drop-s { color: var(--ink-2); max-width: 46ch; }
/* A textarea that fits its content. Height is set inline by grow(); this is
   only the floor and the "stop growing" ceiling's scrollbar. */
.uv-grow { min-height: 96px; max-height: 360px; resize: vertical; }

/* ---- 36. THE REPRINT REGISTER -------------------------------------------
   `.uv-bad` is the only thing this screen needed that did not exist: a number
   that has crossed a threshold and should read as a fact rather than a value.
   327 in a column of numbers is a number; 327 in --bad-fg beside a red chip is
   an item somebody forgot for the better part of a year.
   -------------------------------------------------------------------------- */
.uv-bad { color: var(--bad-fg); font-weight: var(--w-bold); }

/* ---- 33b. A NON-UV BATCH ON AN ORDER PAGE -------------------------------
   Read-only, greyed and inert by design. It lived in an inline style
   attribute with a literal 10px in it, which is both a call-site literal and
   off the 4px grid; it is a class now. */
.uv-nonuv { display: flex; flex-wrap: wrap; gap: var(--s3); align-items: center;
  padding: var(--s3) 0; border-top: 1px solid var(--line); color: var(--muted); }
.uv-nonuv:first-child { border-top: none; }

/* ---- 37. THE QUICK PRINT BAR -------------------------------------------
   A header popover, not a modal: it is a two-step ACTION with a preview in
   the middle, and it must not black out the page behind it — an operator
   checks the panel against the sheet they were already looking at.

   It reuses .cd-search-pop's box so the two header panels are visibly the
   same kind of thing; only the inside differs.
   -------------------------------------------------------------------------- */
/* .cd-search-pop is a POSITIONER, not a card — the palette draws its own
   surface inside it. This panel puts its content straight in, so it has to
   bring the card itself; without it the page showed straight through the
   panel and the two sets of text overlapped. */
.uv-quick-pop { width: min(460px, 92vw); padding: 16px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-surface); box-shadow: var(--sh-modal); }
/* Our own classes as the subject, never a .cd-* — app.css owns that library
   and a rule whose subject is one of its classes is an override however it is
   scoped. The first cut of this block reached for .cd-form-input and .cd-btn-
   primary and check.sh caught it, which is the layer rule doing its job on its
   own author. */
.uv-quick-row { display: flex; gap: var(--s2); }
.uv-quick-in { flex: 1; min-width: 0; }
.uv-quick-empty { margin-top: 14px; padding: 16px; border-radius: var(--r);
  background: var(--sunken); border: 1px dashed var(--line-2);
  font-size: var(--t-meta); color: var(--muted); }
.uv-quick-res { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--line); }
.uv-quick-t { font-size: var(--t-lead); font-weight: var(--w-semi); margin-bottom: 10px; }
.uv-quick-miss { margin-top: 8px; font-size: var(--t-micro); color: var(--warn-fg);
  background: var(--warn-bg); border: 1px solid var(--warn-line);
  border-radius: var(--r-badge); padding: 6px 9px; }
.uv-quick-foot { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--line); }
.uv-quick-go { flex: 1; }
/* On a phone the panel is the width of the screen and the print button is the
   thing a thumb reaches for, so it goes full width and the test label sits
   under it rather than beside. */
@media (max-width: 560px) {
  .uv-quick-pop { width: 96vw; right: -8px; }
  .uv-quick-foot { flex-direction: column; align-items: stretch; }
}

/* The printer chooser inside the quick bar. A refusal you can act on in one
   tap, rather than a link to a settings page and a retyped batch number. */
.uv-quick-pick { flex: 1; display: flex; flex-direction: column; gap: 6px; }
.uv-quick-pick-l { font-size: var(--t-meta); color: var(--ink-2); }
.uv-quick-pick-b { justify-content: flex-start; gap: 8px; }

/* The item hero's artwork, once it exists. Contained, not cropped: the
   operator compares this against a physical case, and a crop that hides the
   design's edge is a comparison that can lie. */
.uv-item-thumb.is-art { display: flex; align-items: center; justify-content: center;
  padding: 10px; background: var(--sunken); }
.uv-item-thumb.is-art img { max-width: 100%; max-height: 320px; object-fit: contain;
  border-radius: var(--r-ctl); }

/* ---- 38. GROUPED BY — the column that says what binds a group ------------
   Bold = every member shares this value; a plain list = the actual mixed
   values, never the word "Multi" (docs/14 §7.1 row 2 — their requirements
   call this "THE most important column" and the reasoning holds here). ---- */
.uv-groupedby { display: inline-flex; align-items: baseline; gap: 6px; flex-wrap: wrap;
  font-size: var(--t-meta); color: var(--ink-2); }
.uv-groupedby-d strong { font-weight: var(--w-bold); color: var(--ink); }
.uv-groupedby-sep { color: var(--ink-5); }

/* ---- 39. THE MERGE VERDICT — an answer, not a greyed button --------------
   Renders continuously while buckets are ticked: green with the saving, red
   with the exact blocking reason. The {ok, why} shape is the batching
   prototypes' (docs/14 §7.1 row 3); the rules are ours. ------------------- */
.uv-verdict { display: flex; align-items: center; gap: 10px; padding: 12px 16px;
  border-radius: var(--r); font-size: var(--t-body); margin: -6px 0 14px; }
.uv-verdict.is-ok { background: var(--ok-bg); border: 1px solid var(--ok-line); color: var(--ok-fg); }
.uv-verdict.is-bad { background: var(--bad-bg); border: 1px solid var(--bad-line); color: var(--bad-fg); }
.uv-verdict svg { flex: none; }

/* ---- 40. MOBILE CARDS — the register below 1024px ------------------------
   docs/14 §7.1 row 5: their grids swap to a card list on a phone instead of
   a sideways scroller. Ours re-renders (cdTable's matchMedia edge) so these
   are the SORTED, FILTERED, PAGED rows as real DOM — never a hidden table.
   One card = one row: identity link + status chips on the first line, the
   2-3 numbers that matter on the last, and the identity link itself is the
   ≥48px tap target (--tap floor, DEVICE_TEST_CHECKLIST §1). ---------------- */
.uv-cardlist { display: flex; flex-direction: column; gap: 10px; }
.uv-cardrow { background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r); padding: 4px 14px 12px; }
/* The identity line. Links inside it get the tap floor via line box, not
   padding, so two chips beside the link do not wrap onto a second line. */
.uv-cardrow-title { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font-size: var(--t-body); }
.uv-cardrow-title .uv-link, .uv-cardrow-title .uv-copy {
  display: inline-flex; align-items: center; min-height: var(--tap); }
.uv-cardrow-sub { font-size: var(--t-meta); color: var(--muted); margin-top: -4px; }
/* The numbers row: value bold and dark, label quiet, dot-separated by gap. */
.uv-cardrow-stats { display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap;
  margin-top: 8px; font-size: var(--t-meta); color: var(--muted); }
.uv-cardrow-stat b { font-size: var(--t-cell); font-weight: var(--w-bold);
  color: var(--ink); font-variant-numeric: tabular-nums; }
/* Row actions and select cells carried into a card sit on their own line so
   a thumb aiming at "Build" is nowhere near the identity link. */
.uv-cardrow-act { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-top: 8px; }
/* A greyed row is a greyed card — same colour-not-opacity rule as tr.uv-off. */
.uv-cardrow.uv-off { color: var(--faint); }
.uv-cardrow.uv-off .uv-link { color: var(--muted); }
.uv-cardrow.uv-off img { filter: grayscale(1); opacity: .55; }

/* ---- 41. VOID — never delete (docs/14 §2.7) ------------------------------
   A voided sheet or build is KEPT, greyed, chipped, and excluded from
   latest-only views and counts. Grey is colour, not opacity (the uv-off
   rule): contrast survives, and links inside stay readable. The chip is the
   batching prototypes' mapping — void renders in the neutral palette, not
   red, because a void is a finished decision, not an alarm. ---------------- */
.uv-voidchip { display: inline-flex; align-items: center; padding: 2px 8px;
  border-radius: var(--r-badge); font-size: var(--t-micro); font-weight: var(--w-bold);
  text-transform: uppercase; letter-spacing: var(--tr-caps);
  color: var(--ink-2); background: var(--sunken); border: 1px solid var(--line-2); }
/* The whole-page grey on a voided sheet's detail: headline and numbers step
   back so the void banner (a .uv-banner.is-warn, styled above) is the loudest
   thing on the page. Content stays legible — this is a record, not a redaction. */
.uv-voided .uv-pagehead h1 { color: var(--muted); text-decoration: line-through;
  text-decoration-color: var(--line-3); text-decoration-thickness: 2px; }
.uv-voided .uv-stats { color: var(--muted); }
.uv-voided .uv-thumb, .uv-voided .uv-item-thumb img { filter: grayscale(1); opacity: .55; }
/* The barcode roll-call in the void banner: one register line, wrapping,
   mono — the audit row NAMES every returned barcode (log-before-clear,
   docs/14 §2.7), and names are read back, so they are not truncated. */
.uv-voidcodes { display: flex; flex-wrap: wrap; gap: 4px 10px; margin-top: 6px;
  font-family: var(--mono); font-size: var(--t-micro); color: var(--ink-2); }

/* ---- 42. PORTAL COLUMN FILTERS (docs/14 §7.1 row 6) ----------------------
   The dropdown is position:fixed and appended to <body> — a portal — so it
   escapes .cd-table-container's overflow instead of being clipped by the
   scroller it serves. Anchored to the header cell by getBoundingClientRect,
   clamped to the viewport. ------------------------------------------------- */
/* The header control is a WORD, lit while its filter bites. It is visually
   compact (a 48px-tall button would double every header row) so the tap
   target is met by the ::after halo instead — the hit area extends past the
   paint to the --tap floor without moving a neighbour. */
.uv-colfilter-btn { position: relative; margin-left: 7px; padding: 2px 7px;
  border: 1px solid var(--line-2); border-radius: var(--r-badge);
  background: var(--surface); color: var(--muted); font-size: var(--t-micro);
  font-weight: var(--w-semi); cursor: pointer; }
.uv-colfilter-btn::after { content: ''; position: absolute; inset: -12px; }
.uv-colfilter-btn:hover { border-color: var(--line-3); color: var(--ink-2); }
.uv-colfilter-btn.is-on { background: var(--accent-tint); border-color: var(--accent-line);
  color: var(--accent); }
.uv-colfilter-pop { position: fixed; z-index: var(--z-popover); width: 264px;
  background: var(--surface); border: 1px solid var(--line-2); border-radius: var(--r);
  box-shadow: var(--sh-pop); padding: 10px; display: flex; flex-direction: column;
  gap: 8px; }
.uv-colfilter-head { font-size: var(--t-micro); font-weight: var(--w-semi);
  text-transform: uppercase; letter-spacing: var(--tr-caps); color: var(--muted); }
.uv-colfilter-list { max-height: 280px; overflow-y: auto; display: flex;
  flex-direction: column; }
.uv-colfilter-row { display: flex; align-items: center; gap: 9px; padding: 0 6px;
  min-height: var(--tap); border-radius: var(--r-sm); cursor: pointer;
  font-size: var(--t-cell); }
.uv-colfilter-row:hover { background: var(--row-hover); }
.uv-colfilter-row input { width: 18px; height: 18px; accent-color: var(--accent);
  flex: none; }
.uv-colfilter-foot { display: flex; justify-content: space-between; gap: 8px; }
/* Active filters as chips in the toolbar — an invisible active filter is a
   register quietly hiding rows with nothing on screen to turn it off (the
   same rule reprints.html states for its entry pills). */
.uv-tablefilters { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }

/* ---- 43. COLUMN ORDER — the Up/Down pair in the column modal -------------
   §16 owns the row box; these are only what ORDER adds to it. Words, not
   drag handles: dragging inside a scrolling modal on a touch panel fights
   the scroll, and two buttons cannot (docs/14 §7.1 row 7). ----------------- */
.uv-coltoggle-lab { display: flex; align-items: center; gap: 10px; flex: 1;
  cursor: pointer; min-width: 0; }
.uv-coltoggle-lab.is-locked { opacity: .65; cursor: default; }
.uv-colorder { display: inline-flex; gap: 4px; margin-left: auto; flex: none; }
.uv-colorder-btn { min-height: 40px; min-width: 52px; padding: 0 10px;
  border: 1px solid var(--line-2); border-radius: var(--r-ctl);
  background: var(--surface); color: var(--ink-2); font-size: var(--t-micro);
  font-weight: var(--w-semi); cursor: pointer; }
.uv-colorder-btn:hover:not(:disabled) { border-color: var(--line-3); background: var(--row-hover); }
.uv-colorder-btn:disabled { color: var(--faint); cursor: default; }

/* ---- 44. THE TICKET STRIP (docs/14 §7.1 row 8) ---------------------------
   The dense 4-cell key-number strip for a detail SUB-SECTION where statStrip
   is too heavy — build.html's timing block is the first tenant. auto-fit so
   four cells at 1440 become two rows of two on a phone without a query. The
   numbers are --t-sub, not --t-num: a ticket is read at the desk inside a
   section, not across a room, and must not compete with the page's strip. -- */
.uv-ticket { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 1px; background: var(--line); border: 1px solid var(--line);
  border-radius: var(--r); overflow: hidden; margin-bottom: 14px; }
.uv-ticket-cell { background: var(--sunken); padding: 9px 12px; }
.uv-ticket-l { font-size: var(--t-micro); font-weight: var(--w-semi);
  text-transform: uppercase; letter-spacing: var(--tr-caps); color: var(--muted); }
.uv-ticket-v { font-size: var(--t-sub); font-weight: var(--w-bold); color: var(--ink);
  font-variant-numeric: tabular-nums; margin-top: 2px; }
.uv-ticket-v .uv-muted { font-size: var(--t-meta); font-weight: var(--w-reg); }

/* ---- 45. THE REGISTER'S ARTWORK ROW — three thumbs in a Preview cell ----- */
.uv-thumbrow { display: inline-flex; gap: 3px; }
.uv-thumbrow .uv-thumb { width: 34px; height: 34px; object-fit: cover;
  border-radius: var(--r-sm); border: 1px solid var(--line); }

/* ---- 46. SCAN FEEDBACK on the quick bar (docs/14 §7.1 row 4) -------------
   The input IS the scan surface (a gun types + Enters), so it confirms where
   the eye already is: green flash = matched, red = no match. Beep and haptic
   are the panel's own toggles, off by default. -------------------------- */
.uv-quick-in { font-family: var(--mono); font-size: var(--t-lead); letter-spacing: .02em; }
.uv-quick-in.is-hit  { animation: uv-scan-hit .55s; }
.uv-quick-in.is-miss { animation: uv-scan-miss .55s; }
@keyframes uv-scan-hit  { 0% { box-shadow: 0 0 0 3px var(--ok-line); border-color: var(--ok-dot); }
  100% { box-shadow: none; } }
@keyframes uv-scan-miss { 0%, 40% { box-shadow: 0 0 0 3px var(--bad-line); border-color: var(--bad-dot); }
  100% { box-shadow: none; } }
.uv-quick-fx { display: flex; gap: 6px; margin-top: 8px; }

/* ---- 47. THE PRINTER SELECTOR on the quick bar ---------------------------
   Live Pulse's PrinterSelector.jsx, in this module's idiom (user, 30 Aug:
   "it should show which printer" + "i think we can make it much better").
   A chip that always states the printer AND whose choice it is — "local"
   (this station's override, purple like live's tag) or "server default"
   (config_master's real BULK_SETTINGS value) — opening a list with driver ·
   DPI detail, Refresh, Setup help and a clear-override row. The setup guide
   is §47b: a calm card, status first, because needing setup is a state, not
   an emergency. */
.uv-prsel { position: relative; margin-top: 12px; }
.uv-prsel-chip { display: flex; align-items: center; gap: 8px; width: 100%;
  min-height: 40px; padding: 8px 12px; border-radius: var(--r-ctl);
  background: var(--sunken); border: 1px solid var(--line-2);
  font-size: var(--t-meta); color: var(--ink-2); cursor: pointer; text-align: left; }
.uv-prsel-chip:hover { border-color: var(--line-3); background: var(--surface); }
.uv-prsel-chip svg { flex: none; color: var(--ink-4); }
.uv-prsel-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; font-weight: var(--w-semi); color: var(--ink); }
.uv-prsel-name.is-none { font-weight: var(--w-reg); font-style: normal;
  color: var(--muted); }
/* The source tag: WHOSE choice is in effect. Purple = this station overrode
   the server (live's "(local)" tag colour); quiet = the server default. */
.uv-prsel-src { flex: none; font-size: var(--t-micro); font-weight: var(--w-semi);
  padding: 2px 7px; border-radius: var(--r-badge);
  background: var(--sunken); border: 1px solid var(--line-2); color: var(--muted); }
.uv-prsel-src.is-local { background: rgba(102, 126, 234, .12);
  border-color: rgba(102, 126, 234, .35); color: var(--nav-ink); }
.uv-prsel-caret { flex: none; color: var(--ink-5); font-size: 11px; }
/* The dropdown OVERLAYS the panel, anchored to the chip — in flow it shoved
   the label preview and the print button below the fold as soon as a machine
   had more than a couple of queues (user's screenshot, 30 Aug). Height-capped
   with its own scroll, so five printers or fifteen cost the same screen. The
   head stays sticky while the list scrolls. */
.uv-prsel-dd { position: absolute; top: calc(100% + 6px); left: 0; right: 0;
  z-index: 5; max-height: min(340px, 46vh); overflow-y: auto;
  border: 1px solid var(--line); border-radius: var(--r-ctl);
  background: var(--surface); box-shadow: var(--sh-modal); }
.uv-prsel-head { display: flex; align-items: center; gap: 6px; padding: 8px 10px;
  border-bottom: 1px solid var(--line); font-size: var(--t-micro);
  font-weight: var(--w-semi); color: var(--ink-3);
  text-transform: uppercase; letter-spacing: .04em;
  position: sticky; top: 0; background: var(--surface); z-index: 1; }
.uv-flex-spacer { flex: 1; }
.uv-prsel-server { padding: 8px 12px; font-size: var(--t-micro); color: var(--ink-3);
  background: var(--sunken); border-bottom: 1px solid var(--line); }
.uv-prsel-server-note { color: var(--faint); }
.uv-prsel-row { display: flex; align-items: center; gap: 10px; width: 100%;
  min-height: var(--tap); padding: 8px 12px; border: 0; background: none;
  font-size: var(--t-meta); color: var(--ink); cursor: pointer; text-align: left; }
.uv-prsel-row:hover { background: var(--row-hover); }
.uv-prsel-row.is-on { background: rgba(102, 126, 234, .08);
  box-shadow: inset 3px 0 0 var(--nav-ink); font-weight: var(--w-semi); }
.uv-prsel-row-n { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.uv-prsel-row-d { flex: none; font-size: var(--t-micro); color: var(--muted);
  font-variant-numeric: tabular-nums; }
.uv-prsel-none { padding: 12px; font-size: var(--t-meta); color: var(--muted); }
/* On a phone the driver detail goes UNDER the name — beside it, both wrapped
   mid-word and neither could be read (user's screenshot, 30 Aug). */
@media (max-width: 560px) {
  .uv-prsel-row { flex-direction: column; align-items: flex-start; gap: 2px; }
}
.uv-prsel-clear { display: block; width: 100%; min-height: var(--tap);
  padding: 10px 12px; border: 0; border-top: 1px solid var(--line);
  background: var(--sunken); font-size: var(--t-micro); color: var(--ink-3);
  cursor: pointer; text-align: left; }
.uv-prsel-clear:hover { color: var(--ink); background: var(--row-hover); }
/* A link that is a button — the setup affordance inside the "not running"
   sentence. Same look as .uv-link, honest about being a control. */
.uv-link-btn { border: 0; background: none; padding: 0; font-size: inherit;
  color: var(--link); text-decoration: underline; cursor: pointer; }

/* ---- 47b. THE SETUP GUIDE ------------------------------------------------ */
.uv-prsetup { padding: 0; }
.uv-prsetup .uv-prsel-head { text-transform: none; letter-spacing: 0;
  font-size: var(--t-meta); }
.uv-prsetup-steps { margin: 0; padding: 10px 12px 10px 30px;
  font-size: var(--t-micro); color: var(--ink-2); }
.uv-prsetup-steps li { margin: 6px 0; line-height: 1.5; }
.uv-prsetup-foot { display: flex; gap: 8px; padding: 10px 12px;
  border-top: 1px solid var(--line); }
.uv-prsetup-foot > button { flex: 1; }
/* The allowed-with-a-caveat note above the print button (§47 companion). */
.uv-quick-adv { margin-top: 12px; padding: 8px 10px; border-radius: var(--r-badge);
  background: var(--warn-bg); border: 1px solid var(--warn-line);
  color: var(--warn-fg); font-size: var(--t-micro); line-height: 1.45; }

/* ---- 48. THE DOWNLOAD CONTROL (js/core.js cdDownload) --------------------
   A split button — the package on the left, the file list under the caret —
   and a portal panel, fixed-positioned like the column filter so an action
   bar's overflow cannot clip it. Every row clears --tap: this is the control
   a gloved finger uses to take a press file off the machine. --------------- */
.uv-dl { display: inline-flex; align-items: stretch; }
.uv-dl.is-block { display: flex; width: 100%; }
/* The two halves read as ONE button: the seam is a hairline, not a gap, so
   the pair is never mistaken for two separate actions. */
.uv-dl-go { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.uv-dl.is-block .uv-dl-go { flex: 1; }
.uv-dl-caret { border-top-left-radius: 0; border-bottom-left-radius: 0;
  padding: 0 10px; border-left: 1px solid rgba(255, 255, 255, .28); }
.uv-dl-caret svg { width: 16px; height: 16px; }
/* A ghost or secondary split button has no filled ground for a white hairline
   to sit on, so the seam takes the line colour instead. */
.uv-dl.is-quiet .uv-dl-caret { border-left-color: var(--line-2); }

.uv-dl-pop { position: fixed; z-index: var(--z-popover); width: 380px;
  max-width: calc(100vw - 16px); background: var(--surface);
  border: 1px solid var(--line-2); border-radius: var(--r);
  box-shadow: var(--sh-pop); display: flex; flex-direction: column; }
.uv-dl-head { display: flex; align-items: center; justify-content: space-between;
  gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--line);
  font-size: var(--t-micro); font-weight: var(--w-semi); color: var(--muted);
  text-transform: uppercase; letter-spacing: var(--tr-caps); }
.uv-dl-pick-all { min-height: 32px; padding: 0 8px; border: 0; background: none;
  color: var(--link); font-size: var(--t-micro); font-weight: var(--w-semi);
  text-transform: none; letter-spacing: 0; cursor: pointer; }
.uv-dl-pick-all:hover { text-decoration: underline; }
.uv-dl-list { max-height: 46vh; overflow-y: auto; }
.uv-dl-row { display: flex; align-items: center; gap: 10px; padding: 0 12px;
  min-height: var(--tap); border-bottom: 1px solid var(--line); }
.uv-dl-row:last-child { border-bottom: 0; }
.uv-dl-row:hover { background: var(--row-hover); }
.uv-dl-pick { display: flex; align-items: center; gap: 9px; flex: 1;
  min-width: 0; min-height: var(--tap); cursor: pointer; }
.uv-dl-pick input { width: 18px; height: 18px; accent-color: var(--accent);
  flex: none; }
.uv-dl-name { font-size: var(--t-cell); color: var(--ink); overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.uv-dl-meta { font-size: var(--t-micro); color: var(--muted); white-space: nowrap;
  flex: none; }
/* The sentence that keeps the control honest: it changes with the selection
   and says which of the two things is about to happen. */
.uv-dl-note { padding: 8px 12px; border-top: 1px solid var(--line);
  background: var(--sunken); font-size: var(--t-micro); color: var(--ink-3);
  line-height: 1.45; }
.uv-dl-foot { display: flex; align-items: center; justify-content: space-between;
  gap: 8px; padding: 8px 12px; border-top: 1px solid var(--line); }
/* At 375 the panel is the width of the screen and the per-file button drops
   under the name — three columns in 359px is three unreadable ones. */
@media (max-width: 560px) {
  .uv-dl-row { flex-wrap: wrap; padding: 8px 12px; }
  .uv-dl-pick { flex: 1 0 100%; }
  .uv-dl-meta { flex: 1; }
}

/* ---- 49. THE SEARCHABLE PICKER (uv-pick) ---------------------------------
   The panel a chip opens once its list outgrows a native <select> — long
   local vocabularies (19 iPhone models) and remote ones (batch numbers, SKUs
   out of 1.35M item rows). Same portal treatment as §42's column filter: it
   is appended to <body> and fixed-positioned, because a chip usually sits
   inside the very scroller that would clip its own dropdown.
   Single-select, so rows are buttons with a tick column rather than
   checkboxes — the shape tells you it takes one answer, not several. ------- */
.uv-pick-pop { position: fixed; z-index: var(--z-popover); width: 300px;
  max-width: calc(100vw - 16px); background: var(--surface);
  border: 1px solid var(--line-2); border-radius: var(--r); box-shadow: var(--sh-pop);
  padding: 10px; display: flex; flex-direction: column; gap: 8px; }
.uv-pick-head { font-size: var(--t-micro); font-weight: var(--w-semi);
  text-transform: uppercase; letter-spacing: var(--tr-caps); color: var(--muted); }
.uv-pick-q[hidden] { display: none; }
.uv-pick-list { max-height: 300px; overflow-y: auto; display: flex;
  flex-direction: column; }
.uv-pick-row { display: flex; align-items: center; gap: 8px; padding: 0 6px;
  min-height: var(--tap); border: 0; background: none; border-radius: var(--r-sm);
  cursor: pointer; font: inherit; font-size: var(--t-cell); color: var(--ink);
  text-align: left; width: 100%; }
.uv-pick-row:hover { background: var(--row-hover); }
.uv-pick-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.uv-pick-row.is-on { font-weight: var(--w-semi); }
.uv-pick-tick { flex: none; width: 14px; color: var(--accent); }
.uv-pick-label { flex: 1 1 auto; min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
/* The hint carries the row's own evidence — an item count, a family — so a
   picker over real data says which entry is the busy one. */
.uv-pick-hint { flex: none; color: var(--muted); font-size: var(--t-micro);
  font-variant-numeric: tabular-nums; }
.uv-pick-note { font-size: var(--t-micro); color: var(--muted); padding: 2px 6px 0; }
