/* Card Studio — a bench instrument, not a web page.
   Dark bench, hairline rules, one warm accent taken from the contact chip,
   and every number set in mono so measurements read as measurements. */

:root {
  --bg:        #0d0e11;
  --bench:     #121419;
  --panel:     #171a20;
  --panel-2:   #1d212a;
  --line:      #272c36;
  --line-soft: #1f242d;
  --ink:       #e7e9ee;
  --dim:       #8d95a3;
  --dimmer:    #626a78;
  --accent:    #e0a63c;
  --accent-dk: #b07f24;
  --sel:       #58a6f0;
  --danger:    #e0674c;
  --ok:        #56b884;

  --r-sm: 5px;
  --r-md: 8px;
  --r-lg: 12px;

  --ui: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
  --mono: "SF Mono", ui-monospace, Menlo, monospace;

  --rail-w: 232px;
  --insp-w: 268px;
  --top-h: 52px;
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 13px/1.45 var(--ui);
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
  user-select: none;
}

button, input, select, textarea { font-family: inherit; font-size: inherit; color: inherit; }
::selection { background: rgba(88,166,240,.35); }

/* ── top bar ─────────────────────────────────────────────────────────── */

.topbar {
  height: var(--top-h);
  display: flex; align-items: center; gap: 18px;
  padding: 0 14px;
  background: linear-gradient(180deg, #191d24, #14171c);
  border-bottom: 1px solid var(--line);
  -webkit-app-region: drag;
}
.topbar button, .topbar input, .topbar select { -webkit-app-region: no-drag; }

.brand { display: flex; align-items: center; gap: 10px; min-width: 190px; }
.mark { width: 34px; height: 24px; overflow: visible; }
.mk-card { fill: #20252e; stroke: var(--accent); stroke-width: 1.6; }
.mk-chip { fill: var(--accent); }
.mk-wave { fill: none; stroke: var(--accent); stroke-width: 1.5; stroke-linecap: round; opacity: .75; }
.brand-text { display: flex; flex-direction: column; line-height: 1.15; }
.brand-name { font-weight: 620; letter-spacing: .2px; }
.brand-sub { font: 10px/1.3 var(--mono); color: var(--dimmer); letter-spacing: .1px; }

.tabs { display: flex; gap: 2px; background: #0f1116; padding: 3px; border-radius: var(--r-md); border: 1px solid var(--line-soft); }
.tab {
  border: 0; background: transparent; color: var(--dim);
  padding: 6px 14px; border-radius: 6px; cursor: pointer;
  font-weight: 520; letter-spacing: .1px;
}
.tab:hover { color: var(--ink); }
.tab.is-active { background: var(--panel-2); color: var(--ink); box-shadow: inset 0 0 0 1px var(--line); }

.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.status { font: 11px var(--mono); color: var(--dimmer); max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── buttons ─────────────────────────────────────────────────────────── */

.btn {
  border: 1px solid var(--line);
  background: var(--panel-2);
  color: var(--ink);
  padding: 6px 13px; border-radius: var(--r-sm);
  cursor: pointer; font-weight: 520;
  transition: background .12s, border-color .12s;
}
.btn:hover { background: #242a34; border-color: #333a46; }
.btn:active { transform: translateY(.5px); }
.btn-primary { background: var(--accent); border-color: var(--accent-dk); color: #201603; font-weight: 640; }
.btn-primary:hover { background: #eeb54b; border-color: var(--accent); }
.btn-ghost { background: transparent; color: var(--dim); }
.btn-ghost:hover { background: var(--panel-2); color: var(--ink); }
.btn.big { padding: 10px 14px; }
.btn:disabled { opacity: .4; cursor: default; }
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; }
.btn-col { display: flex; flex-direction: column; gap: 8px; }

.mini {
  border: 1px solid var(--line); background: var(--panel-2); color: var(--dim);
  width: 24px; height: 22px; border-radius: 4px; cursor: pointer;
  font-size: 11px; line-height: 1; display: inline-flex; align-items: center; justify-content: center;
}
.mini:hover { color: var(--ink); background: #262c36; }
.mini.danger:hover { color: #fff; background: var(--danger); border-color: var(--danger); }

/* ── layout ──────────────────────────────────────────────────────────── */

.app { height: calc(100% - var(--top-h)); position: relative; }
.view { position: absolute; inset: 0; display: none; }
.view.is-active { display: flex; }

.rail {
  width: var(--rail-w); flex: 0 0 var(--rail-w);
  background: var(--panel); border-right: 1px solid var(--line);
  display: flex; flex-direction: column; overflow: hidden;
}
.rail-right { width: var(--insp-w); flex-basis: var(--insp-w); border-right: 0; border-left: 1px solid var(--line); overflow-y: auto; }

.rail-block { padding: 11px 12px; border-bottom: 1px solid var(--line-soft); }
.rail-grow { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
.rail-head, .panel-head {
  font: 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase;
  color: var(--dimmer); margin-bottom: 9px;
  display: flex; align-items: center; justify-content: space-between;
}
.rail-head-actions { display: flex; gap: 3px; }
.dim { color: var(--dimmer); text-transform: none; letter-spacing: 0; font-weight: 400; }

/* tool grid */
.tool-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px; }
.tool {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 7px 2px 5px; border: 1px solid var(--line-soft); border-radius: 6px;
  background: var(--panel-2); color: var(--dim); cursor: pointer;
}
.tool:hover { color: var(--ink); border-color: var(--line); background: #232833; }
.tool svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.tool span { font-size: 9px; letter-spacing: .02em; }

/* layers */
.layers { list-style: none; margin: 0; padding: 0; overflow-y: auto; flex: 1 1 auto; min-height: 0; }
.layers li {
  display: flex; align-items: center; gap: 7px;
  padding: 5px 7px; border-radius: 5px; cursor: pointer;
  color: var(--dim); border: 1px solid transparent;
}
.layers li:hover { background: var(--panel-2); color: var(--ink); }
.layers li.is-sel { background: rgba(88,166,240,.12); border-color: rgba(88,166,240,.4); color: var(--ink); }
.layers .l-kind { font: 9px var(--mono); color: var(--dimmer); text-transform: uppercase; width: 30px; flex: 0 0 30px; }
.layers .l-name { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.layers .l-hide { opacity: .5; }

/* fields */
.field-row { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; }
.field-row > label { flex: 0 0 74px; color: var(--dim); font-size: 11.5px; }
.field-row > input, .field-row > select, .field-row > textarea { flex: 1 1 auto; min-width: 0; }

input[type=text], input[type=number], select, textarea {
  background: #0f1116; border: 1px solid var(--line);
  border-radius: var(--r-sm); padding: 5px 7px; color: var(--ink);
  outline: none; width: 100%;
}
input[type=number] { font-family: var(--mono); font-size: 12px; }
input:focus, select:focus, textarea:focus { border-color: var(--sel); box-shadow: 0 0 0 2px rgba(88,166,240,.16); }
input[type=color] {
  -webkit-appearance: none; appearance: none; width: 34px; height: 24px; padding: 0;
  border: 1px solid var(--line); border-radius: 4px; background: transparent; cursor: pointer; flex: 0 0 34px;
}
input[type=color]::-webkit-color-swatch-wrapper { padding: 2px; }
input[type=color]::-webkit-color-swatch { border: 0; border-radius: 2px; }
input[type=range] { accent-color: var(--accent); }
select { cursor: pointer; }
textarea { resize: vertical; font-family: var(--mono); font-size: 11.5px; line-height: 1.5; }

.seg { display: flex; background: #0f1116; border: 1px solid var(--line); border-radius: var(--r-sm); padding: 2px; flex: 1 1 auto; }
.seg-btn { flex: 1; border: 0; background: transparent; color: var(--dim); padding: 4px;
  border-radius: 3px; cursor: pointer;
  /* 44px is the touch floor the mobile gate enforces — this control sat at 34px
     and failed at every phone width. min-height, not height: the segmented row
     still collapses to its content on a mouse-driven layout. */
  min-height: 44px; }
.seg-btn.is-on { background: var(--panel-2); color: var(--ink); box-shadow: inset 0 0 0 1px var(--line); }

.checks { display: flex; gap: 10px; }
.chk { display: inline-flex; align-items: center; gap: 4px; color: var(--dim); cursor: pointer; font-size: 11.5px; }
.chk input { accent-color: var(--accent); }

/* ── stage ───────────────────────────────────────────────────────────── */

.stage { flex: 1 1 auto; display: flex; flex-direction: column; min-width: 0; background: var(--bench); }
.stage-bar {
  height: 38px; flex: 0 0 38px; display: flex; align-items: center; gap: 10px;
  padding: 0 12px; border-bottom: 1px solid var(--line-soft); background: var(--panel);
}
.doc-name {
  background: transparent; border: 1px solid transparent; border-radius: 5px;
  padding: 4px 7px; font-weight: 560; width: 260px;
}
.doc-name:hover { border-color: var(--line-soft); }
.doc-name:focus { border-color: var(--sel); background: #0f1116; }
.spacer { flex: 1 1 auto; }
.zoom { display: flex; align-items: center; gap: 5px; }
.zoom-val { font: 11px var(--mono); color: var(--dim); width: 42px; text-align: center; }
.handoff-help {
  flex: 0 0 auto; margin: 0; padding: 6px 12px; min-height: 30px;
  display: flex; align-items: center; justify-content: center; gap: 10px;
  border-bottom: 1px solid var(--line-soft); background: var(--panel);
  color: var(--dim); font-size: 11px; line-height: 1.45; text-align: center;
}
.handoff-help .btn { flex: 0 0 auto; padding: 4px 10px; }

.canvas-wrap {
  flex: 1 1 auto; min-height: 0; overflow: auto; position: relative;
  display: flex; align-items: center; justify-content: center;
  padding: 22px;
  background:
    radial-gradient(ellipse at 50% 0%, rgba(224,166,60,.05), transparent 60%),
    var(--bench);
}
#canvas { display: block; }
/* Absent on a desktop, where #faceSeg and #templateSel are already on screen in
   the Card block. Not `visibility` — a second control that a mouse user can tab
   to but not see is the reachability bug with the sign flipped. The phone rules
   turn them on. */
.face-chip, .tpl-chip { display: none; }

/* Empty state. The app opens blank on purpose — the first move is almost always
 * "put my image on it" — so the blank card has to say what to do rather than sit
 * there. pointer-events:none is load-bearing: #canvasWrap owns the dragover/drop
 * handlers that importPhotos() depends on, and an overlay that swallowed them
 * would break the exact gesture this panel is advertising. Only the two buttons
 * opt back in. */
.canvas-empty {
  position: absolute; inset: 0; display: grid; place-items: center;
  pointer-events: none; padding: 22px; z-index: 4;
}
.canvas-empty[hidden] { display: none; }
.ce-inner {
  max-width: 380px; text-align: center; color: var(--dim);
  background: rgba(15,17,22,.82); backdrop-filter: blur(3px);
  border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: 22px 24px 20px;
  box-shadow: 0 18px 44px rgba(0,0,0,.42);
}
.ce-art { width: 64px; height: 44px; color: var(--accent); margin-bottom: 12px; }
.ce-inner h2 { margin: 0 0 7px; font-size: 15px; color: var(--ink); font-weight: 640; }
.ce-inner p { margin: 0; font-size: 12px; line-height: 1.6; color: var(--dim); }
.ce-actions { display: flex; gap: 8px; justify-content: center; margin: 15px 0 12px; pointer-events: auto; }
.ce-actions .btn.primary { background: var(--accent); border-color: var(--accent); color: #17130a; font-weight: 620; }
.ce-actions .btn.primary:hover { filter: brightness(1.08); }
.ce-more { font-size: 11px; color: var(--dimmer); }
.ce-more b { color: var(--dim); font-weight: 600; }

/* Fallback for browsers without HTMLSelectElement.showPicker(): if we cannot open
 * the picker for them, at least say unmistakably where it is. */
@keyframes tplPing { 0%,100% { box-shadow: 0 0 0 0 rgba(224,166,60,0); } 35% { box-shadow: 0 0 0 3px rgba(224,166,60,.5); } }
.is-pinged { animation: tplPing .6s ease-in-out 2; border-color: var(--accent) !important; }

/* drop affordance — the card lights up so it is obvious where a photo lands */
.canvas-wrap.is-dropping { outline: 2px dashed var(--accent); outline-offset: -8px; }

/* Open menu — Save had no counterpart, so saved cards were write-only */
/* The wish popover borrows .open-menu's geometry on purpose — same anchor, same
   panel, same shadow — so the topbar keeps one popover idiom instead of two. */
.wish-wrap { position: relative; }
.wish-pop {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 40;
  width: 320px; background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-lg); box-shadow: 0 16px 40px rgba(0,0,0,.5); padding: 12px;
}
.wish-pop[hidden] { display: none; }
/* 44px of box, 12px of glyph — the target is the padding, not the letters
   (the face-chip's rule). Room for it is paid by the label's padding-right,
   not by pushing the label down: the pop must not grow when the X arrives. */
.wish-x {
  position: absolute; top: 2px; right: 2px; z-index: 1;
  min-width: 44px; min-height: 44px;
  border: 0; background: none; color: var(--dim);
  font-size: 14px; line-height: 1; cursor: pointer;
}
.wish-x:hover { color: var(--ink); }
.wish-pop label { display: block; font-size: 12px; color: var(--ink); margin-bottom: 7px; padding-right: 44px; }
.wish-pop textarea {
  width: 100%; resize: vertical; font: inherit; font-size: 12px; line-height: 1.45;
  color: var(--ink); background: var(--bg); border: 1px solid var(--line);
  border-radius: 8px; padding: 8px 9px;
}
.wish-row { display: flex; align-items: center; gap: 10px; margin-top: 9px; }
.wish-said { flex: 1; font-size: 11px; color: var(--dim); }
.wish-stamp { margin-top: 6px; font: 11px ui-monospace, Menlo, monospace; color: var(--dim); }
.wish-said.bad { color: var(--danger); }

.open-wrap { position: relative; }
.open-menu {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 40;
  min-width: 260px; max-height: 320px; overflow: auto;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: 0 16px 40px rgba(0,0,0,.5); padding: 5px;
}
.open-menu[hidden] { display: none; }
.open-item {
  display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
  width: 100%; text-align: left; background: none; border: 0; cursor: pointer;
  padding: 7px 9px; border-radius: 6px; color: var(--ink); font-size: 12px;
}
.open-item:hover { background: var(--panel-2); }
.oi-when { font: 10px var(--mono); color: var(--dimmer); white-space: nowrap; }
.open-none { margin: 0; padding: 10px; font-size: 11.5px; color: var(--dimmer); }
.stage-foot {
  height: 26px; flex: 0 0 26px; display: flex; align-items: center; gap: 14px;
  padding: 0 12px; border-top: 1px solid var(--line-soft); background: var(--panel);
  font-size: 11px; color: var(--dimmer);
}
.readout { font-family: var(--mono); color: var(--dim); min-width: 130px; }
.hint { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── inspector ───────────────────────────────────────────────────────── */

.inspector { padding: 11px 12px 30px; }
.inspector .empty { color: var(--dimmer); font-size: 12px; line-height: 1.6; }
.insp-group { border-top: 1px solid var(--line-soft); margin-top: 12px; padding-top: 11px; }
.insp-group:first-child { border-top: 0; margin-top: 0; padding-top: 0; }
.insp-title { font: 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; color: var(--dimmer); margin-bottom: 8px; }
/* A one-line consequence under a control whose failure is silent — the QR plate
   is the first, because a code with the plate removed over busy artwork still
   renders correctly on screen and simply stops scanning once printed. */
.insp-note { font: 11px/1.45 var(--sans); color: var(--dimmer); margin: 8px 0 0; }
.xy-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 8px; }
.xy-grid label { display: flex; align-items: center; gap: 5px; color: var(--dim); font: 11px var(--mono); }
.xy-grid input { width: 100%; }
.unit { color: var(--dimmer); font: 10px var(--mono); }

/* ── panels / generic views ──────────────────────────────────────────── */

.panel { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 13px 14px; }
.note { color: var(--dimmer); font-size: 11.5px; line-height: 1.55; margin: 8px 0 0; }
.note code { font-family: var(--mono); color: var(--accent); background: rgba(224,166,60,.1); padding: 1px 4px; border-radius: 3px; }
/* Links in notes take the accent: the browser default blue is unreadable on
   this panel, and there is no global anchor rule to inherit from. */
.note a { color: var(--accent); }
.log {
  margin: 10px 0 0; padding: 8px 9px; background: #0b0d11; border: 1px solid var(--line-soft);
  border-radius: 6px; font: 10.5px/1.5 var(--mono); color: var(--dim);
  max-height: 150px; overflow: auto; white-space: pre-wrap; word-break: break-all; user-select: text;
}

/* tray view */
.tray-layout { display: flex; gap: 16px; padding: 16px; width: 100%; overflow: auto; }
.tray-stage { flex: 1 1 auto; background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 13px 14px; display: flex; flex-direction: column; min-width: 0; }
.tray-canvas-wrap { flex: 1 1 auto; display: flex; align-items: center; justify-content: center; padding: 10px; min-height: 0; }
#trayCanvas { display: block; }
.tray-side { width: 320px; flex: 0 0 320px; display: flex; flex-direction: column; gap: 14px; }

/* batch view */
.batch-layout { display: flex; gap: 16px; padding: 16px; width: 100%; overflow: hidden; }
.batch-source { width: 380px; flex: 0 0 380px; display: flex; flex-direction: column; gap: 9px; }
.batch-source textarea { flex: 1 1 auto; min-height: 180px; }
.batch-preview { flex: 1 1 auto; display: flex; flex-direction: column; min-width: 0; }
.batch-grid {
  flex: 1 1 auto; overflow: auto; margin: 4px -4px 0; padding: 4px;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(184px, 1fr)); gap: 10px; align-content: start;
}
.batch-card { border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: #0b0d11; }
.batch-card.is-now { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(224,166,60,.18); }
.batch-card canvas { display: block; width: 100%; height: auto; }
.batch-card .bc-cap { font: 10px var(--mono); color: var(--dimmer); padding: 4px 6px; border-top: 1px solid var(--line-soft); display: flex; justify-content: space-between; }
.batch-run { border-top: 1px solid var(--line-soft); margin-top: 10px; padding-top: 10px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.run-state { font: 11.5px var(--mono); color: var(--dim); flex: 1 1 auto; }
.chips { display: flex; flex-wrap: wrap; gap: 5px; }
.chip {
  font: 10.5px var(--mono); padding: 3px 7px; border-radius: 20px; cursor: pointer;
  background: rgba(224,166,60,.1); border: 1px solid rgba(224,166,60,.28); color: var(--accent);
}
.chip:hover { background: rgba(224,166,60,.2); }

/* calibrate view */
.cal-layout { display: grid; grid-template-columns: 320px 1fr 320px; gap: 16px; padding: 16px; width: 100%; overflow: auto; align-items: start; }
.nudge { display: flex; gap: 16px; align-items: center; }
.nudge-pad { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.nudge-mid { display: flex; gap: 4px; }
.nudge-pad .mini { width: 30px; height: 26px; }
.nudge-vals { display: flex; flex-direction: column; gap: 7px; flex: 1 1 auto; }
.nudge-vals label { display: flex; align-items: center; gap: 6px; font: 11px var(--mono); color: var(--dim); }
.nudge-vals input { width: 76px; }
.steps { margin: 0; padding-left: 18px; color: var(--dim); font-size: 12px; line-height: 1.65; }
.steps strong { color: var(--ink); }
.steps li { margin-bottom: 5px; }
.geom { width: 100%; border-collapse: collapse; font: 11px var(--mono); }
.geom td { padding: 4px 6px; border-bottom: 1px solid var(--line-soft); color: var(--dim); }
.geom td:first-child { color: var(--dimmer); width: 46%; }
.geom td:last-child { color: var(--ink); text-align: right; }

/* toast */
.toast {
  position: fixed; left: 50%; bottom: 26px; transform: translate(-50%, 14px);
  background: #23282f; border: 1px solid var(--line); color: var(--ink);
  padding: 9px 16px; border-radius: 8px; font-size: 12px;
  box-shadow: 0 10px 30px rgba(0,0,0,.5);
  opacity: 0; pointer-events: none; transition: opacity .18s, transform .18s; z-index: 50;
  /* width:max-content is load-bearing, not tidiness. left:50% puts the box's
     containing space at HALF the viewport, so a shrink-to-fit toast wraps at
     50vw and max-width never binds: at 320px a 90-character message rendered
     160x107 — six lines in half the screen — while the 70vw it was allowed went
     unused. max-content asks for the text's own width first and lets max-width
     do the clamping it was written to do. Same defect, same fix as persona500's
     generator toasts (2026-08-25); swept here the day after. */
  width: max-content;
  max-width: 70vw;
}
.toast.is-on { opacity: 1; transform: translate(-50%, 0); }
.toast.is-err { border-color: var(--danger); background: #3a2320; }
.toast.is-ok { border-color: rgba(86,184,132,.5); }

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #2a3038; border-radius: 6px; border: 2px solid var(--bg); }
::-webkit-scrollbar-thumb:hover { background: #38404a; }

/* calibration preview — the target as it lands on one card */
.cal-preview { margin-top: 14px; border-top: 1px solid var(--line-soft); padding-top: 12px; }
.cal-preview canvas {
  display: block; border-radius: 4px; background: #fff;
  box-shadow: 0 4px 16px rgba(0,0,0,.45);
  max-width: 100%; height: auto;
}

/* compact procedural list used for the tray-loading order */
.steps.tight { font-size: 11.5px; line-height: 1.55; padding-left: 16px; }
.steps.tight li { margin-bottom: 4px; }

/* the guides row carries three toggles — too many for the 74 px label column */
.field-row.stack { flex-direction: column; align-items: flex-start; gap: 5px; }
.field-row.stack > label { flex: none; }
.field-row.stack .checks { flex-wrap: wrap; gap: 12px; }

/* printer status panel */
.pstat { font-size: 11.5px; }
.pstat-line { display: flex; align-items: center; gap: 7px; margin-bottom: 8px; }
.pstat-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 8px; }
.pstat-dot.is-good { background: var(--ok); box-shadow: 0 0 0 3px rgba(86,184,132,.15); }
.pstat-dot.is-bad { background: var(--danger); box-shadow: 0 0 0 3px rgba(224,103,76,.15); }
.pstat-dot.is-warn { background: var(--accent); box-shadow: 0 0 0 3px rgba(224,166,60,.15); }
.pstat-dot.is-unknown { background: var(--dimmer); }
.pstat-fix {
  border: 1px solid rgba(224,103,76,.4); background: rgba(224,103,76,.08);
  border-radius: 6px; padding: 8px 10px; margin-bottom: 9px; line-height: 1.55; color: var(--ink);
}
.pstat-fix-t { font-weight: 620; margin-bottom: 3px; }
.pstat-fix a { color: var(--accent); text-decoration: none; display: inline-block; margin-top: 5px; }
.pstat-fix a:hover { text-decoration: underline; }
.pstat-grid {
  display: grid; grid-template-columns: 84px 1fr; gap: 3px 8px;
  font: 10.5px var(--mono); color: var(--dim);
}
.pstat-grid span:nth-child(odd) { color: var(--dimmer); }
.ink { display: flex; gap: 7px; margin-top: 11px; }
.ink-cell { text-align: center; }
.ink-bar {
  width: 15px; height: 34px; border: 1px solid var(--line); border-radius: 3px;
  background: #0f1116; position: relative; overflow: hidden;
}
.ink-bar span { position: absolute; bottom: 0; left: 0; right: 0; background: var(--accent); opacity: .8; }
.ink-lab { font: 8px var(--mono); color: var(--dimmer); margin-top: 3px; }

/* supplies — a shop, not a manual
 *
 * The old block styled six 400px cards of dense reference. That is the right
 * design for a spec sheet and the wrong one for a page whose job is "which of
 * these do I buy". Tiles now lead with an illustration and a price; the whole
 * reference set is still here, inside <details>, styled to stay readable.
 *
 * The picture is not decoration. Half of what the old prose explained was WHICH
 * OBJECT this is — a 125 kHz coil versus a 13.56 MHz one, a writer versus a
 * wedge — and a drawing settles that before a sentence is read. */
.sup-layout { padding: 16px; width: 100%; overflow: auto; display: flex; flex-direction: column; gap: 14px; }

.s-grid {
  display: grid; gap: 14px;
  /* 260px, not 400px: a tile is a product, and three across beats one-and-a-half. */
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}
.s-tile {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg);
  overflow: hidden; display: flex; flex-direction: column;
  transition: border-color .18s, transform .18s;
}
.s-tile:hover { border-color: #333a46; transform: translateY(-1px); }
.s-art {
  background: linear-gradient(160deg, #14161c, #0c0e12);
  border-bottom: 1px solid var(--line);
  padding: 14px 12px 10px; display: grid; place-items: center;
  color: var(--dim);                       /* the SVGs draw with currentColor */
}
.s-art img { width: 100%; max-width: 190px; height: auto; display: block }
.s-body { padding: 12px 14px 13px; display: flex; flex-direction: column; flex: 1 }
.s-need {
  font: 9.5px var(--mono); letter-spacing: .07em; text-transform: uppercase;
  color: var(--dimmer); margin-bottom: 5px;
}
.s-tile h3 { margin: 0 0 5px; font-size: 13.5px; line-height: 1.3 }
.s-blurb { margin: 0 0 11px; color: var(--dim); font-size: 12px; line-height: 1.5; flex: 1 }
.s-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px }
.s-price { font: 12.5px var(--mono); color: var(--ink) }
.s-buy {
  text-decoration: none; font-size: 12px; font-weight: 620; color: #0f1116;
  background: var(--accent); border: 1px solid var(--accent);
  padding: 6px 15px; border-radius: var(--r-sm);
}
.s-buy:hover { filter: brightness(1.12) }

/* The stock in the drawer is settled fact, not one more option to weigh. */
.s-tile.is-owned { border-color: rgba(86,184,132,.42) }
.s-tag {
  margin-left: 7px; font: 9.5px var(--mono); letter-spacing: .06em; padding: 2px 6px;
  border-radius: 999px; background: rgba(86,184,132,.14);
  border: 1px solid rgba(86,184,132,.4); color: #7fd3a6;
}
.s-you { margin: 0 0 10px; font-size: 11.5px; line-height: 1.6; color: var(--dim) }
.s-you a { color: #7fd3a6 }

/* Everything below is the old guide, demoted. Open it and it is all still there. */
.s-more, .s-help {
  border-top: 1px solid var(--line-soft); margin-top: 11px; padding-top: 9px;
}
.s-help {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: 12px 16px; margin-top: 0;
}
.s-more > summary, .s-help > summary {
  cursor: pointer; font-size: 11.5px; color: var(--dimmer); list-style: none;
  display: flex; align-items: center; gap: 6px;
}
.s-help > summary { font-size: 12.5px; color: var(--dim) }
.s-more > summary::-webkit-details-marker, .s-help > summary::-webkit-details-marker { display: none }
.s-more > summary::before, .s-help > summary::before {
  content: "▸"; font-size: 9px; transition: transform .15s;
}
.s-more[open] > summary::before, .s-help[open] > summary::before { transform: rotate(90deg) }
.s-more > summary:hover, .s-help > summary:hover { color: var(--ink) }
.s-more > *:not(summary) { margin-top: 10px }
.s-help > *:not(summary) { margin-top: 10px }
.s-help ul { margin: 0; padding-left: 18px; color: var(--dim); font-size: 12px; line-height: 1.7 }

.s-search { display: flex; gap: 8px }
.s-search code {
  flex: 1 1 auto; min-width: 0; background: #0b0d11; border: 1px solid var(--line);
  border-radius: 6px; padding: 7px 9px; font: 11px/1.4 var(--mono);
  color: var(--accent); user-select: all; overflow-wrap: anywhere;
}
.s-kw { display: flex; flex-wrap: wrap; gap: 5px }
.s-kw .k { font: 10px var(--mono); padding: 3px 7px; border-radius: 4px }
.s-kw .k.yes { background: rgba(86,184,132,.12); border: 1px solid rgba(86,184,132,.35); color: #7fd3a6 }
.s-kw .k.no  { background: rgba(224,103,76,.1); border: 1px solid rgba(224,103,76,.3); color: #e08b74; text-decoration: line-through }
.s-specs { width: 100%; border-collapse: collapse; font-size: 11.5px }
.s-specs th, .s-specs td { padding: 5px 6px; border-bottom: 1px solid var(--line-soft); vertical-align: top; text-align: left }
.s-specs th { color: var(--dimmer); width: 74px; font: 10.5px var(--mono); font-weight: 400 }
.s-specs td { color: var(--ink) }
.s-specs td.s-why { color: var(--dim) }
.s-links { display: flex; flex-wrap: wrap; gap: 7px }
.s-links a {
  font-size: 11.5px; text-decoration: none; color: var(--ink);
  border: 1px solid var(--line); background: var(--panel-2);
  padding: 5px 10px; border-radius: var(--r-sm);
}
.s-links a:hover { background: #242a34 }
.s-note { color: var(--dimmer); font-size: 11.5px; line-height: 1.55; margin: 0 }
.s-faq { margin-top: 10px }
.s-q { font-size: 12px; color: var(--ink); font-weight: 600 }
.s-a { font-size: 11.5px; color: var(--dim); line-height: 1.6; margin-top: 3px }

/* supplies FAQ */
.sup-faq { border-top: 1px solid var(--line-soft); padding: 11px 0 2px; }
.sup-faq:first-of-type { border-top: 0; padding-top: 0; }
.sup-q { font-weight: 620; font-size: 12.5px; margin-bottom: 5px; }
.sup-a { color: var(--dim); font-size: 12px; line-height: 1.65; }
.sup-why {
  margin-top: 7px; padding: 8px 10px; border-radius: 6px;
  background: rgba(224,166,60,.07); border: 1px solid rgba(224,166,60,.25);
  color: var(--dim); font-size: 11.5px; line-height: 1.6;
}
.sup-why strong { color: var(--accent); }

/* your-reader panel — the fact that decides every card purchase */
.sup-reader {
  background: var(--panel); border: 1px solid rgba(88,166,240,.4);
  border-radius: var(--r-lg); padding: 14px 16px;
  box-shadow: 0 0 0 3px rgba(88,166,240,.06);
}
.sup-reader-grid {
  display: grid; grid-template-columns: 86px 1fr; gap: 6px 12px;
  font-size: 12px; line-height: 1.6; margin-bottom: 11px;
}
.sup-reader-grid > span:nth-child(odd) { color: var(--dimmer); font: 10.5px var(--mono); padding-top: 2px; }
.sup-reader-grid > *:nth-child(even) { color: var(--dim); }
.sup-reader-grid strong { color: var(--ink); }
.sup-test {
  padding: 9px 11px; border-radius: 6px; font-size: 12px; line-height: 1.6;
  background: rgba(88,166,240,.09); border: 1px solid rgba(88,166,240,.3); color: var(--dim);
}
.sup-test strong { color: var(--sel); }

/* web build only — the print-at-100% warning next to the download button */
.web-note {
  margin: 12px 0 0; padding: 10px 12px; border-radius: 8px; font-size: 11.5px; line-height: 1.6;
  background: rgba(224,166,60,.08); border: 1px solid rgba(224,166,60,.32); color: var(--dim);
}
.web-note strong { color: var(--accent); }
.web-note em { font-style: normal; text-decoration: underline; }

/* ───────── CHIP ───────── */
.chip-layout {
  padding: 16px; width: 100%; overflow: auto;
  display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr);
  gap: 14px; align-content: start;
}
@media (max-width: 900px) { .chip-layout { grid-template-columns: 1fr; } }

.chip-card { margin-top: 12px; }
.chip-id { font: 600 15px/1.2 var(--mono); color: var(--ink); letter-spacing: .02em; }
.chip-title { font-size: 13px; color: var(--ink); margin-top: 2px; }
.chip-meta { font-size: 11.5px; color: var(--dimmer); margin: 3px 0 10px; }

.chip-row {
  display: flex; gap: 10px; align-items: baseline;
  padding: 5px 0; border-top: 1px solid var(--line-soft);
}
.chip-row > span { flex: 0 0 74px; font-size: 11.5px; color: var(--dim); }
/* Card contents can be long and are not ours to reflow — wrap on any character
   rather than letting a 300-byte URL push the panel wider than the window. */
.chip-row > code {
  font: 11.5px/1.5 var(--mono); color: var(--ink);
  min-width: 0; overflow-wrap: anywhere; word-break: break-all;
}
.chip-row.is-bad > code { color: var(--danger); }

.chip-hint { margin: 8px 0 0; font-size: 11.5px; line-height: 1.6; color: var(--dimmer); }
.chip-result:empty { display: none; }
.chip-result { margin-top: 10px; }
.chip-note { margin: 4px 0 0; font-size: 11.5px; line-height: 1.55; color: var(--accent); }
.chip-err  { margin: 4px 0 0; font-size: 11.5px; line-height: 1.55; color: var(--danger); overflow-wrap: anywhere; }
.chip-ok   { margin: 0; font-size: 12px; color: var(--ok); }


/* ── narrow screens ──────────────────────────────────────────────────────
   The app was laid out for a desktop window: a 232px tool rail, a stage, and a
   268px inspector, all side by side. At 390px wide that is three columns in the
   space of one and everything crushes.

   On a phone the three become a vertical stack — tools on top, canvas in the
   middle at a workable height, inspector below — and every fixed-width control
   is released. The stage keeps a minimum height so the card is never squeezed to
   a sliver by whatever is above it.

   Breakpoint at 820px rather than a phone width: the layout needs ~820px before
   the three columns stop fighting, so a small laptop window gets the stacked
   version too, which is the right call for it. */
@media (max-width: 820px) {
  :root { --top-h: auto; }

  .topbar {
    height: auto; flex-wrap: wrap; gap: 8px 12px; padding: 8px 10px;
    position: sticky; top: 0; z-index: 20;
  }
  .topbar-right { margin-left: 0; width: 100%; justify-content: flex-start; flex-wrap: wrap; }

  /* THE TOPBAR POPOVERS BECOME SHEETS, because on a phone they cannot be hung
     off their own button and stay on the screen.
       Both are `position:absolute; right:0; width:320px` inside a wrapper that
     is the width of the button it belongs to. On the desktop bar that wrapper
     sits hard against the right edge, so a panel growing leftward from it lands
     inside the window. Here the action row is not against that edge: measured at
     390px, .wish-wrap spans x=26..134, so `right:0` puts the panel's LEFT edge at
     -186 and 186px of a 370px box is off the side of the screen. That is the
     report, in numbers: the wish box opens off the viewable window.
       Anchoring it to the BAR instead does not work either, and the reason is
     worth writing down because it looks like it should. The design view is a
     non-scrolling flex shell and `.topbar` is deliberately `position:static`
     inside it (see part 2 below), so a static wrapper hands the containing block
     to the page, `top:calc(100% + 6px)` resolves against the full viewport
     height, and the panel lands at y=850 on an 844px screen — off the BOTTOM
     instead of off the side. Both anchors are wrong for the same reason: on a
     phone there is no fixed relationship between that button and the free space
     around it.
       So pin them to the viewport, in the idiom this phone layout already uses
     everywhere else — a sheet at the bottom edge, lifted clear of the keyboard by
     the same --kb the rails use, because the wish box is a textarea and the
     keyboard is up the moment it matters. One rule for both: .open-menu is the
     same geometry and had the same defect, unreported only because Open is
     reached far less often than Wish it better. */
  .wish-pop, .open-menu {
    position: fixed; top: auto; left: 0; right: 0; bottom: var(--kb, 0px);
    width: auto; min-width: 0;
    /* In the design view the dock owns the bottom 56px, so a sheet that stops at
       the bottom edge puts its own Send button underneath it — which is the
       original complaint with two extra steps. Sit ON the dock, the same offset
       .rail uses for exactly this reason. Scoped to that view because the dock is
       display:none in the other five, where the gap would be 56px of nothing. */
    /* The third term is what the keyboard leaves: --vvh is the visual viewport,
       and without it a 420px sheet lifted onto a 508px visible strip is clipped
       off the TOP, which is the same bug pointing the other way. */
    max-height: min(60dvh, 420px, calc(var(--vvh, 100dvh) - 90px));
    overflow-y: auto; -webkit-overflow-scrolling: touch;
    border-radius: 14px 14px 0 0; border-bottom: 0;
    padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
    box-shadow: 0 -18px 40px rgba(0,0,0,.45);
    z-index: 60;                       /* over the rails (30) and the dock (40) */
  }
  body[data-view="design"] .wish-pop,
  body[data-view="design"] .open-menu {
    bottom: calc(var(--dock-h, 56px) + env(safe-area-inset-bottom, 0px) + var(--kb, 0px));
  }

  /* Tabs scroll sideways instead of wrapping into two rows that shift the
     canvas down every time the label lengths change. */
  .tabs {
    order: 3; width: 100%; overflow-x: auto; overflow-y: hidden;
    -webkit-overflow-scrolling: touch; scrollbar-width: none;
  }
  .tabs::-webkit-scrollbar { display: none; }
  .tab { flex: 0 0 auto; padding: 7px 12px; }

  .app { height: auto; min-height: calc(100dvh - 1px); }
  .view { position: static; inset: auto; }
  .view.is-active { flex-direction: column; height: auto; }

  /* Rails give up their fixed widths and cap their height so neither can push
     the canvas off the screen. */
  .rail, .rail-right {
    width: 100%; flex: 0 0 auto; max-height: 46vh; overflow-y: auto;
    border-right: 0; border-left: 0; border-bottom: 1px solid var(--line);
  }
  .rail-right { border-top: 1px solid var(--line); border-bottom: 0; }
  .rail-grow { flex: 0 0 auto; }

  .stage { min-height: 58vh; }
  .stage-bar { flex-wrap: wrap; height: auto; padding: 7px 10px; gap: 8px; }
  .handoff-help { padding: 7px 10px; }
  .handoff-help .btn { min-height: 44px; }
  .doc-name { width: 100%; order: -1; }
  .stage-foot { flex-wrap: wrap; height: auto; padding: 7px 10px; gap: 6px 10px; }
  .hint { display: none; }          /* keyboard-only guidance on a touch device */

  /* Multi-column panels collapse. minmax(400px,…) does NOT collapse on its own —
     below 400px it overflows the viewport instead of shrinking. */
  .cal-layout { grid-template-columns: 1fr; padding: 12px; gap: 12px; }
  .s-grid { grid-template-columns: 1fr; }
  .des-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
  .chip-layout { padding: 12px; }
  .xy-grid { grid-template-columns: 1fr 1fr; }

  /* The tray was missed when the other views got their collapses. Its desktop
     flex row kept the hard 320px side column, crushing the stage to ~30px at
     390px wide — and because the canvas wrap centers its overflow, the canvas's
     left half sat PAST the scroll origin, where no scroll can reach (scrollLeft
     floors at 0). Preview stacks above controls instead. flex-basis must become
     auto: in a column, the 320px basis would set the side panel's HEIGHT. */
  .tray-layout { flex-direction: column; }
  .tray-side { width: auto; flex: 0 0 auto; }

  /* 16px is the threshold below which iOS Safari zooms the page on focus, which
     then leaves the layout scrolled sideways with no way back. */
  input, select, textarea { font-size: 16px; }

  /* Touch targets. 44px, not 34 — this comment used to claim 34px was enough and
     the project's own mobile gate (tools/verify_mobile.mjs, MIN_TAP = 44) failed
     button.seg-btn at every phone width for exactly that reason. The gate is the
     authority; a comment is not. .seg grows with its buttons rather than clipping. */
  .btn, .mini, .tool-grid button, .seg-btn { min-height: 44px; }
  .seg { height: auto; align-items: stretch; }
  .tool-grid { grid-template-columns: repeat(4, 1fr); gap: 7px; }
}

/* Very narrow: one column for anything still paired. */
@media (max-width: 430px) {
  .xy-grid { grid-template-columns: 1fr; }
  .topbar { gap: 6px 8px; }
  .brand span { display: none; }     /* keep the mark, drop the wordmark */
}

.clipwarn{
  font-size:11.5px; color:var(--danger);
  border:1px solid rgba(224,103,76,.4); background:rgba(224,103,76,.08);
  border-radius:99px; padding:2px 9px; white-space:nowrap;
}
.clipwarn[hidden]{ display:none }

.cal-preset{ padding:11px 12px; border-top:1px solid var(--line-soft) }
.cal-preset[hidden]{ display:none }
.cal-preset label{ display:block; font-size:11px; letter-spacing:.06em; text-transform:uppercase;
  color:var(--dim); margin:0 0 5px }
.cal-preset select{ width:100%; background:#0f1116; border:1px solid var(--line);
  border-radius:6px; padding:6px 8px }
.cal-preset .note{ margin:7px 0 0; font-size:11.5px; line-height:1.5; color:var(--dim) }

/* margin control — compact enough for the rail, thumb-sized on a phone */
.margin-head{ margin-bottom:6px }
.margin-ctl{ display:flex; align-items:center; gap:5px; flex-wrap:wrap; transition:opacity .2s }
.margin-ctl.is-off{ opacity:.42 }
.margin-ctl input[type=number]{
  width:56px; background:#0f1116; border:1px solid var(--line);
  border-radius:5px; padding:4px 6px; font-family:var(--mono); font-size:12px;
}
.margin-ctl .mx{ color:var(--dimmer); font-size:11px }
.margin-ctl .mu{ color:var(--dim); font-size:11px; margin-right:2px }
.seg-sm{ margin-left:auto }
.seg-sm .seg-btn{ padding:4px 8px; font-size:11px }
@media (max-width:820px){
  /* 44, not 34. Thirty lines up this file already says "44px, not 34 -- this
     comment used to claim 34px was enough and the project's own mobile gate
     failed button.seg-btn at every phone width for exactly that reason", and
     then these two rules set 34 again in a later block that wins the cascade.
     The gate agreed with the comment and not with the code: served properly it
     reports `button.seg-btn is 34px ("Round")` at all four widths.
     Found while moving these controls into a phone sheet, where a closed sheet
     is visibility:hidden and the gate skips it -- so leaving them at 34 would
     have looked like a fix and been a place to hide. */
  .margin-ctl input[type=number]{ width:72px; min-height:44px }
  .seg-sm{ margin-left:0; width:100% }
  .seg-sm .seg-btn{ flex:1; min-height:44px }
  /* The document name is the card's title and, on a phone, the only thing in the
     stage bar. 28px was a desktop field in a desktop row. */
  .doc-name{ min-height:44px }
}

.bleed-ctl{ display:flex; align-items:center; gap:9px; flex:1 }
.bleed-ctl input[type=range]{ flex:1; accent-color:var(--accent); min-width:0 }
.bleed-ctl output{ font-family:var(--mono); font-size:11.5px; color:var(--ink);
  min-width:52px; text-align:right }
@media (max-width:820px){ .bleed-ctl input[type=range]{ height:34px } }


/* ── TOUCH ─────────────────────────────────────────────────────────────────
 * The web build of the studio is opened by tapping a card, so on a touch device
 * every control has to clear 44px. Measured by tools/verify_mobile.mjs against a
 * real emulated phone: tabs came in at 30px, buttons at 34, the mini row at 24,
 * segment buttons at 24, and the visibility checkboxes at 19.
 *
 * `pointer: coarse` and not a width query, on purpose. This is a dense editor and
 * a mouse at 400px is still a mouse — sizing by width would inflate the desktop
 * UI to fix a phone, which is the wrong trade in the wrong direction.
 *
 * Where a control is genuinely small by design (the mini row, the checkboxes),
 * the BOX grows rather than the glyph: padding and min-height carry the target,
 * so the layout keeps its proportions and the thumb still lands.
 */
@media (pointer:coarse){
  /* WITHOUT THIS LINE THE CARD CANNOT BE EDITED BY FINGER, and the reason is not
   * about size at all. A browser claims a one-finger drag for itself — panning,
   * scrolling — and once it has, it stops sending the app anything: the six drag
   * variants measured on the deployed build each moved the element 0.00 mm while
   * the canvas took 66 touchmoves and 0 mousemoves. `pinch-zoom` gives the app
   * one-finger drags and keeps two-finger zoom for the browser, which is the pair
   * that matters: the gesture that moves artwork is ours, the gesture that lets
   * someone with poor eyesight read the card stays theirs. `none` would have taken
   * both, and a page that cannot be zoomed is the complaint one layer up.
   *   THE COST, stated because it is real: a one-finger drag over the canvas no
   * longer scrolls it, so a card zoomed past the window is panned with the zoom
   * buttons and Fit rather than by dragging. On a phone the card is fitted to the
   * window at boot and on every rotate, so that state is one the person chose. */
  #canvas   { touch-action: pinch-zoom }
  .tab      { min-height:44px; padding-top:0; padding-bottom:0 }
  .btn      { min-height:44px }
  .mini     { min-width:44px; min-height:44px }
  .seg-btn  { min-height:44px; padding-left:14px; padding-right:14px }
  .chk      { min-height:44px; display:inline-flex; align-items:center }
  .chk input[type=checkbox]{ width:22px; height:22px }
  .dots button{ width:44px; height:44px }
  select, input[type=text], input[type=number]{ min-height:44px }
  /* 16px is where iOS Safari stops zooming the whole page when a field takes
   * focus. A 16px floor already exists in the 820px block above and LOSES the
   * cascade three ways: input[type=number] (12px, specificity 0,1,1) outranks
   * its bare element selectors, .margin-ctl input[type=number] (12px, 0,2,1)
   * outranks them AND sits later in source, and .wish-pop textarea (12px,
   * 0,1,1) outranks them too — so the wish box, the margin pair and every
   * inspector number field still zoomed. And that floor is width-scoped: a
   * phone held sideways is 844px wide, matches no 820px query, and zoomed on
   * EVERY field. The pointer is the right question (the block header's
   * argument), and this block sits after all three offenders, so restating
   * their own selectors here wins every tie by source order. NOT fixed with
   * user-scalable=no on the viewport meta: that kills the pinch zoom that lets
   * someone with poor eyesight read the card, and tools/verify_mobile.mjs
   * fails any page that ships it. */
  input, select, textarea,
  input[type=number], .margin-ctl input[type=number], .wish-pop textarea { font-size: 16px }
}

/* ══════════════════════════════════════════════════════════════════════════
   THE PHONE SHELL — the design view stops being a document

   WHAT WAS WRONG, measured before this was written (390x844, Playwright,
   tools/verify_mobile.mjs's own browser):

     .topbar      201px   brand row + wrapped action row + tab strip
     .rail-left   388px   Add, Layers and Card, all resident, all capped
     .stage-bar    95px   doc name on its own row, then zoom
     #canvasWrap  364px   starting at y=684 -- 160px of it on screen
     .stage-foot   31px   at y=1048
     .rail-right  104px   at y=1079

   684px of chrome above the card on an 844px screen: 81% of the phone spent
   before the card starts, and the card then clipped in half by the bottom edge.
   At 320px not one pixel of it was visible.

   And the half nobody had noticed: html,body carry `overflow:hidden` (they are
   an app shell, correctly), while the old phone rules relaxed `.app` to
   `min-height:100dvh`. So everything past the fold was not merely below it, it
   was UNREACHABLE -- #inspector sat at y=1099 and a finger dragged from 700 to
   150, three times, moved it exactly 0px. Properties was not hard to get to on
   a phone; it did not exist there. Every element's position, size, colour and
   text was unreachable, and tools/verify_mobile.mjs passed the page at all four
   widths, because it measures horizontal overflow and tap-target size and every
   unreachable control was a perfectly compliant 44px.

   THE RULE THIS GETS WRONG-WAY-ROUND, and it is the whole lesson: a responsive
   stack turns COLUMN ORDER INTO SCROLL ORDER. Three columns become three
   sections, the tool columns were written first, so the tools push the artwork
   off the screen. Capping them with max-height:46vh does not fix it -- it makes
   every panel permanently present AND permanently too small, which is the worst
   of both. No editor that ships on a phone reflows its desktop columns. They
   layer chrome in Z and leave the canvas alone in Y.

   So: the canvas holds the screen, and the four panels that already exist in
   this view become one bottom sheet, raised one at a time from a dock. The
   panels are the SAME DOM NODES as on desktop -- nothing here duplicates a
   control, because two copies of a control is two places to fix its bugs.
   ══════════════════════════════════════════════════════════════════════════ */

/* Absent above 820px. Not `visibility`, not opacity -- the dock must not be in
   the accessibility tree or the tab order on a desktop that will never show it. */
.dock, .sheet-scrim { display: none; }

@media (max-width: 820px) {

  /* ── 1. The topbar, 201px -> ~92px ────────────────────────────────────────
     Nothing is removed that a phone user can act on. `brand-sub` and `status`
     are the same fact ("Canon MP tray · 2 × CR-80") written twice, and neither
     is a control; the wordmark goes because the mark already says it. What is
     left is one row you can act on and one strip of views. */
  .brand-sub,
  .topbar .status { display: none; }
  .brand-name     { display: none; }
  /* The 190px is desktop's reservation for the wordmark and the tray subtitle.
     With both hidden it is 190px held for nothing, and it was enough to push the
     action row onto a line of its own -- 44px of a phone spent on the gap beside
     a logo. */
  .brand { min-width: 0; flex: 0 0 auto; }
  .topbar   { padding: 6px 10px; gap: 6px 8px; row-gap: 6px; }
  .topbar-right { width: auto; margin-left: auto; flex-wrap: nowrap; gap: 6px; }
  .topbar-right .btn { padding-inline: 11px; }

  /* ── 2. The design view becomes a fixed shell ─────────────────────────────
     Scoped to the design view by a body attribute that switchView() already had
     to set something for. The other five views are documents -- Tray, Batch,
     Calibrate, Chip and Supplies are meant to scroll, and forcing a shell on
     them would break them to fix a view they are not. */
  /* --vvh is the visual viewport, set by app.js from window.visualViewport. It
     exists because the on-screen keyboard is laid OVER the viewport on iOS
     rather than subtracted from it, so 100dvh keeps measuring a screen the user
     can no longer see the bottom of. 100dvh stays as the fallback. */
  body[data-view="design"] { display: flex; flex-direction: column; height: var(--vvh, 100dvh); }
  body[data-view="design"] .topbar { flex: 0 0 auto; position: static; }
  body[data-view="design"] .app { flex: 1 1 auto; min-height: 0; height: auto; }
  body[data-view="design"] .view[data-view="design"].is-active {
    display: flex; flex-direction: column; height: 100%; min-height: 0;
  }

  /* The stage takes everything the topbar did not, and stops short of the dock. */
  body[data-view="design"] .stage {
    flex: 1 1 auto; min-height: 0;
    padding-bottom: calc(var(--dock-h) + env(safe-area-inset-bottom, 0px));
  }
  /* 22px of padding on a 390px screen is 11% of the width the card is competing
     for, and width is what binds: a 1.586:1 card on a 0.46:1 screen is always
     limited by the short side. */
  body[data-view="design"] .canvas-wrap { padding: 10px; }

  /* With a sheet up the stage gives back exactly the sheet's height, so the
     canvas re-centres the card into the band that is still showing instead of
     centring it behind the panel. Same duration and curve as the sheet, so the
     card rises with it rather than jumping when it lands. */
  body[data-view="design"] .stage,
  body[data-view="design"] .canvas-wrap { transition: padding .22s cubic-bezier(.32,.72,0,1); }
  body[data-view="design"][data-sheet] .stage {
    padding-bottom: calc(var(--sheet-h) + var(--dock-h) + env(safe-area-inset-bottom, 0px));
  }

  /* Stage chrome. The document name is the one thing here worth a phone row;
     the cursor readout and the keyboard hint are desktop instruments. */
  .stage-bar { height: auto; flex: 0 0 auto; padding: 5px 10px; gap: 6px; flex-wrap: nowrap; }
  .doc-name  { width: auto; order: 0; flex: 1 1 auto; min-width: 0; }
  .zoom      { flex: 0 0 auto; }
  .stage-foot { display: none; }
  /* ...except the clip warning, which is the one thing down there that means a
     card will print wrong. It moves onto the canvas rather than being deleted. */
  #clipWarn:not([hidden]) {
    display: block; position: absolute; left: 10px; right: 10px; bottom: 10px;
    z-index: 5; text-align: center; pointer-events: none;
  }
  .canvas-wrap { position: relative; }

  /* THE FACE CHIP — which side of the card you are looking at, and a tap to see
     the other one.
       Floated over the canvas rather than added to the stage bar, because the bar is
     the last row of chrome above the artwork and this whole shell exists to stop
     chrome eating the card: an overlay costs zero vertical pixels.
       WHAT IT COVERS, measured rather than claimed. With no sheet up: 0 px² of the
     card at all four widths — at 390x844 the canvas box is ~640px tall and the card
     renders ~230px, so the chip sits in empty bench above its top-right corner. With
     a sheet RAISED it does overlap, because the stage gives back the sheet's height
     and the card re-centres upward into the band that is left: 2187 px² at 390px,
     1982 at 430, 1491 at 360, 467 at 320. The scrim is painted over both, so it is
     dimmed rather than tappable. An earlier version of this comment said "there is
     room for it without covering anything" — true of the one state it had been looked
     at in, false of the other four.
       IT SCROLLS WITH A ZOOMED CANVAS, and that is a cosmetic loss and not a
     reachability one: zoom the card past the window and the chip goes with the
     content, but the face is still reachable, because the dock's Card panel holds
     #faceSeg at any zoom — which is where that control lives on a desktop anyway.
     (A previous version justified the placement by saying #clipWarn makes the same
     trade. It does not: .stage-foot is display:none in this shell, so the rule that
     moves #clipWarn onto the canvas never fires and that warning is not drawn on a
     phone at all. Pre-existing and worth fixing on its own; it cannot be cited as a
     precedent for anything.)
       WIDTH-GATED ON PURPOSE, while the tap-to-flip gesture is gated on the POINTER.
     Different questions: this chip stands in for a control the LAYOUT hid, so it
     belongs wherever #faceSeg is out of reach — including a narrow desktop window,
     where a mouse user needs it just as much. The gesture is a finger gesture, so it
     asks what made the event. Conflating the two put a flip on a mouse click at
     819px; the note in app.js has the measurement.
       44px of BOX and 12px of glyph, the rule the mini row above uses: the target is
     the padding, not the letters. The border is --dim rather than --line because at
     1px it carries the whole "this is a control" signal, and --line measured 1.27:1
     against the bench where a non-text boundary wants 3:1 — the gold word was doing
     all the work. --dim is 5.88:1 and is a token this file already has, rather than a
     new hex nobody else uses. */
  .face-chip, .tpl-chip {
    display: inline-flex; align-items: center; justify-content: center;
    position: absolute; top: 8px; right: 8px; z-index: 6;
    min-height: 44px; padding: 0 15px;
    background: rgba(15,17,22,.86); border: 1px solid var(--dim);
    border-radius: 99px; color: var(--accent);
    font: 620 12px/1 var(--ui); letter-spacing: .06em; text-transform: uppercase;
    cursor: pointer; -webkit-tap-highlight-color: transparent;
  }
  /* TEMPLATES, the same pill on the opposite corner. It stands in for
     #templateSel the way the face chip stands in for #faceSeg: the one-tap path
     (#ceTemplate) lives in the empty state, which hides the moment the face has
     any element, so on a loaded card the picker sat three taps deep behind the
     dock. Top-LEFT because the face chip owns top-right, and two overlays on
     one corner would cover each other exactly when a raised sheet squeezes the
     card's band. Everything else — 44px box / 12px glyph, the overlap trade,
     the scrim dimming it — is the face chip's, measured above. */
  .tpl-chip { right: auto; left: 8px; }
  .face-chip::after {
    content: '⇄'; margin-left: 7px; color: var(--dim);
    font-size: 14px; text-transform: none;
  }

  /* ── 3. The four panels become one sheet ──────────────────────────────────
     They keep their own DOM. All that changes is that they leave the column
     flow, sit above the dock, and are translated off-screen until asked for. */
  /* THE SHEET IS SIZED BY WHAT MUST STAY VISIBLE BEHIND IT, not by what fits in
     it. At 390x844 the chrome above the canvas is 170px and the dock is 56, so a
     360px sheet leaves a 258px band -- and the card renders 255px tall. One more
     row of sheet and the card starts being covered by the panel editing it,
     which is the failure every editor on a phone is built to avoid. The panel
     scrolls; the card cannot. */
  /* --kb is how much of the screen the keyboard has taken: the layout viewport
     minus the visual one. Sizing <body> to --vvh is not enough on its own,
     because the dock and the sheet are position:fixed and fixed is resolved
     against the LAYOUT viewport, which the keyboard does not change. Measured
     with the visual viewport forced to 508 of 844: body shrank correctly and
     the dock stayed at bottom 844, behind the keys. Lifting them by --kb is what
     actually moves them. Zero when --vvh is unset, so nothing shifts on a
     browser without visualViewport. */
  /* THE SHEET YIELDS TO THE CARD WHEN THE KEYBOARD ARRIVES. min(42dvh,360px)
     was sized against the SCREEN, and dvh does not shrink when the keyboard
     takes half of it: at 390x844 with the keys up the visible strip is ~508px,
     and 92px of topbar + 40px of stage bar + a 360px sheet + the 56px dock is
     548 — the card's band is negative and the card is entirely off the frame,
     which is the report ("you push the card out of frame"). The third term is
     the same --vvh idiom the wish-pop uses: whatever the keyboard leaves, minus
     the fixed chrome (92+40+56) and a 104px card floor, so the panel being
     typed into can never fully evict the thing it is editing. The clamp floor
     keeps the sheet itself usable when even that cannot be honoured (an SE
     with the keys up) — at that point the keyboard is the modal moment and the
     sheet, where the typing is, wins. The sheet scrolls; the card cannot. */
  :root { --dock-h: 56px;
          --sheet-h: clamp(150px, min(42dvh, 360px), calc(var(--vvh, 100dvh) - 292px));
          --kb: calc(100dvh - var(--vvh, 100dvh)); }

  .rail, .rail-right {
    position: fixed; left: 0; right: 0;
    bottom: calc(var(--dock-h) + env(safe-area-inset-bottom, 0px) + var(--kb));
    width: auto; max-height: none; height: var(--sheet-h);
    overflow-y: auto; -webkit-overflow-scrolling: touch;
    background: var(--panel);
    border: 1px solid var(--line); border-bottom: 0;
    border-radius: 14px 14px 0 0;
    box-shadow: 0 -18px 40px rgba(0,0,0,.45);
    transform: translateY(calc(100% + var(--dock-h) + 40px));
    transition: transform .22s cubic-bezier(.32,.72,0,1);
    z-index: 30;
  }
  /* A sheet that is down must not be reachable by a swipe or the tab key. Any
     panel still hit-testable off-screen is the unreachable-Properties bug with
     the sign flipped: present to the machine, absent to the person. */
  body:not([data-sheet]) .rail,
  body:not([data-sheet]) .rail-right { visibility: hidden; }

  body[data-sheet="add"]    .rail-left,
  body[data-sheet="layers"] .rail-left,
  body[data-sheet="card"]   .rail-left,
  body[data-sheet="props"]  .rail-right { transform: translateY(0); visibility: visible; }

  /* One block at a time. The left rail holds three and a phone sheet shows the
     one that was asked for -- stacking all three is the resident-column mistake
     again, in a smaller box. */
  .rail-left > .rail-block { display: none; }
  body[data-sheet="add"]    .rail-left > [data-block="add"],
  body[data-sheet="layers"] .rail-left > [data-block="layers"],
  body[data-sheet="card"]   .rail-left > [data-block="card"] { display: block; }
  .rail-grow { flex: 0 0 auto; }

  /* ── 4. The dock ──────────────────────────────────────────────────────────
     Four buttons, each raising one panel. It is the only thing added, and it
     adds no capability -- it is a way to reach capability that was already
     there and had become unreachable. */
  .dock {
    display: flex; position: fixed; left: 0; right: 0; bottom: var(--kb); z-index: 40;
    height: calc(var(--dock-h) + env(safe-area-inset-bottom, 0px));
    padding-bottom: env(safe-area-inset-bottom, 0px);
    background: linear-gradient(180deg, #191d24, #14171c);
    border-top: 1px solid var(--line);
  }
  body:not([data-view="design"]) .dock,
  body:not([data-view="design"]) .sheet-scrim { display: none; }

  .dock-btn {
    flex: 1 1 0; min-width: 0; min-height: 52px;
    border: 0; background: transparent; color: var(--dim);
    font: 600 12px/1 var(--ui); letter-spacing: .04em;
    cursor: pointer; padding: 0 4px;
    border-top: 2px solid transparent;
  }
  .dock-btn.is-on { color: var(--accent); border-top-color: var(--accent); background: rgba(224,166,60,.07); }
  /* Edit is dimmed with nothing selected, because there is nothing to edit --
     and it is NOT disabled, so a tap still opens the panel and says so. A
     control that silently does nothing teaches people it is broken. */
  .dock-btn[data-sheet="props"].is-idle { color: var(--dimmer); }

  /* ABSOLUTE INSIDE THE VIEW, not fixed to the viewport. The first version used
     `position:fixed; inset:0`, which put the scrim over the topbar too -- so with
     a panel up the six view tabs stopped responding and you had to dismiss the
     sheet before you could leave Design. The scrim's job is "tap the card to put
     this away", so it should cover the card and nothing else. Scoping it to the
     view gets that without racing the topbar for a z-index, and the top bar of a
     phone editor is supposed to stay live while a sheet is open. */
  body[data-view="design"] .view[data-view="design"].is-active { position: relative; }
  .sheet-scrim {
    display: block; position: absolute; inset: 0;
    bottom: calc(var(--dock-h) + env(safe-area-inset-bottom, 0px));
    z-index: 25; background: rgba(8,9,12,.34);
  }
  .sheet-scrim[hidden] { display: none; }
}
