/* Workshop — the design system.
 *
 * Built on Linear's approach, per the popular-web-designs reference: a near-black canvas where
 * content emerges from darkness, translucent white surfaces instead of solid fills, whisper-thin
 * semi-transparent borders instead of heavy ones, one chromatic accent (indigo) reserved for
 * actions, and type set in Inter at 400/500/600 rather than bold. Elevation is expressed as
 * background luminance, never as a drop shadow.
 *
 * Everything below is mobile-first: the phone is the tool that goes to the mould, the desktop
 * is the afterthought.
 */

/* ---- fonts (self-hosted, so the app has its real typeface with no signal) ---------------- */

@font-face { font-family: 'Inter'; font-style: normal; font-weight: 400; font-display: swap; src: url('/fonts/Inter-400.woff2') format('woff2'); }
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 500; font-display: swap; src: url('/fonts/Inter-500.woff2') format('woff2'); }
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 600; font-display: swap; src: url('/fonts/Inter-600.woff2') format('woff2'); }
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 700; font-display: swap; src: url('/fonts/Inter-700.woff2') format('woff2'); }
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url('/fonts/JetBrainsMono-400.woff2') format('woff2'); }
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 500; font-display: swap; src: url('/fonts/JetBrainsMono-500.woff2') format('woff2'); }

/* ---- tokens ----------------------------------------------------------------------------- */

:root {
  --canvas: #08090a;
  --panel: #0f1011;
  --surface: #191a1b;
  --raised: #202124;
  --hover: #28282c;

  --text: #f7f8f8;
  --text-2: #d0d6e0;
  --text-3: #8a8f98;
  --text-4: #62666d;

  --accent: #5e6ad2;
  --accent-bright: #7170ff;
  --accent-hover: #828fff;
  --accent-soft: rgba(94, 106, 210, 0.14);

  --green: #27a644;
  --green-soft: rgba(39, 166, 68, 0.14);
  --amber: #d9a227;
  --amber-soft: rgba(217, 162, 39, 0.15);
  --red: #e5484d;
  --red-soft: rgba(229, 72, 77, 0.15);
  --blue: #4a9eda;
  --blue-soft: rgba(74, 158, 218, 0.15);

  --border: rgba(255, 255, 255, 0.08);
  --border-subtle: rgba(255, 255, 255, 0.05);
  --border-solid: #23252a;

  --r-xs: 4px;
  --r-sm: 6px;
  --r: 8px;
  --r-lg: 12px;
  --r-pill: 9999px;

  --shadow-sheet: 0 8px 2px rgba(0, 0, 0, 0), 0 5px 2px rgba(0, 0, 0, 0.01),
                  0 3px 2px rgba(0, 0, 0, 0.04), 0 1px 1px rgba(0, 0, 0, 0.07),
                  0 0 0 1px rgba(255, 255, 255, 0.06);

  --font: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  --bar-h: 56px;
  --tab-h: 58px;
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);

  --w-ui: 500;
  --w-strong: 600;
}

/* ---- base ------------------------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--canvas);
  color: var(--text);
  font-family: var(--font);
  font-size: 16px;
  font-weight: 400;
  line-height: 1.5;
  font-feature-settings: 'cv01', 'ss03';
  -webkit-font-smoothing: antialiased;
  overscroll-behavior-y: none;
}

h1, h2, h3, h4 { margin: 0; font-weight: var(--w-ui); line-height: 1.25; letter-spacing: -0.015em; }
p { margin: 0; }
a { color: var(--accent-bright); text-decoration: none; }
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; }
svg { width: 100%; height: 100%; display: block; }

:focus-visible {
  outline: 2px solid var(--accent-bright);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

.mono, .num { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.num { font-weight: 500; letter-spacing: -0.01em; }

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--surface); color: var(--text); padding: 10px 14px; border-radius: var(--r);
}
.skip:focus { left: 8px; top: 8px; }

.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* The `hidden` attribute has to win. Its browser default is one low-priority rule, so any author
 * `display:` on the same element quietly overrides it — which is how the offline bar and the tab
 * bar ended up permanently on screen, the app claiming to be offline when it was nothing of the
 * sort. */
[hidden] { display: none !important; }

/* second marker */

/* Touched to prove a new build is noticed and installed in place, with no reinstall. */

/* ---- boot / loading --------------------------------------------------------------------- */

.boot {
  min-height: 100dvh; display: grid; place-content: center; gap: 18px; justify-items: center;
}
.boot-text { color: var(--text-3); font-size: 14px; }
.boot-mark { display: grid; gap: 6px; width: 54px; }
.boot-mark span { height: 8px; border-radius: 3px; background: var(--accent); display: block; animation: boot 1.1s ease-in-out infinite; }
.boot-mark span:nth-child(1) { width: 100%; }
.boot-mark span:nth-child(2) { width: 78%; background: var(--accent-bright); animation-delay: .12s; }
.boot-mark span:nth-child(3) { width: 54%; animation-delay: .24s; }
@keyframes boot { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }

/* ---- layout ----------------------------------------------------------------------------- */

#app { min-height: 100dvh; display: flex; flex-direction: column; }

/* Solid, not frosted. The bar used to be 86% opaque with a 14px blur, which is a nice look on a marketing page and a
   bad one on a stock list: rows sliding under it showed their words through the bar, blurred but readable, and the
   owner reported exactly that four times in one evening ("parts of it through the top lines above items"). A bar that
   content scrolls beneath is a bar with something to hide, and the only honest way to hide it is to paint it. */
.appbar {
  position: sticky; top: 0; z-index: 30;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: calc(var(--safe-top) + 10px) 14px 10px;
  min-height: calc(var(--bar-h) + var(--safe-top));
  background: var(--canvas);
  border-bottom: 1px solid var(--border-subtle);
}
.appbar-title { display: flex; align-items: center; gap: 8px; min-width: 0; }
.appbar-text { min-width: 0; }
.appbar h1 { font-size: 19px; letter-spacing: -0.02em; font-weight: var(--w-ui); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.appbar-sub { font-size: 12.5px; color: var(--text-4); margin-top: 1px; }
.appbar-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }

main {
  flex: 1; padding: 14px 14px calc(var(--tab-h) + var(--safe-bottom) + 20px);
  max-width: 780px; width: 100%; margin: 0 auto;
}

/* The rail's switch and its list of shelves belong to a window's side column. A phone's bar has neither, and the
   desktop layer (min-width: 780px) is the only place that draws them. */
.rail-sub, .rail-toggle { display: none; }

/* "A newer Binstock is on the shop server" — only ever drawn by the desktop shell (public/js/desktop-update.js).
   A strip rather than a toast, because it is not news that goes away: it is a small unfinished job at the top of the
   window until somebody reloads. Colours are the app's own notice colours, so it reads as the app talking. */
.desktop-update-bar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 8px 14px; background: rgba(93, 138, 255, 0.14);
  border-bottom: 1px solid var(--border-subtle); color: var(--text);
  font-size: 13px; position: relative; z-index: 60;
}
.desktop-update-bar[hidden] { display: none; }
.desktop-update-bar span { flex: 1 1 auto; }
.desktop-update-bar .btn { flex: 0 0 auto; }

/* A control built for one shape and not the other. Both copies exist in the markup; the media queries decide which
   one is drawn, so a window dragged narrow switches sets without needing to be reloaded. */
.only-desk { display: none; }

.tabbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
  display: grid;
  /* One column per tab, whatever the build has: this was a fixed five columns, and the fork has three tabs, so
     Dispatch, Stock and More sat in the first three fifths with two empty slots beside them and the row read
     left-of-centre (owner, 21 October 2026: "can you move dispatch stock and more, to be more centered"). With
     auto columns the three divide the bar evenly, and a build that grows a fourth tab still fills it. */
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  background: var(--panel);
  border-top: 1px solid var(--border-subtle);
  padding-bottom: var(--safe-bottom);
}
.tab {
  display: grid; justify-items: center; gap: 3px; padding: 9px 2px 8px;
  color: var(--text-4); font-size: 10.5px; font-weight: var(--w-ui); letter-spacing: 0.01em;
  position: relative; min-height: 48px;
}
.tab svg { width: 22px; height: 22px; }
.tab[aria-current='page'] { color: var(--text); }
.tab[aria-current='page']::after {
  content: ''; position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  width: 22px; height: 2px; border-radius: 0 0 2px 2px; background: var(--accent-bright);
}
.tab-dot {
  position: absolute; top: 6px; right: 50%; margin-right: -20px;
  min-width: 16px; height: 16px; padding: 0 4px; border-radius: var(--r-pill);
  background: var(--accent); color: #fff; font-size: 10px; font-weight: var(--w-strong);
  font-style: normal; display: grid; place-items: center;
}

.offline-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin: 10px 14px 0; padding: 9px 12px;
  background: var(--amber-soft); border: 1px solid rgba(217, 162, 39, 0.3);
  border-radius: var(--r); font-size: 13px; color: #f0d9a0;
}

/* ---- surfaces --------------------------------------------------------------------------- */

.card {
  background: rgba(255, 255, 255, 0.02);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 14px;
}
.card + .card { margin-top: 10px; }
.card-flush { padding: 0; overflow: hidden; }
.card-head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 12px 14px; border-bottom: 1px solid var(--border-subtle);
  background: rgba(255, 255, 255, 0.01);
}
.card-head h2 { font-size: 14px; color: var(--text-2); font-weight: var(--w-ui); letter-spacing: -0.005em; }
.card-head .count {
  font-family: var(--mono); font-size: 12px; color: var(--text-3);
  background: rgba(255, 255, 255, 0.05); border-radius: var(--r-pill); padding: 1px 8px;
}
.card-body { padding: 14px; }

.section { margin-top: 18px; }
.section-title {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  margin: 0 2px 8px;
}
.section-title h2 { font-size: 13px; color: var(--text-3); font-weight: var(--w-ui); letter-spacing: 0.02em; text-transform: uppercase; }
.section-title a, .section-title button.link { font-size: 13px; color: var(--text-3); background: none; border: 0; padding: 0; }
.section-title a:hover { color: var(--accent-bright); }

/* A folding section (Settings). The heading keeps the look of a section title, and the browser's own
   disclosure triangle is the affordance: no new control to learn, and it is there before any script
   runs. The body inherits the spacing the sections above it have. */
/* A fold is a row you can tap, so it uses the app's own 48px tap target and the surface the cards
   use, with the chevron on the right where a phone expects to find it. Ten of these stack into a
   list of panels rather than ten lines of small print. Used by Settings and by the item page. */
.fold { margin-top: 18px; }
.fold-head {
  cursor: pointer; list-style: none; display: flex; align-items: center; gap: 12px;
  min-height: 48px; padding: 13px 14px;
  background: var(--surface); border: 1px solid var(--border-subtle); border-radius: var(--r);
  transition: background 0.15s ease, border-color 0.15s ease;
}
.fold-head::-webkit-details-marker { display: none; }
.fold-head:hover { background: var(--hover); border-color: var(--border); }
.fold-head h2 {
  flex: 1; min-width: 0; font-size: 13.5px; color: var(--text-2); font-weight: var(--w-ui);
  letter-spacing: 0.02em; text-transform: uppercase;
}
/* The chevron: drawn from two borders so it needs no icon font and no dependency. It points right
   while the section is shut and turns down when it is open. */
.fold-head::after {
  content: ''; flex-shrink: 0; width: 8px; height: 8px; margin-right: 3px;
  border-right: 2px solid var(--text-3); border-bottom: 2px solid var(--text-3);
  transform: rotate(-45deg); transition: transform 0.18s ease, border-color 0.15s ease;
}
.fold[open] > .fold-head::after { transform: rotate(45deg); border-color: var(--accent-bright); }
.fold[open] > .fold-head { background: var(--panel); }
.fold-count {
  flex-shrink: 0; font-size: 12.5px; color: var(--text-4); font-variant-numeric: tabular-nums;
  min-width: 16px; text-align: right;
}
/* What is inside the lid, in one line. A phone keeps the bar and drops the line (see .fold-name below): nine lids on a
   phone is a scroll, and a title plus a sentence each would double it for no reading anybody does twice. On a window
   the same nine lids sit over a whole screen, and a bar that only says "Security" makes somebody open it to find out
   whether it was the lock or the record they wanted. */
.fold-desc { display: none; font-size: 12.5px; color: var(--text-3); line-height: 1.35; }
.fold-body { margin-top: 10px; }
.fold-body > .section:first-child { margin-top: 0; }
/* Inside an open section the small heading is a label, not a title, so it stops competing with the
   section's own name: sentence case, and quieter than the row above it. */
.fold-body .section-title h2 { text-transform: none; font-size: 12.5px; letter-spacing: 0; color: var(--text-4); }

/* ---- rows ------------------------------------------------------------------------------- */

.rows { display: block; }
.row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 14px; border-top: 1px solid var(--border-subtle);
  color: inherit; text-align: left; width: 100%; background: none; border-left: 0; border-right: 0; border-bottom: 0;
  transition: background 120ms ease;
}
.rows > .row:first-child { border-top: 0; }
a.row:hover, button.row:hover { background: rgba(255, 255, 255, 0.03); }

/* What the rows do, stacked under them: one per line and the full width, so a long label ("Draw a pattern instead")
   can never crowd a short one ("Change it") or wrap inside a third of the screen. Every card on a screen that has
   settings uses this, so the eye learns one pattern instead of three. */
.card-actions { display: grid; gap: 8px; padding: 4px 14px 14px; }
.card-actions .btn { width: 100%; min-height: 46px; }

/* The same, inside a sheet, where the padding belongs to the sheet rather than the card. */
.sheet-actions-stack { display: grid; gap: 8px; }
.sheet-actions-stack .btn { flex: none; width: 100%; min-height: 46px; }

.row-chevron { color: var(--text-4); font-size: 20px; line-height: 1; padding-right: 2px; }

/* A switch is a control, not a badge. The track and the knob say it can be thrown, so it is never mistaken for the
   status pill sitting beside it on the neighbouring card. The ::after gives it a 44px hit area without making the
   control itself look like a slab. */
.toggle { position: relative; flex: 0 0 auto; width: 50px; height: 30px; padding: 0; cursor: pointer;
  border-radius: 999px; background: rgba(255, 255, 255, 0.10); border: 1px solid var(--border-solid);
  transition: background 150ms ease, border-color 150ms ease; -webkit-tap-highlight-color: transparent; }
.toggle::after { content: ''; position: absolute; inset: -8px; }
.toggle-knob { position: absolute; top: 3px; left: 3px; width: 22px; height: 22px; border-radius: 50%;
  background: #9ca3af; transition: transform 150ms ease, background 150ms ease; }
.toggle[aria-checked="true"] { background: var(--accent); border-color: transparent; }
.toggle[aria-checked="true"] .toggle-knob { transform: translateX(20px); background: #fff; }
.toggle:disabled { opacity: 0.45; cursor: default; }
.toggle:active { transform: none; }

/* A row that states something rather than doing something: no hand, no press, no hover — nothing to press.
   Before this, every row was a <button>, so a fact looked like an action and a tap on it did nothing. */
.row-static { cursor: default; -webkit-tap-highlight-color: transparent; }
.row-static:hover, .row-static:active { background: none; }
.row-main { min-width: 0; flex: 1; }
.row-title {
  font-size: 15px; font-weight: var(--w-ui); color: var(--text);
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.row-sub { font-size: 12.5px; color: var(--text-3); margin-top: 3px; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.row-end { text-align: right; flex-shrink: 0; }
.row-end .num { font-size: 15px; color: var(--text-2); }
.row-end small { display: block; font-size: 11px; color: var(--text-4); margin-top: 2px; }
/* The line under a count box: what the app holds, plus the chip that turns a typed figure into a change.
   A phone's number keypad has no minus key, so the chip is how somebody on the bench says "take five off";
   it stays small because it sits beneath a box that is already a thumb's width. */
.count-tail { display: inline-flex; align-items: center; gap: 5px; justify-content: flex-end; flex-wrap: wrap; }
.count-tail-text { font-size: 11px; color: var(--text-4); }
.count-sign { min-width: 26px; min-height: 26px; padding: 0 6px; font-size: 13px; line-height: 1;
  border-radius: var(--r-xs); border: 1px solid var(--border); background: var(--surface); color: var(--text-2); }
/* The chip that is on says so: a sign nobody can see they have chosen is a sign they will type twice. */
.count-sign[aria-pressed='true'] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-bright); font-weight: 600; }
body.gloves .count-sign { min-width: 40px; min-height: 40px; font-size: 17px; }
.row-lead { flex-shrink: 0; width: 34px; display: grid; place-items: center; }

/* A column dragged out of the window leaves a strip at the edge to say so. Hidden by default and drawn only in the
   window block below, where the grips themselves are drawn: a phone has no grips to drag, and without this rule the
   strip is a plain div with nothing hiding it. */
.stock-col-drop { display: none; }

/* A row that can be dragged where the shop wants it. The grip is the sign; the whole bar is the handle, so
   the row lifts as a whole rather than the words sliding out of it. `touch-action: none` on the grip only:
   that is the handle a glove or a thumb can rely on, while a swipe anywhere else still scrolls the sheet. */
.drag-row { position: relative; }
.drag-grip {
  flex-shrink: 0; width: 34px; padding: 0; margin-left: -8px;
  background: none; border: 0; color: var(--text-4); font-size: 19px; line-height: 1;
  display: grid; place-items: center; cursor: grab; touch-action: none;
  -webkit-tap-highlight-color: transparent;
}
.drag-grip:active { cursor: grabbing; color: var(--text-2); }
.drag-grip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--r-xs); }
.row.is-dragging {
  background: var(--raised); z-index: 3;
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.45);
  border-radius: var(--r);
  border-top-color: transparent;
}
.reorderable.is-reordering { cursor: grabbing; user-select: none; -webkit-user-select: none; }
.reorderable.is-reordering .row { transition: none; }

/* A row wider than the phone, with a sign that there is more of it: without the fade and the chevron the row
   just stops at the edge of the screen and everything past it may as well not exist. */
.side-scroll { position: relative; }
/* The fade and the chevron have to be above the row they are drawn over. They were not: the figures strip is
   itself a sticky element with a z-index, so it painted over both and the shop could not see that there was
   anything to the side of it. */
.side-scroll::after {
  content: ''; position: absolute; top: 0; bottom: 0; right: 0; width: 36px; pointer-events: none;
  background: linear-gradient(to right, rgba(8, 9, 10, 0), var(--canvas));
  opacity: 0; transition: opacity 140ms ease; z-index: 6;
}
.side-scroll.has-more::after { opacity: 1; }
.side-scroll-next {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  width: 30px; height: 30px; padding: 0; border-radius: 50%; display: none; place-items: center;
  background: var(--raised); border: 1px solid var(--border-solid); color: var(--text-2);
  font-size: 19px; line-height: 1; cursor: pointer; z-index: 7;
}
.side-scroll.has-more .side-scroll-next { display: grid; }
.side-scroll:focus-within .side-scroll-next { border-color: var(--accent); }

/* The tick on a row that chooses something for an order. A square, because it is a box somebody ticks rather
   than a state somebody toggles — and the row itself carries aria-pressed for the same reason. */
.pick {
  width: 22px; height: 22px; border-radius: var(--r-xs); border: 1.5px solid var(--border-solid);
  display: grid; place-items: center; font-size: 13px; line-height: 1; color: var(--accent-bright);
  background: transparent;
}
.pick.is-on { border-color: var(--accent); background: var(--accent-soft); }

.dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.dot-green { background: var(--green); }
.dot-amber { background: var(--amber); }
.dot-red { background: var(--red); }
.dot-blue { background: var(--blue); }
.dot-grey { background: var(--text-4); }
.dot-accent { background: var(--accent-bright); }
.dot-pulse { box-shadow: 0 0 0 4px var(--green-soft); animation: pulse 2s ease-in-out infinite; }
@keyframes pulse { 0%, 100% { box-shadow: 0 0 0 3px var(--green-soft); } 50% { box-shadow: 0 0 0 7px rgba(39,166,68,.06); } }

/* ---- pills & badges --------------------------------------------------------------------- */

.pill {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 9px; border-radius: var(--r-pill); line-height: 1.4;
  border: 1px solid var(--border-solid);
  font-size: 11.5px; font-weight: var(--w-ui); color: var(--text-2);
  white-space: nowrap;
}
.pill-accent { background: var(--accent-soft); border-color: rgba(113, 112, 255, 0.35); color: #b9bcff; }
.pill-green { background: var(--green-soft); border-color: rgba(39, 166, 68, 0.32); color: #7fd396; }
.pill-amber { background: var(--amber-soft); border-color: rgba(217, 162, 39, 0.32); color: #e8c877; }
.pill-red { background: var(--red-soft); border-color: rgba(229, 72, 77, 0.32); color: #f2a1a4; }
.pill-blue { background: var(--blue-soft); border-color: rgba(74, 158, 218, 0.32); color: #96c8ec; }
.pill-solid { background: rgba(255,255,255,0.06); border-color: transparent; color: var(--text-2); }
/* The four tones the app actually asks for by these names, which had no rule at all: a "bad" pill and a "warn" pill
   rendered as neutral grey, so a state the code called bad looked exactly like one it called ordinary. The names below
   are aliases of the colours above, not new palettes — one place decides what red means. */
.pill-bad, .pill-red { background: var(--red-soft); border-color: rgba(229, 72, 77, 0.32); color: #f2a1a4; }
.pill-warn, .pill-amber { background: var(--amber-soft); border-color: rgba(217, 162, 39, 0.32); color: #e8c877; }
.pill-good, .pill-green { background: var(--green-soft); border-color: rgba(39, 166, 68, 0.32); color: #7fd396; }
.pill-grey, .pill-quiet { color: var(--text-3); }
/* Small pills keep the same 2px of air above and below as the ordinary ones, and 8px at the ends rather than 7:
   'short 5.6 for work' is the longest word in the row and at 1px/7px its text touched the border while its
   neighbours sat comfortably, so three pills in a line looked like three different kinds of thing. */
.pill-xs { font-size: 10.5px; padding: 2px 8px; }
.pill-lg { font-size: 13px; padding: 4px 12px; }

.badge-code {
  font-family: var(--mono); font-size: 11px; color: var(--text-3);
  background: rgba(255, 255, 255, 0.05); border: 1px solid var(--border-subtle);
  border-radius: var(--r-xs); padding: 1px 6px; letter-spacing: -0.02em;
}

/* ---- the picture of an item, and its label ----------------------------------------------- */

.photo-box { margin: 0; }
.item-photo {
  display: block; width: 100%; max-height: 320px; object-fit: contain;
  background: rgba(255, 255, 255, 0.04); border: 1px solid var(--border-subtle);
  border-radius: var(--r);
}
.photo-empty { font-size: 13px; color: var(--text-3); line-height: 1.5; }

.qr-line {
  display: flex; gap: 12px; align-items: center;
  margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border-subtle);
}
/* The canvas carries its own pixel size so it stays sharp on any screen; these rules decide how much
   room it takes on the page, which is the one thing the CSS has to say with !important. */
.qr-line canvas {
  width: 92px !important; height: 92px !important; flex: none;
  background: #fff; border-radius: 6px; padding: 3px;
}
.qr-line-text { min-width: 0; }
.qr-line-sub { font-size: 12.5px; color: var(--text-3); line-height: 1.45; }

/* The label is white whatever the theme is: it is going onto paper. */
.label-card {
  display: flex; gap: 14px; align-items: center;
  background: #fff; color: #111; border-radius: var(--r); padding: 14px;
}
.label-card canvas { width: 132px !important; height: 132px !important; flex: none; }
.label-text { min-width: 0; }
.label-code { font-family: var(--mono); font-size: 19px; font-weight: 600; color: #111; word-break: break-all; }
.label-name { font-size: 13px; line-height: 1.35; color: #333; margin-top: 3px; }
.label-spot { font-size: 12px; color: #555; margin-top: 5px; }
.label-note { font-size: 12.5px; color: var(--text-3); line-height: 1.5; margin-top: 12px; }

/* ---- filter chips ----------------------------------------------------------------------- */

.chips {
  display: flex; gap: 7px; overflow-x: auto; padding: 2px 14px 10px; margin: 0 -14px;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.chips::-webkit-scrollbar { display: none; }
.chip {
  flex-shrink: 0; padding: 6px 13px; border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.02); border: 1px solid var(--border-solid);
  color: var(--text-2); font-size: 13px; font-weight: var(--w-ui); white-space: nowrap;
  display: inline-flex; align-items: center; gap: 6px;
}
.chip[aria-pressed='true'], .chip.is-on {
  background: var(--accent-soft); border-color: var(--accent); color: var(--text);
}
.chip .n { font-family: var(--mono); font-size: 11.5px; color: var(--text-4); }
.chip[aria-pressed='true'] .n { color: var(--text-3); }

/* Two questions in one bar: WHEN (today, this week, everything) and ONLY (deliveries, collections). Six chips in
   one row used to put "Everything" next to "All" with nothing saying they answered different questions. The
   small word in front is the separation, plus a hairline where one half ends — inside a single bordered box, so it
   still reads as one bar. Narrow screens scroll sideways, which the shop is happy with; scrolling is not something
   to design out of a phone, it is how a phone is used. The question mark is pinned outside the scroll. */
/* Across the whole width, edge to edge, on its own rules top and bottom — a bar between the header and the list
   rather than a card that floats in the page. That is not decoration: a rounded box narrows the row by its own
   margins, and at 390px it cut the second word in half, so "ONLY" arrived on screen reading as "ON". The words
   that name the two halves have to be whole, even when the chips beside them are still scrolling into view. */
.filter-bar {
  display: flex; align-items: center; gap: 8px;
  /* No right padding on the bar itself: the scroll area runs to the screen edge so a chip that fits is shown
     whole. With the padding inside, the box ended 14px short and a chip that measured as fitting was drawn
     clipped — "All" arrived as "Al". A half-visible word is worse than a word off-screen. */
  padding: 8px 0 8px 14px; margin: 0 -14px 12px;
  background: rgba(255, 255, 255, 0.02);
  border-top: 1px solid var(--border-subtle); border-bottom: 1px solid var(--border-subtle);
}
.filter-scroll {
  display: flex; align-items: center; gap: 10px; flex: 1 1 auto; min-width: 0;
  overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
  padding-right: 14px;
}
.filter-scroll::-webkit-scrollbar { display: none; }
.filter-group { display: flex; flex: none; align-items: center; gap: 6px; }
.filter-what {
  font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--text-4); margin-right: 1px; flex: none;
}
/* The line between the halves is drawn darker than a hairline background edge: at 5% white it was invisible on a
   phone in daylight, and the whole point of it is to be the boundary somebody's eye lands on. */
.filter-group + .filter-group { border-left: 1px solid rgba(255, 255, 255, 0.14); padding-left: 13px; }
.filter-bar .chip { padding: 5px 9px; font-size: 12.5px; }
.filter-bar .tip-i { margin: 0; flex: none; }

/* ---- stat tiles ------------------------------------------------------------------------- */

.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: 8px; }
.stat {
  background: rgba(255, 255, 255, 0.02); border: 1px solid var(--border);
  border-radius: var(--r); padding: 10px 12px;
}
.stat .k { font-size: 11px; color: var(--text-4); letter-spacing: 0.03em; text-transform: uppercase; }
.stat .v { font-family: var(--mono); font-size: 22px; font-weight: 500; margin-top: 3px; letter-spacing: -0.03em; }
.stat .v small { font-size: 12px; color: var(--text-4); font-family: var(--font); }
.stat .stat-sub { font-size: 11.5px; color: var(--text-4); margin-top: 4px; line-height: 1.35; }
/* The summary line above the stock list: the separators, and the one figure on it you can press. Kept at the size of
   the sentence it sits in, because a count that grows into a button is a row of shelf pushed off a phone. */
.sum-sep { color: var(--text-4); }
.sum-action {
  background: none; border: 0; padding: 0; margin: 0; font: inherit;
  color: var(--accent); text-decoration: underline; text-underline-offset: 2px; cursor: pointer;
}
.sum-action:hover { color: var(--text); }
/* Where you are: no underline, solid text. The other two in the line stay links, so the row reads as a
   choice of three ways to look at the same list rather than as one action and two facts. */
.sum-action.on { color: var(--text); text-decoration: none; font-weight: 600; }
.sum-action.on:hover { color: var(--text); }
/* The figure you are looking at, when there is nothing to tap: it is the current state, so it is set solid. */
.sum-here { color: var(--text); font-weight: 600; }
.sum-action:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }
.stat .stat-sub .up { color: #e8c877; }
.stat .stat-sub .down { color: #7fd396; }
.stat-bad .v { color: #f2a1a4; }
.stat-warn .v { color: #e8c877; }
.stat-good .v { color: #7fd396; }

/* ---- buttons ---------------------------------------------------------------------------- */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  padding: 10px 15px; min-height: 42px;
  border-radius: var(--r-sm); border: 1px solid var(--border-solid);
  background: rgba(255, 255, 255, 0.02); color: var(--text);
  font-size: 14px; font-weight: var(--w-ui); letter-spacing: -0.005em;
  transition: background 130ms ease, border-color 130ms ease, transform 90ms ease;
}
.btn:active { transform: translateY(1px); }
.btn:hover { background: rgba(255, 255, 255, 0.05); }
.btn svg { width: 17px; height: 17px; }
.btn-primary {
  background: var(--accent); border-color: transparent; color: #fff; font-weight: var(--w-strong);
}
.btn-primary:hover { background: var(--accent-hover); }
.btn-subtle { background: rgba(255, 255, 255, 0.04); border-color: var(--border-subtle); color: var(--text-2); min-height: 34px; padding: 6px 11px; font-size: 13px; }
.btn-danger { background: var(--red-soft); border-color: rgba(229, 72, 77, 0.35); color: #f2a1a4; }
.btn-block { width: 100%; }
/* A compact button: the size the inline `minHeight: '34px'` on twenty-three call sites was reaching for. It lives here
   now, so gloves mode can grow it — an inline style beats the sheet, which is why those buttons stayed 34px with gloves
   on while everything around them grew. */
.btn-xs { min-height: 34px; padding: 6px 11px; font-size: 12px; border-radius: var(--r-xs); }
.btn:disabled { opacity: 0.45; pointer-events: none; }
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; }
/* Add item: the one button in the stock row that makes something rather than narrowing what is already there, so
   it is the one with colour in it. Same size and shape as its neighbours — it is a hint, not a shouting match. */
.btn-add-item { color: var(--accent); border-color: var(--border-solid, #3a3f44); font-weight: var(--w-strong); }
/* Four buttons on one line on a phone. At 11px of padding the fourth wrapped alone onto a second line — a single
   "Full screen" under three others reads as a mistake rather than a row. The row keeps the same type size and
   takes a little less air, and it is only this row: nothing else on the screen is packed this tightly. */
/* A 360 phone gives this row 332px and the four buttons came to 336 — "Full screen" dropped to a second line,
   which on this screen is a whole row of the shelf pushed off the bottom. Six pixels of padding instead of seven
   and four pixels between buttons rather than five is the difference between one line and two (20 October 2026). */
.stock-page .btn-row { gap: 8px; }
/* Across the row, not bunched at the left. The buttons used to be as wide as their words, which left a gap at the
   right-hand end of a row whose search box above runs the full width — the owner's words: "the buttons doesn't fit
   perfectly across from left to right". Now they share whatever width there is, so the row starts and ends with
   the rest of the page. */
.stock-page .btn-row > .btn { padding: 6px 6px; flex: 1 1 auto; min-width: 0; }
/* Except the magnifier, which is a square of icon: it keeps its size and the four named buttons share the rest. */
.stock-page .btn-row > .stock-search-btn { flex: 0 0 auto; }

.icon-btn {
  width: 36px; height: 36px; display: grid; place-items: center; padding: 0;
  background: rgba(255, 255, 255, 0.03); border: 1px solid var(--border-subtle);
  border-radius: var(--r-pill); color: var(--text);
}
.icon-btn svg { width: 19px; height: 19px; }
/* A button that carries a word is as wide as the word. The square one is for a row where every icon is
   the same size; the sheet's way back is a sentence with a chevron in front of it, because a lone glyph
   in the corner of a sheet is the thing the shop could not identify. */
.icon-btn.labelled {
  width: auto; height: 34px; padding: 0 12px 0 8px;
  display: inline-flex; align-items: center; gap: 4px;
}
.icon-btn.labelled svg { width: 17px; height: 17px; }
.icon-btn-word { font-size: 13px; color: var(--text-2); }
.clock-btn { color: var(--text-dim); }
/* A running clock is the one icon on a row that is not a suggestion, so it is the only one in the accent
   colour. Nothing else on the row changes: a person can see at a glance that it is already going. */
.clock-btn.on { color: var(--accent-bright); background: var(--accent-soft); }
.clock-btn.on svg { animation: clock-tick 4s linear infinite; }
@keyframes clock-tick {
  0%, 84% { transform: rotate(0deg); }
  92% { transform: rotate(6deg); }
  96% { transform: rotate(-4deg); }
  100% { transform: rotate(0deg); }
}
.icon-btn:hover { background: rgba(255, 255, 255, 0.07); }

/* ---- forms ------------------------------------------------------------------------------ */

.field { margin-bottom: 13px; }
.field label, .label {
  display: block; font-size: 12.5px; color: var(--text-3); margin-bottom: 5px; font-weight: var(--w-ui);
}
/* --text-4 on this background is around 3:1, which is too faint for the sentence that tells somebody
   what a field is for. --text-3 clears it comfortably and still reads as secondary. */
.field .hint { font-size: 11.5px; color: var(--text-3); margin-top: 4px; }
/* Every box a person types into. `tel` and `time` were missing from this list, so the phone number on Settings kept the
   browser's own width (242px in a 147px column, overlapping the email box beside it) and neither of them got the padding,
   fill or border the other boxes have — two boxes on one screen that did not look like the rest of the app. */
input[type='text'], input[type='email'], input[type='password'], input[type='date'],
input[type='number'], input[type='search'], input[type='tel'], input[type='time'], input[type='url'],
select, textarea {
  width: 100%; padding: 11px 13px;
  background: rgba(255, 255, 255, 0.02);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  color: var(--text-2); font-size: 16px; /* 16px stops iOS zooming on focus */
  min-height: 44px;
}
input[type='date'], input[type='time'] { font-family: var(--mono); font-size: 14.5px; }
input:focus, select:focus, textarea:focus { border-color: rgba(113, 112, 255, 0.5); outline: none; }
textarea { min-height: 84px; resize: vertical; line-height: 1.5; }
select { appearance: none; background-image: linear-gradient(45deg, transparent 50%, var(--text-3) 50%), linear-gradient(135deg, var(--text-3) 50%, transparent 50%); background-position: calc(100% - 17px) 19px, calc(100% - 12px) 19px; background-size: 5px 5px, 5px 5px; background-repeat: no-repeat; padding-right: 34px; }
/* minmax(0, 1fr) rather than 1fr: a grid column defaults to its content's smallest size, so one
   long line in the left box used to squeeze its neighbour to about 50px. The phone-and-email pair
   on the shop's own details was rendering 242px against 52px before this. */
.grid-2 { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 10px; }
.grid-2 > * { min-width: 0; }
.form-error { color: #f2a1a4; font-size: 13px; margin: 4px 0 0; }
.check { display: flex; align-items: center; gap: 9px; font-size: 14px; color: var(--text-2); min-height: 40px; }
.check input { width: 20px; height: 20px; min-height: 0; accent-color: var(--accent); }

/* ---- detail blocks ---------------------------------------------------------------------- */

.kv { display: grid; grid-template-columns: auto 1fr; gap: 7px 14px; font-size: 13.5px; }
.kv dt { color: var(--text-4); }
.kv dd { margin: 0; color: var(--text-2); text-align: right; }
.kv dd.num { font-family: var(--mono); }

.meta-list { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 12.5px; color: var(--text-3); }
.meta-list b { color: var(--text-2); font-weight: var(--w-ui); }

.money { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.money-lg { font-family: var(--mono); font-size: 26px; font-weight: 500; letter-spacing: -0.03em; }

/* ---- progress --------------------------------------------------------------------------- */

.bar { height: 5px; border-radius: var(--r-pill); background: rgba(255, 255, 255, 0.07); overflow: hidden; }
.bar > i { display: block; height: 100%; border-radius: var(--r-pill); background: var(--accent-bright); }
.bar-green > i { background: var(--green); }
.bar-amber > i { background: var(--amber); }
.bar-red > i { background: var(--red); }

.cure { display: grid; gap: 6px; }
.cure-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.cure-time { font-family: var(--mono); font-size: 17px; font-weight: 500; }

/* ---- steps ------------------------------------------------------------------------------ */

.steps { display: grid; gap: 2px; }
.step { display: flex; align-items: center; gap: 10px; padding: 9px 2px; border-top: 1px solid var(--border-subtle); min-height: 42px; }
.step:first-child { border-top: 0; }
.step input[type='checkbox'] { width: 20px; height: 20px; accent-color: var(--accent); flex-shrink: 0; }

/* A switch, and the phone's own answer underneath it. The answer is the point: a switch on its own can
   say "on" while nothing has been looked at for days, and this app does not do comfortable lies. */
.switch-row { display: flex; gap: 12px; align-items: flex-start; padding: 2px 0; }
.switch-row input.switch { width: 22px; height: 22px; margin: 1px 0 0; flex-shrink: 0; accent-color: var(--accent); }
.switch-label { display: flex; flex-direction: column; gap: 3px; }
.switch-title { font-size: 15px; font-weight: 600; }
.switch-sub { font-size: 13px; color: var(--text-3); line-height: 1.45; }
.step span { font-size: 14px; color: var(--text-2); }
.step.is-done span { color: var(--text-4); text-decoration: line-through; }

/* ---- timeline / activity ---------------------------------------------------------------- */

.timeline { display: grid; gap: 0; }
.tl-item { display: flex; gap: 11px; padding: 10px 0; border-top: 1px solid var(--border-subtle); }
.tl-item:first-child { border-top: 0; }
.tl-mark { width: 7px; height: 7px; border-radius: 50%; background: var(--text-4); margin-top: 7px; flex-shrink: 0; }
.tl-body { min-width: 0; }
.tl-text { font-size: 13.5px; color: var(--text-2); }
.tl-when { font-size: 11.5px; color: var(--text-4); margin-top: 2px; }

/* ---- empty / error ---------------------------------------------------------------------- */

.empty { text-align: center; padding: 34px 18px; }
.empty h2, .empty h3 { font-size: 15px; color: var(--text-2); font-weight: var(--w-ui); }
.empty p { font-size: 13px; color: var(--text-4); margin-top: 5px; }
.empty-mark { width: 40px; height: 40px; margin: 0 auto 12px; color: var(--text-4); }

.skeleton { background: linear-gradient(90deg, rgba(255,255,255,.03), rgba(255,255,255,.06), rgba(255,255,255,.03)); background-size: 200% 100%; animation: sk 1.3s ease-in-out infinite; border-radius: var(--r-sm); height: 14px; }
@keyframes sk { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
.skeleton-row { display: grid; gap: 10px; padding: 14px; }

/* ---- "somebody changed something while you were typing" --------------------------------- */

/* Sits above the tab bar, because the bottom is where a thumb already is, and it is a button: catching up is one tap
   rather than a hunt for what moved. Deliberately quiet — this is information, not an alarm. */
/* A control that cannot work without signal: greyed, and it says why when tapped. Not `disabled` — a disabled button on
   a phone answers a tap with nothing, and "nothing happened" is the least useful thing a screen can say. */
.btn.needs-signal,
.icon-btn.needs-signal,
button.needs-signal,
a.needs-signal {
  opacity: 0.45;
  filter: grayscale(0.4);
  cursor: not-allowed;
}
.needs-signal:active { transform: none; }

.live-note {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(72px + env(safe-area-inset-bottom, 0px));
  z-index: 60;
  max-width: min(92vw, 460px);
  padding: 10px 16px;
  border-radius: var(--r-pill);
  border: 1px solid var(--border);
  background: var(--raised);
  color: var(--text);
  font: inherit;
  font-size: 14px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
  cursor: pointer;
}
.live-note[hidden] { display: none; }

/* ---- sheet (bottom sheet on phone) ------------------------------------------------------ */

.sheet-backdrop {
  position: fixed; inset: 0; z-index: 50;
  background: rgba(0, 0, 0, 0.7);
  display: flex; align-items: flex-end; justify-content: center;
  animation: fade 160ms ease;
}
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.sheet {
  width: 100%; max-width: 560px; max-height: 92dvh; overflow-y: auto;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  box-shadow: var(--shadow-sheet);
  padding: 16px 16px calc(20px + var(--safe-bottom));
  animation: rise 200ms cubic-bezier(.2,.8,.2,1);
}
@keyframes rise { from { transform: translateY(14px); opacity: .6; } to { transform: none; opacity: 1; } }
.sheet-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
.sheet-head h2 { font-size: 17px; font-weight: var(--w-ui); letter-spacing: -0.02em; }
.sheet-head p { font-size: 13px; color: var(--text-3); margin-top: 3px; }
/* A field that opens a list we draw ourselves, rather than a <select> that draws its list outside the page.
   It looks like the boxes beside it, because it is the same question being answered. */
.field-pick-wrap { position: relative; }
.field-pick {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; min-height: 44px; padding: 10px 12px; text-align: left;
  background: rgba(255, 255, 255, 0.03); border: 1px solid var(--border-solid, #3a3f44);
  border-radius: var(--r-sm); color: var(--text); font: inherit; font-size: 15px;
  -webkit-tap-highlight-color: transparent;
}
.field-pick:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.pick-value { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pick-caret { flex: 0 0 auto; color: var(--text-3); font-size: 17px; }
.pick-list.tall { max-height: 52dvh; }
.pick-list .pick-add { border-top: 1px solid var(--border); }
.pick-list .pick-add .row-title { color: var(--accent); font-weight: var(--w-strong); }

/* The shelves in the filter, in a box of their own that scrolls. A <select> draws its list outside the page —
   on a phone that means it lands over the bottom bar with the last few entries unreachable. This box is ours:
   40dvh tall at most, scrollable, and the sheet still scrolls underneath it. */
/* The unit a box is counting, beside the word that asks for it: "How much arrived (kg)". */
.field-unit { color: var(--text-3); font-weight: 400; }

/* Who a thing is for: a dropdown that opens where it stands, with a box to type into. */
.pick-wrap { position: relative; }
.pick-trigger {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 12px 14px; background: var(--surface); color: var(--text);
  border: 1px solid var(--border-subtle); border-radius: var(--r);
  font: inherit; text-align: left; cursor: pointer;
}
.pick-trigger[aria-expanded='true'] { border-color: var(--accent); }
.pick-trigger-name { flex: 1 1 auto; }
.pick-trigger-caret { flex: 0 0 auto; color: var(--text-3); font-size: 12px; }
.pick-menu {
  margin-top: 8px; padding: 8px;
  background: var(--surface); border: 1px solid var(--border-subtle); border-radius: var(--r);
}
.pick-menu input { width: 100%; }

.pick-list {
  /* 30dvh, not 40: at 40 the sheet itself had to scroll as well, and two scrolls inside each other on a phone is
     a guessing game. The list takes what is left of the screen and the sheet stays still. */
  max-height: 30dvh; overflow-y: auto; -webkit-overflow-scrolling: touch;
  border: 1px solid var(--border-subtle); border-radius: var(--r-sm);
  background: rgba(255, 255, 255, 0.015);
}
.pick-list .pick-row { border-top: 1px solid var(--border-subtle); }
.pick-list .pick-row:first-child { border-top: 0; }
.pick-list .pick-row.is-on { background: rgba(93, 138, 255, 0.12); }
.pick-tick { flex: 0 0 auto; width: 18px; text-align: center; color: var(--accent); font-size: 15px; }

.sheet-actions { display: flex; gap: 8px; margin-top: 16px; }
.sheet-actions .btn { flex: 1; }

@media (min-width: 560px) {
  .sheet-backdrop { align-items: center; }
  .sheet { border-radius: var(--r-lg); }
}

/* ---- toast ------------------------------------------------------------------------------ */

.toast-root {
  position: fixed; left: 0; right: 0; bottom: calc(var(--tab-h) + var(--safe-bottom) + 12px);
  z-index: 60; display: grid; gap: 8px; justify-items: center; padding: 0 14px;
  pointer-events: none;
}
.toast {
  background: var(--raised); border: 1px solid var(--border);
  border-radius: var(--r); padding: 10px 14px; font-size: 13.5px; color: var(--text);
  box-shadow: var(--shadow-sheet); max-width: 460px;
  animation: rise 180ms ease;
  /* The area toasts sit in is tap-through so it never blocks the screen behind it. A toast that
     carries a button has to opt back in, or the button exists and cannot be pressed — which reads
     exactly like a feature that does not work. */
  pointer-events: auto;
  display: flex; align-items: center; gap: 10px;
}
.toast-said { flex: 1 1 auto; }
/* A thumb, with cold hands, on a phone that is being rained on: the offer to undo is the one control in
   the app that is only on screen for a few seconds, so it gets a real target rather than the small size
   the other in-row buttons use. */
.toast-btn { flex: 0 0 auto; padding: 8px 16px; font-size: 14px; min-height: 40px; }
.toast-bad { border-color: rgba(229, 72, 77, 0.45); color: #f2a1a4; }
.toast-good { border-color: rgba(39, 166, 68, 0.45); color: #9fdbb0; }

/* ---- tables (run sheet, valuation) ------------------------------------------------------ */

.table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.table th { text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-4); font-weight: var(--w-ui); padding: 8px 10px; border-bottom: 1px solid var(--border); }
.table td { padding: 9px 10px; border-bottom: 1px solid var(--border-subtle); color: var(--text-2); }
.table td.num, .table th.num { text-align: right; font-family: var(--mono); }
.table tr:last-child td { border-bottom: 0; }

/* ---- desktop ----------------------------------------------------------------------------
   The same app in a window, not a phone stretched wider.

   What was wrong: `main` was capped at 780px, so a 1440 window showed a phone-width column with 300px of nothing on
   each side, and the navigation stayed a strip along the bottom — the two things that make a desktop app read as a
   phone in a box. (Owner, 23 October 2026: "the desktop version still looks too much like a phone. I need you to
   fully fit everything into a desktop screen and make sure everything will fit perfectly no matter what desktop size
   screen it is.")

   What it is now: the three destinations move to a rail down the left, where a window expects them, and the page uses
   the whole window. Both come from ONE custom property, --rail-w, and the gutter is solved from the window width, so
   the title in the bar and the first pixel of the page sit on the same line at 1024 and at 3440. Nothing here adds a
   screen or a control: same three tabs, same content, laid out for the shape of the thing it is running in.

   Every rule below is a min-width query, so a phone — portrait or landscape — is untouched by all of it. */

@media (min-width: 780px) {
  main { padding-bottom: 32px; }
  #app { flex-direction: column; }
}

@media (min-width: 900px) {
  :root {
    --rail-w: 232px;
    /* The widest a line of content is allowed to get. Beyond this the page centres instead of stretching, because a
       table 2,300px wide is not "fitting the screen", it is a row you have to read like a road. */
    --page-max: 1680px;
    /* Solved, not guessed: half of whatever is left over once the rail and the page's own maximum are accounted for.
       This is what keeps the bar's title and the page's left edge in a straight line at every window width. */
    --gutter: max(24px, calc((100vw - var(--rail-w) - var(--page-max)) / 2));
  }

  /* The page starts to the right of the rail; the rail itself is fixed and takes no space in the flow. */
  #app { padding-left: var(--rail-w); }

  /* ---- the rail ------------------------------------------------------------------------ */
  .tabbar {
    position: fixed; inset: 0 auto 0 0; z-index: 40;
    width: var(--rail-w); height: 100dvh;
    display: flex; flex-direction: column; align-items: stretch; justify-content: flex-start;
    gap: 2px; padding: 14px 12px calc(14px + var(--safe-bottom));
    background: #0b0c0e; border-top: 0; border-right: 1px solid var(--border-subtle);
    backdrop-filter: none;
  }
  /* The app's name, where a window keeps it. Drawn rather than marked up: it is a label on the furniture, not another
     thing to press, and it must not appear in the tab order or a screen reader pass over navigation. */
  .tabbar::before {
    content: 'Binstock'; display: block; margin: 2px 10px 16px;
    font-size: 17px; font-weight: 650; letter-spacing: -0.02em; color: var(--text);
  }
  .tab {
    display: flex; flex-direction: row; align-items: center; justify-content: flex-start; gap: 11px;
    min-height: 44px; padding: 11px 12px 11px 14px; border-radius: 10px;
    font-size: 14px; letter-spacing: 0; text-align: left;
  }
  .tab svg { width: 20px; height: 20px; flex: 0 0 auto; }
  .tab span { font-size: 14px; }
  .tab:hover { background: rgba(255, 255, 255, 0.04); color: var(--text-2); }
  /* The mark moves from "over this tab" to "beside it": a rail is read top to bottom, so the current place is marked
     on the edge the eye travels along. */
  .tab[aria-current='page'] { background: rgba(93, 138, 255, 0.13); color: var(--text); }
  .tab[aria-current='page']::after {
    top: 50%; bottom: auto; left: 0; width: 3px; height: 22px;
    transform: translateY(-50%); border-radius: 0 3px 3px 0;
  }
  .tab-dot { top: 6px; right: auto; left: 30px; margin-right: 0; }

  /* ---- the shelves this place holds, and the switch that gives the width back ------------------ */
  /* A window has a column down the side; a phone has a bar along the bottom and a sheet behind a hamburger. Same
     list, two shapes — so the column's copy is drawn here and nowhere else. */
  .rail-sub {
    display: flex; flex-direction: column; gap: 1px;
    margin: 14px 0 0; padding-top: 12px; border-top: 1px solid var(--border-subtle);
    overflow-y: auto; overscroll-behavior: contain; min-height: 0;
  }
  /* The shelf a screen belongs to — Counting, The shelf, Money — is the strongest thing in the column after the
     app's own name, because it is what the column is *for*: somebody looking for "what it's all worth" is really
     looking for Money. It was 10.5px in the quietest grey the palette has, which read as a caption on the item below
     it. Now it is the brightest small text in the column (as bright as a page title, a size smaller than the links),
     ruled off from the group above, so the three or four sections of the rail can be seen at a glance rather than read
     line by line. Owner, 23 October 2026: "These such as counting, money, the shelf, basically the main category parts
     need to be easier to see, maybe slightly bigger text, different colour or something." */
  .rail-sub-head {
    margin: 18px 10px 6px; padding-top: 13px; border-top: 1px solid var(--border-subtle);
    font-size: 12px; font-weight: 700; letter-spacing: 0.075em;
    text-transform: uppercase; color: var(--text);
  }
  /* The first heading sits under the column's own rule, so it does not draw a second one directly below it. */
  .rail-sub-title { margin-top: 0; padding-top: 0; border-top: 0; }
  /* A group heading belongs to the rows under it, so it never ends up alone at the foot of a scroll. */
  .rail-sub-head { break-after: avoid; }
  .rail-sub a {
    display: block; padding: 7px 10px; border-radius: 8px;
    font-size: 13px; color: var(--text-2); text-decoration: none; line-height: 1.3;
  }
  .rail-sub a:hover { background: rgba(255, 255, 255, 0.04); color: var(--text); }
  .rail-sub a[aria-current='page'] { background: rgba(93, 138, 255, 0.13); color: var(--text); }

  /* A control at the foot of the column, not another destination in the list: no pill, no box, just a quiet line
     under a rule — so it reads as furniture that belongs to the rail rather than a screen you could go to. */
  .rail-toggle {
    display: flex; align-items: center; justify-content: flex-start; gap: 8px;
    margin-top: auto; min-height: 40px; padding: 14px 8px 2px; border: 0; border-radius: 8px;
    border-top: 1px solid var(--border-subtle); background: transparent;
    color: var(--text-3); font-size: 12.5px; cursor: pointer;
  }
  .rail-toggle:hover { color: var(--text-2); }
  .rail-toggle svg { width: 17px; height: 17px; flex: 0 0 auto; transition: transform 0.15s ease; }

  /* Collapsed: icons only. The words stay in the markup and stay readable to a screen reader — they are moved out of
     sight, not out of the tree — and the rail gives 168px back to whatever is on the page. */
  body.rail-icons { --rail-w: 64px; }
  body.rail-icons .tabbar { padding-inline: 8px; }
  body.rail-icons .tabbar::before { content: 'B'; margin-inline: 0; margin-bottom: 14px; text-align: center; font-size: 15px; }
  body.rail-icons .tab { justify-content: center; padding-inline: 0; }
  body.rail-icons .tab span {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap; border: 0;
  }
  body.rail-icons .tab-dot { left: auto; right: 9px; }
  body.rail-icons .rail-sub { display: none; }
  body.rail-icons .rail-toggle { justify-content: center; padding-inline: 0; }
  body.rail-icons .rail-toggle svg { transform: rotate(180deg); }
  body.rail-icons .rail-toggle-text { display: none; }

  /* ---- the bar and the page ------------------------------------------------------------ */
  /* The bar stays full-bleed so nothing slides past its edges while scrolling, but its contents are pushed to exactly
     where the page starts: the rail's width, then the same gutter the centred page gets. */
  .appbar {
    margin-left: calc(-1 * var(--rail-w));
    padding: calc(var(--safe-top) + 12px) var(--gutter) 12px calc(var(--rail-w) + var(--gutter));
  }
  .appbar h1 { font-size: 21px; }
  .appbar-sub { font-size: 13px; }

  main {
    /* page-max is the width of the CONTENT, so the two gutters are added on top of it. Without that the padding eats
       into the maximum, and a 2,560px window would show a 1,032px column — the exact complaint this work started from,
       just with better manners. */
    max-width: calc(var(--page-max) + 2 * var(--gutter)); width: 100%;
    padding: 18px var(--gutter) 44px;
  }

  /* ---- density: the same cards, laid out for a window ---------------------------------- */
  /* Figures fill the width rather than sitting in two columns built for a thumb. auto-fit means the row answers the
     window instead of a fixed count that is right on exactly one screen size. */
  /* #app is there to out-rank the phone rules further down this file (a three-column grid written for a thumb), not
     because the id means anything to the layout. */
  #app .kv-grid, #app .grid-3, #app .stat-grid { grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
  .kv-value { font-size: 1.6rem; }
  .hero-figure { font-size: 2.6rem; }

  /* A list may use the window's height: on a phone 62vh is a window onto a list, on a desktop the list IS the page. */
  /* The shelf runs to the bottom of the window.
     It used to be capped at calc(100dvh - 260px) — a guess at how much the top of the page takes, made when the page
     had a bar, a title row, a count row and a strip above it. Two of those rows have since gone and the figure was
     never revisited, so the list stopped 255px short of the bottom in a 900px window, cutting the last row in half and
     leaving bare canvas underneath it (owner, 23 October 2026: "the bottom of the item search is still cut off
     though, it needs to fit to the bottom").
     A guess cannot keep up with a page that changes, so there is no guess any more: on this screen the window is a
     column exactly as tall as the viewport, and the shelf is what is left after the headline and the strip have taken
     what they need. Whatever is above the list, the list ends at the bottom edge. */
  body.stock-headless-bar #app { height: 100dvh; overflow: hidden; }
  /* The 44px under main is the phone's bottom bar, and the 8px is the list's own breathing room. A window has neither,
     and between them they held the shelf 52px off the bottom edge after the height cap was gone. */
  body.stock-headless-bar main { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; padding-bottom: 0; }
  body.stock-headless-bar .stock-list-view { padding-bottom: 0; }
  body.stock-headless-bar .stock-page { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
  body.stock-headless-bar .stock-list-view { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
  body.stock-headless-bar .stock-table-wrap { flex: 1 1 auto; min-height: 0; display: flex; }
  body.stock-headless-bar .stock-table { flex: 1 1 auto; min-height: 0; max-height: none; padding-bottom: 2px; }
  /* More rows below the edge: the last few pixels dissolve rather than cutting a row through the middle. The class is
     set and cleared by the list itself (markMore in views-stock.js), so at the end of the list there is no fade and the
     last row finishes cleanly against the bottom of the window. */
  body.stock-headless-bar .stock-table[data-more='1'] {
    mask-image: linear-gradient(to bottom, #000 calc(100% - 38px), transparent 100%);
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 38px), transparent 100%);
  }

  /* Dialogs are dialogs: centred, and wide enough for the two-column forms inside them. A 560px sheet in a 1,440px
     window reads as a phone panel that has been dropped in the middle of the screen. */
  .sheet { max-width: 720px; max-height: 86dvh; padding: 20px 22px 22px; }
  .sheet-backdrop { align-items: center; padding: 24px; }
  .sheet-head h2 { font-size: 19px; }
  .pick-list.tall { max-height: 46dvh; }

  /* Reading width has a limit that has nothing to do with the window: past about 90 characters the eye loses the
     line it was on. Applies to prose only — figures and rows still use the whole page. */
  .kv-sub, .hint, .empty p, .card p, .doc p { max-width: 78ch; }

  /* A list of facts (name, value, name, value) becomes two pairs to a row once the window is wide. On a phone the pairs
     stack, which is right for a thumb; on a desktop a single column of eight pairs in a 1,160px card is a card with a
     strip of text down its left edge and nothing else. Values are left-aligned in this shape because the right-align
     that reads well on a phone would push each value into the next pair's name. */
  .kv { grid-template-columns: max-content minmax(0, 1fr) max-content minmax(0, 1fr); column-gap: 26px; row-gap: 10px; }
  .kv dd { text-align: left; }

  /* A record page is a card of facts and then a stack of lids. On a phone the stack is the page; on a desktop one lid
     under another down 1,100px of height is a wall of bars, and the right-hand half of the screen does nothing. The
     lids pair up instead, under a card that still spans the width — no extra panel, no second column of content that
     has to be invented, just the same twelve lids in six rows. */
  main > div:has(> .fold) {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 14px 18px; align-items: start;
  }
  /* A card above the lids (an item's figures, a person's details) is a heading for the whole page, so it keeps the
     full width and the lids begin underneath it. */
  main > div:has(> .fold) > .card { grid-column: 1 / -1; }
  main > div:has(> .fold) > .fold { margin: 0; }

  /* Each lid says what is inside it, on two lines instead of one bar. The nine lids on Settings were nine identical
     bars above two-thirds of an empty screen: nothing said whether "Security" meant the lock or the record, so the
     only way to find anything was to open it. The line is the sentence, the title still leads it. */
  .fold-name { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
  .fold-desc { display: block; }
  main > div:has(> .fold) > .fold > .fold-head { padding: 12px 16px; }

  /* The lid you open takes the width. Sharing a row with a closed one squeezed a shop's whole details form into half a
     window — the widest boxes on the screen were the ones nobody could read — so an open lid leaves the row it was in
     and the form inside it lays out across the window. Closed lids pair up underneath. */
  main > div:has(> .fold) > .fold[open] { grid-column: 1 / -1; }
  /* A line that says what is inside has done its job once it is open: the card's own headings name the boxes. */
  main > div:has(> .fold) > .fold[open] > .fold-head .fold-desc { display: none; }
  /* An open lid sits above the closed ones rather than beside them, so it is the thing being worked in. */
  main > div:has(> .fold) > .fold[open] > .fold-head { background: var(--panel); border-color: var(--border-solid); }
  /* The shop's details are three cards of boxes, one under another: full width that is 2,100px of scrolling and a
     column of inputs a thousand pixels wide, which is neither quicker to read nor quicker to fill than the phone's
     half-window version. With the lid open the form takes two columns the width of a closed lid, so the boxes stay the
     size a person types into and the whole form is about one screen. Only inside an open lid: every other form in the
     app is a sheet on a phone and is untouched. */
  main > div:has(> .fold) > .fold[open] .fold-body > form {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 18px; align-items: start;
  }
  main > div:has(> .fold) > .fold[open] .fold-body > form > .section { margin-top: 0; }
  /* The Save that belongs to the whole form stays under both columns rather than beside the last card. */
  main > div:has(> .fold) > .fold[open] .fold-body > form > .btn-row { grid-column: 1 / -1; }

  /* An empty state is a sentence in the middle of a room, so it is centred in the room rather than pinned to its left
     wall. On a phone the two are the same thing. */
  .empty { max-width: 56ch; margin-inline: auto; padding: 52px 18px; }

  /* The bar that says the shop is out of reach: it kept a phone's 14px margin, which on a desktop left it hanging off
     the rail instead of starting where the page starts. */
  .offline-bar { margin: 10px var(--gutter) 0; }

  /* Toasts were lifted clear of a bottom tab bar that no longer exists here, so they hovered in space; they now sit
     where a desktop puts them, centred over the page rather than over the whole window. */
  .toast-root {
    bottom: 24px;
    left: var(--rail-w); right: 0;
    padding: 0 var(--gutter);
  }

  /* ---- a row of pills that runs off the edge ------------------------------------------------------- */
  /* A phone hides the bar and lets a thumb do the work. A window has no thumb on that strip, and the pills past the
     edge were simply invisible — so it gets a real scrollbar, and a More button pinned to the right-hand edge (see
     public/js/scroll-rows.js) that lists everything not in sight. */
  .chips.chips-overflow, .filter-scroll.chips-overflow {
    scrollbar-width: thin;
    scrollbar-color: var(--border-solid) transparent;
    /* Room for the More button at the end of the scroll: the button is pinned over the row, so without this the last
       pill could never be brought out from under it. */
    padding-right: 36px; padding-bottom: 6px;
  }
  .chips.chips-overflow::-webkit-scrollbar, .filter-scroll.chips-overflow::-webkit-scrollbar { display: block; height: 8px; }
  .chips.chips-overflow::-webkit-scrollbar-thumb, .filter-scroll.chips-overflow::-webkit-scrollbar-thumb {
    background: var(--border-solid); border-radius: 4px;
  }
  .chips.chips-overflow::-webkit-scrollbar-track, .filter-scroll.chips-overflow::-webkit-scrollbar-track { background: transparent; }
  /* Pinned, not parked at the end of the row: it has to stay in reach while the strip is scrolled across. */
  .chip-more {
    position: sticky; right: 0; flex: 0 0 auto; align-self: center;
    display: inline-flex; align-items: center; justify-content: center;
    width: 30px; height: 30px; padding: 0; margin-left: 2px;
    border: 1px solid var(--border-solid); border-radius: var(--r-pill);
    background: var(--raised); color: var(--text-2); font-size: 15px; line-height: 1;
    box-shadow: -10px 0 12px -8px rgba(0, 0, 0, 0.65);
  }
  .chip-more:hover { color: var(--text); border-color: var(--accent); }
  .chip-more[aria-expanded='true'] { color: var(--text); border-color: var(--accent); background: var(--accent-soft); }

  /* The list of what was off the edge. */
  .rowmenu {
    position: fixed; z-index: 90; min-width: 190px; max-width: 320px; max-height: 60dvh; overflow-y: auto;
    padding: 5px; border: 1px solid var(--border-solid); border-radius: 12px;
    background: var(--raised); box-shadow: 0 18px 40px rgba(0, 0, 0, 0.5);
  }
  .rowmenu-item {
    display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%;
    padding: 8px 10px; border: 0; border-radius: 8px; background: transparent;
    color: var(--text); font-size: 13px; text-align: left;
  }
  .rowmenu-item:hover, .rowmenu-item:focus-visible { background: rgba(255, 255, 255, 0.06); outline: none; }
  .rowmenu-item[aria-checked='true'] { color: var(--text); }
  .rowmenu-tick { color: var(--accent); font-size: 13px; }

  /* ---- one row at the top, not two ---------------------------------------------------------------- */
  /* The bar said "Stock" and a count under it; the page then said the same count again. On a window the bar goes
     entirely and the page carries the name itself, in the row it already had. A phone is untouched. */
  body.stock-headless-bar .appbar { display: none; }
  body.stock-headless-bar main { padding-top: 20px; }
  /* #app again for the same reason as the name below: the phone's rule is written later in this file. */
  #app .stock-headline { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin: 0 0 12px; }
  /* #app is here to out-rank the base rule that hides this name on a phone, not for meaning: the base rule is
     written later in this file and would otherwise win the tie. */
  #app .stock-headline-name {
    display: block; margin: 0; font-size: 22px; font-weight: 680; letter-spacing: -0.01em; color: var(--text);
  }
  .stock-headline .stock-summary-line { flex: 1 1 auto; min-width: 0; }

  /* ---- the edge of a column name drags that column wider ------------------------------------------- */
  /* The handle exists on a window only: 9px is a mouse's aim rather than a thumb's, and a phone's tracks are elastic
     already. The line is drawn on the grip itself and stays quiet until the pointer is near it, so the bar does not
     become a row of dashes; it lights up on hover and while dragging, which is the only hint the feature needs. */
  /* The target reaches past the name both ways — up into the bar's own padding and down past its underline — because a
     line the height of the text is a line nobody can hit. The line itself is drawn full height and in the colour of the
     dividers, so the handle IS the divider rather than an icon added next to it. */
  .stock-line-head .stock-col-edge {
    display: block; position: absolute; top: -9px; bottom: -9px; right: -9px; width: 18px;
    cursor: col-resize; touch-action: none; z-index: 3;
  }
  .stock-line-head .stock-col-edge::after {
    content: ''; position: absolute; top: 0; bottom: 0; left: 8px; width: 2px; margin-left: -1px;
    background: var(--border-subtle); border-radius: 2px; transition: background 120ms ease, transform 120ms ease;
  }
  /* Three dots at every column edge, so the hand has something to aim at without having to hover first — a line alone
     reads as a divider, which is what it was before and why nobody found it. The dots are the grip; the line behind
     them is the column's edge. */
  .stock-line-head .stock-col-edge::before {
    content: ''; position: absolute; left: 8px; top: 50%; width: 3px; height: 3px; margin: -1.5px 0 0 -1.5px;
    border-radius: 50%; background: var(--text-4);
    box-shadow: 0 -5px 0 var(--text-4), 0 5px 0 var(--text-4);
    transition: opacity 120ms ease;
  }
  /* Hovering anywhere in a column name shows where its edge is; hovering the edge itself turns the grip into a solid
     accent pill, so the one place that does something is unmistakable under the pointer. */
  .stock-line-head > span:hover .stock-col-edge::after { background: var(--border-solid); }
  .stock-line-head > span:hover .stock-col-edge::before { background: var(--text-2); box-shadow: 0 -5px 0 var(--text-2), 0 5px 0 var(--text-2); }
  .stock-line-head .stock-col-edge:hover::after,
  .stock-line-head .stock-col-edge:focus-visible::after,
  body.stock-col-dragging .stock-line-head .stock-col-edge::after {
    background: var(--accent); transform: scaleX(2.5);
  }
  .stock-line-head .stock-col-edge:hover::before,
  .stock-line-head .stock-col-edge:focus-visible::before,
  body.stock-col-dragging .stock-line-head .stock-col-edge::before { opacity: 0; }
  .stock-line-head .stock-col-edge:focus-visible { outline: none; }

  /* ---- a column dragged out of the window ------------------------------------------------------------ */
  /* Past the right edge of the window the gesture changes meaning, and there has to be something at the edge
     saying so: a shop would otherwise drag right, watch the column stop growing, and never guess that letting go
     out there takes the column off the shelf. It sits over everything because by then the pointer is past the
     table and there is nothing else to look at. Shown by a class on the strip while a drag is running, so it
     cannot appear on a screen nobody is dragging. */
  .stock-col-drop {
    display: block; position: fixed; right: 14px; top: 50%; width: 236px; z-index: 70;
    padding: 14px 16px; border-radius: var(--r-lg);
    background: var(--raised); border: 1px solid var(--accent);
    box-shadow: var(--shadow-sheet);
    opacity: 0; transform: translateY(-50%) scale(0.97); pointer-events: none;
    transition: opacity 120ms ease, transform 120ms ease;
  }
  .stock-col-drop.is-on { opacity: 1; transform: translateY(-50%) scale(1); }
  .stock-col-drop-title {
    font-size: 11.5px; font-weight: 650; letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--accent-bright);
  }
  .stock-col-drop-name { margin-top: 6px; font-size: 15px; font-weight: 600; color: var(--text); }
  .stock-col-drop-note { margin-top: 5px; font-size: 12.5px; line-height: 1.35; color: var(--text-3); }

  /* ---- the stock screen's own toolbar ------------------------------------------------------------- */
  /* A window has room for one line where a phone needs three: the search box, the filters, the views and the two
     actions on the shelf all fit across it, so they sit together and the list starts higher up the page. What a phone
     keeps in its top bar and behind a chip is here on the page, in reach, at the size a mouse expects. */
  .only-phone { display: none !important; }
  .stock-filters { display: flex; align-items: center; gap: 8px 10px; flex-wrap: wrap; }
  /* The search box takes the room the filters do not need, and never grows past a comfortable reading width: a box
     a metre wide with three words in it is a box nobody knows what to type in. */
  .stock-filters .search-row { flex: 1 1 260px; max-width: 460px; margin-bottom: 0 !important; }
  /* The button row carries an inline margin for the phone's stacked layout; on one line it is just padding. */
  .stock-filters .btn-row { margin-bottom: 0 !important; flex: 0 0 auto; }
  .stock-filters .stock-scroll-cue { display: none; }
  .stock-desk-actions { display: flex; align-items: center; gap: 8px; margin-left: auto; }

  /* ---- the shelf with the page's top cleared away -------------------------------------------------- */
  /* The title, the figures, the search and the filter chips are four lines of a window that is shorter than a shelf.
     Minimise puts them away and leaves the column names, the rows and the rail — which is the part full screen was
     reaching for, without hiding the thing that tells somebody where they are. */
  body.stock-tidy .appbar { display: none; }
  /* !important because the headline and its summary carry their layout inline (the page builds them), and an inline
     rule out-ranks any selector short of this. */
  body.stock-tidy .stock-headline,
  body.stock-tidy .stock-summary-line,
  body.stock-tidy .stock-filters > .search-row,
  body.stock-tidy .stock-filters > .btn-row,
  body.stock-tidy .stock-filters > .stock-scroll-cue { display: none; }
  body.stock-tidy .stock-headline { display: none !important; }
  /* Up against the top instead of 46px down. The strip pins under the old bar's height, which on this screen is a bar
     that is not there — so it hung below a gap the list showed through, and its own top edge was clipped by it (owner,
     23 October 2026: "when minimising can you make it all live up to the top of the screen to give the item search
     more space, also the button if the item search screen gets cut off"). With the bar gone the strip is the top. */
  body.stock-headless-bar .stock-filters,
  body.stock-tidy .stock-filters { top: 0; }
  body.stock-tidy .stock-filters { padding-top: 4px; }
  body.stock-tidy main { padding-top: 0; }
  /* The two lines under the strip say what is being shown and how much of it. With the filters away they are the only
     thing above the shelf, so they stay — but quietly, one line each. */
  body.stock-tidy .stock-filters { margin-bottom: 6px; }
  /* Standing on a page of its own, above the figures rather than beside a search box that page does not have. */
  .stock-page > .stock-desk-actions { margin: 0 0 12px; justify-content: flex-end; }
  .stock-desk-actions .btn { min-height: 34px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
}

/* ---- added with the stock, work and more screens ----------------------------------------- */

/* An invisible wrapper, so a view can return several nodes without adding a box to the layout. */
.frag { display: contents; }

.search-row { margin-bottom: 12px; }
.search-row input { width: 100%; }

.stat-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-bottom: 16px; }
/* min-width: 0 lets a tile be narrower than its own text; the value then sizes itself to the tile
   rather than spilling past the border. £3,880.10 in a third of a 360px phone was doing exactly that. */
.stat-grid .stat { border: 1px solid var(--border-subtle); border-radius: var(--r); background: var(--panel); padding: 10px 8px; min-width: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; text-align: center; }
/* A label the width of a thumb and a number beside it are read as one fact, so both sit in the middle of the
   box rather than hugging the left edge. The label keeps two lines' worth of height so the numbers line up
   across the three boxes even when one label wraps and another does not. */
.stat-grid .stat .k { font-size: 11.5px; color: var(--text-4); min-height: 2.5em; line-height: 1.25; text-align: center; }
/* The value is given the whole width of its box and shrinks until it fits rather than running past the border:
   the stylesheet sets the size, and views-stock.js takes it down when a word like "kilograms" is wider than the
   third of a phone it lives in. */
.stat-grid .stat .v { font-size: clamp(12px, 3.7vw, 19px); font-weight: 500; font-variant-numeric: tabular-nums; letter-spacing: -0.02em; margin-top: 1px; white-space: nowrap; max-width: 100%; text-align: center; }

/* The one big number on a product page: how much is actually on the shelf. */
.hero { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.hero-v { font-size: 34px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; color: var(--text); }
.hero-v small { font-size: 13px; font-weight: 400; color: var(--text-3); margin-left: 5px; letter-spacing: 0; }
.hero-k { font-size: 12.5px; color: var(--text-3); display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* A tick big enough to hit with a thumb, for tasks and their steps. */
.tick { flex: 0 0 30px; width: 30px; height: 30px; margin-top: 2px; border-radius: var(--r-pill); border: 1.5px solid var(--border); background: transparent; color: transparent; display: inline-flex; align-items: center; justify-content: center; padding: 0; cursor: pointer; transition: background 120ms ease, border-color 120ms ease, color 120ms ease; -webkit-tap-highlight-color: transparent; }
.tick svg { width: 15px; height: 15px; }
.tick:active { transform: scale(0.94); }
.tick-on { background: var(--green); border-color: transparent; color: #06170c; }
.tick:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 2px; }

/* The little ⓘ that "Explain things as you go" turns on. Deliberately quiet: it is a hint sitting
   beside a label, not another button competing with the real ones. Nothing of it is on the screen
   when the setting is off. */
.tip-i {
  /* Small on purpose: it is an aside, not an action. Its tap area is taller than it looks — 21 by 31
     rather than 17 by 17 — because a phone gets used one-handed in a workshop, but it is not widened
     sideways beyond a couple of pixels: in a button row an invisible tap area overlapping the button
     beside it would take taps meant for that button, and a button that does the wrong thing is worse
     than a small target. */
  position: relative;
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 17px; height: 17px; margin-left: 6px; padding: 0;
  border-radius: 50%; border: 1px solid var(--border-solid, #23252a);
  background: rgba(255, 255, 255, 0.04); color: var(--text-3, #8a8f98);
  font: inherit; font-size: 10.5px; font-weight: 600; line-height: 1;
  vertical-align: middle; cursor: pointer; -webkit-tap-highlight-color: transparent;
}

.tip-i::after { content: ''; position: absolute; inset: -7px -2px; }
.tip-i:hover { color: var(--text, #f7f8f8); border-color: var(--border-strong, #34363d); }
.tip-i:focus-visible { outline: 2px solid var(--accent-bright, #7170ff); outline-offset: 2px; }
.tip-text { font-size: 14px; line-height: 1.6; color: var(--text-2, #d0d6e0); }

/* A numbered dot, for the one place where the order itself is the explanation (Help → The order
   things happen in). Reads as a list you can follow rather than a heap of facts. */
.step-n {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 19px; height: 19px; margin: 3px 9px 0 0;
  border-radius: 50%; border: 1px solid var(--border-solid, #23252a);
  background: rgba(255, 255, 255, 0.06); color: var(--text-2, #d0d6e0);
  font-size: 11px; font-weight: 600; line-height: 1;
}

/* ---- The lock on this phone (Settings → Security) ----------------------------------------------

   The lock screen is the one screen in the app that is not about the shop's work, so it is deliberately
   plain: a closed padlock, one way in, and — always, in the same place — the way in that cannot be
   forgotten. The keypad is sized for a thumb in a workshop: 68px keys, no key closer than 10px to the
   next, because a passcode typed with cold hands must not become three wrong passcodes. */

.lock-screen {
  max-width: 380px; margin: 0 auto; padding: 26px 16px 30px;
  display: flex; flex-direction: column; gap: 10px;
}
.lock-mark { display: flex; justify-content: center; color: var(--text-3, #8a8f98); }
.lock-mark svg { width: 34px; height: 34px; }
/* Where the phone has a reader, the fingerprint is drawn in the app's own accent and larger than the padlock
   it replaces: the first thing a person should see is the way in they chose, not the fallback. */
.lock-mark-lg { color: var(--accent-bright); margin-bottom: 2px; }
.lock-mark-lg svg { width: 58px; height: 58px; }
.lock-mark-sm { color: var(--accent-bright); justify-content: flex-start; }
.lock-mark-sm svg { width: 24px; height: 24px; }
.lock-why {
  margin: 0; text-align: center; color: var(--text-2, #d0d6e0);
  font-size: 13px; line-height: 1.5; padding: 0 6px;
}
.lock-title { margin: 0; text-align: center; font-size: 21px; font-weight: 500; letter-spacing: -0.01em; }
.lock-entry { display: flex; flex-direction: column; gap: 14px; margin-top: 6px; }
/* A nested heading carrying its own explanation: the words, then the ⓘ touching them. Anything the icon
   explains elsewhere in the app sits beside a button in a row, so this is the one place that needs a line. */
.filter-head { display: flex; align-items: center; gap: 8px; }
.lock-dots { display: flex; justify-content: center; gap: 10px; min-height: 14px; }
.lock-dot {
  width: 11px; height: 11px; border-radius: 50%;
  border: 1px solid var(--border-strong, #34363d); background: transparent;
}
.lock-dot.on { background: var(--accent-bright, #7170ff); border-color: var(--accent-bright, #7170ff); }

.keypad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.key {
  height: 58px; display: flex; align-items: center; justify-content: center;
  font: inherit; font-size: 21px; font-weight: 400; color: var(--text, #f7f8f8);
  border: 1px solid var(--border-solid, #23252a); border-radius: 12px;
  background: rgba(255, 255, 255, 0.045);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.key:active { background: rgba(255, 255, 255, 0.1); }
.key-quiet { font-size: 15px; color: var(--text-3, #8a8f98); }

/* The pattern pad: nine dots joined up with a finger. touch-action: none keeps a phone from scrolling
   the page while a line is being drawn across it; the line itself is an SVG laid over the dots. */
.pattern-pad { position: relative; max-width: 300px; margin: 0 auto; touch-action: none; }
.pattern-grid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 26px 26px;
  justify-items: center; padding: 18px 6px 6px;
}
.pattern-dot {
  width: 62px; height: 62px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--border-strong, #34363d); background: rgba(255, 255, 255, 0.035);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.pattern-trail { position: absolute; inset: 0; pointer-events: none; overflow: visible; }
.pattern-line {
  fill: none; stroke: var(--accent-bright, #7170ff); stroke-width: 4;
  stroke-linecap: round; stroke-linejoin: round; opacity: 0; transition: opacity 0.13s ease;
}
.pattern-line.on { opacity: 0.95; }
.pattern-dot.on {
  border-color: var(--accent-bright, #7170ff);
  background: rgba(113, 112, 255, 0.28);
  box-shadow: 0 0 0 4px rgba(113, 112, 255, 0.13);
}
.btn-block { width: 100%; justify-content: center; }

/* ---- printing a label ---------------------------------------------------------------------------
   Printing a shelf label means printing the label and nothing else: no app bar, no tabs, no sheet
   chrome, and no dark theme, because a label is read off paper. The class is put on <body> only while
   the label sheet asks for a print, so pressing Ctrl+P on any other screen still prints the page. */
@media print {
  /* Nothing may be clipped by a scrolling container when it is printed. */
  html, body { overflow: visible !important; }
  body.printing-label { background: #fff; }
  body.printing-label #app,
  body.printing-label .toast-root,
  body.printing-label .skip,
  body.printing-label .sheet-head,
  body.printing-label .sheet-actions,
  body.printing-label .label-note { display: none !important; }

  body.printing-label .sheet-backdrop {
    position: static !important; inset: auto !important; display: block !important;
    background: #fff !important; animation: none !important;
  }
  body.printing-label .sheet {
    position: static !important; width: auto !important; max-width: none !important;
    max-height: none !important; overflow: visible !important; padding: 0 !important;
    background: #fff !important; border: 0 !important; border-radius: 0 !important;
    box-shadow: none !important; animation: none !important;
  }
  body.printing-label .label-card {
    background: #fff !important; color: #000 !important;
    border: 1px dashed #999 !important; border-radius: 0 !important;
    padding: 4mm !important; gap: 4mm !important; max-width: 68mm !important;
  }
  body.printing-label .label-card canvas { width: 40mm !important; height: 40mm !important; }
  body.printing-label .label-code { color: #000 !important; font-size: 11pt; }
  body.printing-label .label-name,
  body.printing-label .label-spot { color: #000 !important; }
}

/* The barcode is the reason a label exists, so it is the thing that must not be split across a page break, and it
   prints as a block rather than as an inline box with a line-height of its own. */
@media print {
  body.printing-label .label-card svg { display: block; break-inside: avoid; }
}

/* The barcode line under a printed label: small, mono, and only there when the item has one. */
.label-barcode { font-family: var(--mono); font-size: 11px; letter-spacing: .08em; color: #444; margin-top: 4px; }
/* What was measured, and what it sells for — the two lines a shelf edge is usually printed for. */
.hero-size { font-size: 13px; color: var(--ink-dim); margin-bottom: 3px; font-variant-numeric: tabular-nums; }
.label-facts { font-size: 12px; color: #222; margin-top: 4px; font-variant-numeric: tabular-nums; }
.label-money { font-size: 13px; font-weight: 650; color: #111; margin-top: 4px; }
.label-money.label-cost { font-weight: 500; color: #555; }
/* Three label sizes, one drawing: the paper changes, not the code. */
.label-card.label-small { max-width: 190px; }
.label-card.label-small .label-code { font-size: 16px; }
.label-card.label-small .label-name { font-size: 12px; }
.label-card.label-large { max-width: 300px; }
.label-card.label-large .label-code { font-size: 22px; }
.label-card.label-large .label-name { font-size: 15px; }

/* The tick list: what goes on the label, and how big the label is. Folded away, because most prints reuse the last
   answer and a shop does not want to walk past thirteen checkboxes every time it labels a tin. */
.label-parts { margin: 12px 0 0; border: 1px solid var(--border-subtle); border-radius: var(--r); padding: 0 12px; }
.label-parts > summary { padding: 10px 0; font-weight: 600; font-size: 14px; cursor: pointer; }
.label-parts-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 2px 14px; padding-bottom: 6px; }
.label-part { display: flex; gap: 8px; align-items: flex-start; padding: 6px 0; font-size: 13px; line-height: 1.3; }
.label-part input { margin-top: 2px; flex: none; }
.label-part strong { display: block; font-weight: 500; }
.label-part em { display: block; font-style: normal; font-size: 11px; color: var(--ink-dim); }
.label-size-row { display: flex; align-items: center; gap: 10px; padding: 6px 0 10px; border-top: 1px solid var(--border-subtle); }
.label-size-label { font-size: 13px; color: var(--ink-dim); }
.label-size-row select { flex: 1; }

/* Folded size group on the item form. */
.size-fields { border: 1px solid var(--border-subtle); border-radius: var(--r); padding: 0 12px; margin: 10px 0; }
.size-fields > summary { padding: 10px 0; font-size: 13px; color: var(--ink-dim); cursor: pointer; }
.size-fields[open] > summary { color: var(--ink); }
.grid-3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
@media (max-width: 520px) { .grid-3 { grid-template-columns: 1fr 1fr; } .label-parts-grid { grid-template-columns: 1fr; } }

@media print {
  body.printing-label .label-facts,
  body.printing-label .label-money { color: #000 !important; font-size: 10.5pt; }
}

/* "£4.20 per kg" over the sentence that says which way it moved. */
.kv-line { display: flex; flex-direction: column; gap: 2px; }
.kv-line strong { font-size: 17px; }

/* Job photos: a strip of square thumbnails, wrapping, with the label under each. */
.photo-strip { display: flex; flex-wrap: wrap; gap: 10px; }
.photo-tile { padding: 0; border: 0; background: none; color: inherit; display: flex; flex-direction: column; gap: 4px; align-items: center; width: 84px; }
.photo-thumb { width: 84px; height: 84px; object-fit: cover; border-radius: 10px; background: #1b1d20; display: block; }
.photo-kind { font-size: 11px; color: var(--muted, #9aa0a6); }
.photo-full { width: 100%; border-radius: 12px; display: block; }

/* A receipt's amount, and the same amount when the money went back. The figure is the largest thing on the
   document because it is the only thing most people look at; the void marker must match it in weight, or a
   returned payment reads as money received. */
.doc-amount { display: flex; flex-direction: column; gap: 4px; padding: 12px 0; border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; }
.doc-amount-figure { font-size: 28px; font-weight: 700; letter-spacing: -0.02em; }
.doc-void .doc-amount-figure { text-decoration: line-through; color: #6b7280; }

/* An invoice on the screen, and the same thing on paper. Deliberately plain: a document people have seen
   before, not a designed page. Black on white both ways, because it is the print that matters. */
.doc { display: flex; flex-direction: column; gap: 14px; }
.doc-top { display: flex; justify-content: space-between; gap: 16px; align-items: flex-start; }
.doc-shop { font-size: 15px; font-weight: 650; }
.doc-right { text-align: right; }
.doc-title { font-size: 15px; font-weight: 700; letter-spacing: .08em; }
.doc-small { font-size: 12px; color: var(--text-4); line-height: 1.45; }
.doc-warn { color: var(--amber, #e0a11b); }
.doc-to { font-size: 14px; font-weight: 600; }
.doc-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.doc-table th { text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-4); border-bottom: 1px solid var(--line, #2a2d31); padding: 6px 4px; }
.doc-table td { padding: 7px 4px; border-bottom: 1px solid var(--line-soft, #232629); }
.doc-table .num, .doc-totals .num { text-align: right; font-variant-numeric: tabular-nums; }
.doc-totals { display: flex; flex-direction: column; gap: 5px; font-size: 13px; }
.doc-totals > div { display: flex; justify-content: space-between; gap: 24px; }
.doc-grand { font-size: 15px; font-weight: 700; border-top: 1px solid var(--line, #2a2d31); padding-top: 6px; }

@media print {
  /* The document is the page. Everything else in the app is hidden, and the sheet stops being a sheet. */
  body.printing-document { background: #fff; }
  body.printing-document #app, body.printing-document .toast-root, body.printing-document .skip,
  body.printing-document .sheet-head, body.printing-document .sheet-actions,
  body.printing-document .tip, body.printing-document .pill { display: none !important; }
  body.printing-document .sheet-backdrop {
    position: static !important; inset: auto !important; display: block !important;
    background: #fff !important; animation: none !important;
  }
  body.printing-document .sheet {
    position: static !important; width: auto !important; max-width: none !important; max-height: none !important;
    overflow: visible !important; padding: 0 !important; background: #fff !important; border: 0 !important;
    border-radius: 0 !important; box-shadow: none !important; animation: none !important;
  }
  body.printing-document .doc, body.printing-document .doc * { color: #000 !important; }
  body.printing-document .doc-table th, body.printing-document .doc-table td,
  body.printing-document .doc-grand { border-color: #999 !important; }
  body.printing-document .doc { max-width: 180mm; }
}

/* ---- a sheet of labels, printed ---------------------------------------------------------- */
/* A grid rather than a list: labels are cut up, so the page is laid out in twos and cut down the middle. */
.label-grid { display: grid; grid-template-columns: 1fr; gap: 10px; margin-top: 12px; }
body.printing-label .label-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0; }
body.printing-label .label-block {
  border: 1px dashed #bbb; border-radius: 0; margin: 0; padding: 10mm 6mm; min-height: 34mm;
  break-inside: avoid; page-break-inside: avoid; background: #fff; color: #000;
}
body.printing-label .label-block .label-code { font-size: 11px; letter-spacing: 0.04em; }
body.printing-label .label-block .label-name { font-size: 15px; font-weight: 650; line-height: 1.2; }
body.printing-label .label-block .label-barcode { font-family: ui-monospace, monospace; font-size: 11px; }

/* ---- a signature drawn with a finger ------------------------------------------------------ */
/* White paper with a dark line, whatever the theme: a signature on a dark background prints as a black
   rectangle, and the point of keeping it is that it can be printed. */
.sign-wrap { background: #fff; border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
.sign-pad { display: block; width: 100%; height: auto; touch-action: none; cursor: crosshair; }

/* ---- gloves mode --------------------------------------------------------------------------- */
/* Bigger targets and no prices. Somebody with resin on their hands is not going to hit a 32px row, and a price
   they cannot act on is one more number to misread. Only the touch targets and the charge-side money are hidden;
   nothing about the job's state changes, so a glance away and back shows the same screen. */
/* Gloves mode is the answer to a big target, so it has to reach every control, not the two classes somebody
   remembered. Measured before this: the app bar's icons stayed 36px, a row's small buttons 28px and the tick 30px while
   .btn and .chip grew to 56 — so the person who most needed the setting still got the smallest things to hit. */
body.gloves .btn, body.gloves .chip { min-height: 56px; padding: 14px 20px; font-size: 16px; }
body.gloves .icon-btn { width: 56px; height: 56px; }
body.gloves .icon-btn svg { width: 24px; height: 24px; }
body.gloves .btn-xs, body.gloves .btn-subtle { min-height: 48px; padding: 10px 16px; font-size: 15px; }
body.gloves .tick { flex-basis: 48px; width: 48px; height: 48px; }
body.gloves .row { min-height: 68px; padding: 16px 14px; }
body.gloves .row-title { font-size: 17px; }
body.gloves .row-sub { font-size: 14px; }
body.gloves .tap-lg { min-height: 64px; }
body.gloves .money, body.gloves .num.money, body.gloves .charge-only { display: none; }
body.gloves .stats { gap: 14px; }
body.gloves .stat-value { font-size: 26px; }

/* ---- the Stock page: a strip of figures, then lids ------------------------------------- */
/*
  The strip is the four numbers somebody opens this page for, held at the top while they scroll. It sits under
  the app bar and above the list, is one row of taps rather than a chart, and each cell is a door to the block
  that explains it. Nothing in it is decoration: the colour of a number is the app's one colour rule — warm for
  something due, quiet for a fact.
*/
.stock-page { --stock-strip-top: 0px; }
/* The page's five places, pinned. One row, never more than the width of the phone, and the current one is the
   only one that looks selected — the tabs answer "where am I", which is the question ten stacked lids could
   never answer. */
/* Five equal cells was the first answer, and it quietly assumed things: a phone at least as wide as the
   design, the default text size, and short counts. "Items 226" on a slightly smaller screen and the labels were
   squeezed, cut, and there was nothing to push — the overflow existed but could not be reached. So the cells
   size themselves, and when they do not fit the row scrolls: a thumb swipe keeps the browser's own momentum, a
   mouse drag moves it, and while there is more past the edge the wrapper draws a fade so nobody has to guess.
   The full phrase is still the tab's accessible name, so nothing here depends on seeing the fade. */
.stock-tabs-wrap {
  /* Under the app bar, not under the thumb: a sticky row at 0 is a row that disappears behind the bar as soon
     as the page moves. The offset is measured from the bar by the page and handed in as a custom property. */
  position: sticky; top: var(--stock-tabs-top, 0px); z-index: 6;
  margin: 0 0 10px;
  background: var(--canvas);
  border-bottom: 1px solid var(--border-subtle, #232629);
}
.stock-tabs-wrap::after,
.stock-tabs-wrap::before {
  content: ''; position: absolute; top: 0; bottom: 0; width: 30px;
  pointer-events: none; opacity: 0; transition: opacity 120ms ease; z-index: 1;
}
.stock-tabs-wrap::after { right: 0; background: linear-gradient(to right, rgba(0,0,0,0), var(--canvas) 70%); }
.stock-tabs-wrap::before { left: 0; background: linear-gradient(to left, rgba(0,0,0,0), var(--canvas) 70%); }
.stock-tabs-wrap[data-more='1']::after { opacity: 1; }
.stock-tabs-wrap[data-back='1']::before { opacity: 1; }
.stock-tabs {
  display: flex; gap: 4px;
  /* When they all fit, spread them across the width as the old equal cells did; when they do not, there is no
     free space to spread and this does nothing. */
  justify-content: space-between;
  padding: 8px 0;
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none; -ms-overflow-style: none;
  touch-action: pan-x; overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  cursor: grab; user-select: none; -webkit-user-select: none;
}
.stock-tabs::-webkit-scrollbar { display: none; }
.stock-tab {
  flex: 0 0 auto; scroll-snap-align: center; -webkit-user-drag: none;
  padding: 7px 10px; border-radius: var(--r-pill, 999px); justify-content: center;
  color: var(--text-3); font-size: 12px; font-weight: var(--w-ui, 500); text-decoration: none;
  display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
  border: 1px solid transparent;
}
.stock-tab .n { font-family: var(--mono); font-size: 11px; color: var(--text-4); }
.stock-tab[aria-current='page'] {
  background: var(--accent-soft); border-color: var(--accent); color: var(--text);
}
.stock-tab[aria-current='page'] .n { color: var(--text-3); }


/* ---- the Items list: columns, separate lines, and a list that can fill the screen ---------------- */
/* There is no grip bar. It sat above the list, looked like a piece of the table, and was the second way to do what
   the Full screen button beside Filter already does. */

/* The shelf as a table: five columns, wider than a phone, in one scroller that goes both ways.
   The column bar is inside the scroller and sticky, so it is over the right column after a sideways push and
   still on screen after a downward one. The two sizes come from one custom property, so the bar and the rows
   cannot drift apart — the bug that made "ID" sit over a quantity. */
/* The wrapper carries the "there is more to the right" mark, because the scroller below it cannot: anything
   drawn inside a scroller scrolls away with the content. */
.stock-table-wrap { position: relative; }
/* A chip rather than a bare glyph: drawn over the sliver of the next column, a lone chevron reads as a
   rendering fault. On its own surface it reads as a control, and it is one — tapping it pushes the columns
   along for anybody who would rather tap than swipe. It is in the tab order with a name, so it is not a
   decoration only a sighted thumb can use. */
/* The cue that the row carries on: a thin bar under the column names, thumb showing how much is in sight. The
   same sign a phone uses for anything that scrolls, and it never covers a word. */
/* The cue that the row carries on: the underside of the column bar itself, a hairline track with a thumb the width
   of the part of the row you can see. Same sign a phone uses for anything that scrolls, and it never covers a
   word. Sticky to the left so it stays in the window while the names slide under it. */
.stock-scroll-cue {
  /* A plain block in the pinned strip now — it used to be sticky inside the table's own head, which stopped
     mattering the moment it moved out of the scroller (20 October 2026). Its width is set to the scroller's by
     views-stock, so the thumb's share is the share of the row actually in sight. */
  position: static; height: 4px; margin: 6px 0 0;
  border-radius: 999px;
  /* A solid grey, not a wash: this bar sits above the column names and a 10–14% white track let whatever was behind
     it (a row card, the bare canvas) tint it, which the owner read as the app being see-through (25 October 2026:
     "It's still see through above the lines"). The track is now the same grey as every other edge in the app, so
     there is nothing in it to see through. */
  background: var(--border-solid);
  overflow: hidden;
  /* No gap and the bar's own colour bleeding round it: the two read as poured together rather than as a bar with
     a widget loose underneath. */
  box-shadow: 0 0 0 4px var(--canvas);
}
.stock-scroll-cue[hidden] { display: none; }
.stock-scroll-thumb {
  display: block; height: 4px; width: 40px; border-radius: 999px;
  background: var(--text-2);
  transition: transform 90ms linear;
}
/* No dark fades down the sides. They were meant to say "there is more this way", and what they actually did was
   paint over the first letters of the item names and the last letters of the figures — a shadow on the words is
   a worse answer than a button that says what it does (19 October 2026). The ways to know the row goes on are
   the half-column at the edge and the named chip below it. */
.stock-table {
  --stock-cols: minmax(240px, 1fr) 96px 84px 112px 88px;
  max-height: 62vh;
  overflow: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  border-top: 1px solid var(--border-subtle);
}
/* No permission to see what it costs, no worth column: a heading over a column of dashes is a column that
   costs every reader a sideways push to learn nothing. */
.stock-table.stock-no-cost { --stock-cols: minmax(240px, 1fr) 96px 84px 112px; }
/* The column bar and the little bar that says how much of the row is in sight are one thing, pinned together at
   the top of the scrolling box. The wrapper holds the stickiness; the names inside it travel sideways with the
   columns, while the little bar stays put across the window. */
/* The heading row sits above the table box, not inside it: a sticky element inside a box that scrolls in either
   direction has nowhere to stick to. On a desk the box is 62vh and never scrolls away, so the headings are simply
   there; on a phone they stick (see the phone block).

   The bar is sticky, its child is slid sideways with a transform to follow the rows, and Android Chrome has painted
   the rows it is covering on top of it for a frame more than once — the owner has now reported text showing through
   "the back of the boxes" three times (25 October 2026). The fills below it are provably opaque (#08090a, no alpha)
   and there is only one bar in the DOM, so the styles are not the fault: promoting the bar to a layer of its own is
   what stops the rows being drawn over it, and the canvas-coloured scroller behind it means even a frame where the
   bar's own fill has not been painted shows the page's colour rather than a word. */
.stock-head-wrap {
  position: static;
  background: var(--canvas);
  transform: translateZ(0);
  isolation: isolate;
}
/* One window over the heading grid, so the headings can be slid sideways with the rows without a second scrollbar
   appearing beside them. It carries the same colour as the bar and the page: a transparent window over a sticky bar
   is one more place for the rows to be seen through. */
.stock-head-scroll { overflow: hidden; background: var(--canvas); }
.stock-line-head {
  display: grid; grid-template-columns: var(--stock-cols); gap: 4px;
  min-width: var(--stock-min, 680px);
  /* The headings sit over boxes now, so they take the box's own inset: 1px border plus 9px padding. A heading
     half a box's padding to the left of its own numbers reads as sloppy without anybody being able to say why. */
  /* Five pixels a side: the row card's own 1px border and 4px padding. A heading box's left edge now sits exactly on
     the left edge of the figure frame under it — with the headings boxed, a 5px drift that used to be invisible
     reads as boxes that do not line up with anything. */
  padding: 9px 5px 7px; margin: 0;
  background: var(--canvas);
  font-size: 10.5px; letter-spacing: 0.07em; text-transform: uppercase; color: var(--text-4);
}
/* A heading is a box with an edge you can actually see. It used to be a bare label over a 1px hairline, and that
   seam was transparent — whatever sat behind the bar (a row card sliding under it, the page itself) showed through
   the gap as a sliver of a different grey, which read as the app being see-through rather than as a division. Owner,
   25 October 2026: "there's a small gap between the boxes and ones around it, I didn't realise it was see through,
   can you make it some sort of grey outlines instead of see through?" So: an opaque fill in the page's own colour
   nothing can show through, and a grey edge one step brighter than the row's hairline so a box reads at a glance.
   The edge is drawn *inside* the cell (an inset ring, not a border), so the word keeps the exact seat it had and
   every heading stays over its own figures — a border would have pushed each one a pixel right of the numbers it
   names, and on a phone it would have cost the bar a pixel of height for nothing. */
.stock-line-head > span {
  position: relative;
  background: var(--canvas);
  box-shadow: inset 0 0 0 1px var(--border-solid);
  border-radius: 5px;
  padding-left: 10px;
}
.stock-line-head > span.stock-line-head-qty { padding-right: 10px; }
/* The bar divides its columns with the box's own edge now, so the old seam between two headings has gone: two boxes
   side by side would otherwise show two hairlines and a gap between them. The grip still sits on the right edge of
   its box, where the pointer expects to find it. */
/* The edge of a column name is the handle that resizes it. Drawn on the window only: a phone has no room to aim at a
   9px strip with a thumb, and its tracks are elastic anyway. The line itself stays quiet until the pointer is near it —
   a bar of nine visible handles over a shop's shelf list would be louder than the list. */
.stock-col-edge { display: none; }
body.stock-col-dragging { cursor: col-resize; user-select: none; }
/* A column the shop has turned off is not drawn, and the grid above simply has one fewer track to fill. The
   hiding is done here rather than by not building the cells, so turning a column back on costs nothing and the
   row keeps the same markup for the tests to read. */
.stock-line-head:not([data-cols~='id']) .stock-col-id,
.stock-table:not([data-cols~='id']) .stock-col-id,
.stock-line-head:not([data-cols~='qty']) .stock-col-qty,
.stock-table:not([data-cols~='qty']) .stock-col-qty,
.stock-line-head:not([data-cols~='where']) .stock-col-where,
.stock-table:not([data-cols~='where']) .stock-col-where,
.stock-line-head:not([data-cols~='spot']) .stock-col-spot,
.stock-table:not([data-cols~='spot']) .stock-col-spot,
.stock-line-head:not([data-cols~='worth']) .stock-col-worth,
.stock-table:not([data-cols~='worth']) .stock-col-worth,
.stock-line-head:not([data-cols~='lasts']) .stock-col-lasts,
.stock-table:not([data-cols~='lasts']) .stock-col-lasts,
.stock-line-head:not([data-cols~='onorder']) .stock-col-onorder,
.stock-table:not([data-cols~='onorder']) .stock-col-onorder,
.stock-line-head:not([data-cols~='cost']) .stock-col-cost,
.stock-table:not([data-cols~='cost']) .stock-col-cost,
.stock-line-head:not([data-cols~='price']) .stock-col-price,
.stock-table:not([data-cols~='price']) .stock-col-price,
.stock-line-head:not([data-cols~='min']) .stock-col-min,
.stock-table:not([data-cols~='min']) .stock-col-min,
.stock-line-head:not([data-cols~='order']) .stock-col-order,
.stock-table:not([data-cols~='order']) .stock-col-order,
.stock-line-head:not([data-cols~='barcode']) .stock-col-barcode,
.stock-table:not([data-cols~='barcode']) .stock-col-barcode,
.stock-line-head:not([data-cols~='supplier']) .stock-col-supplier,
.stock-table:not([data-cols~='supplier']) .stock-col-supplier,.stock-line-head:not([data-cols~='moved']) .stock-col-moved,
.stock-table:not([data-cols~='moved']) .stock-col-moved { display: none; }
.stock-line-head-qty { text-align: right; }
.stock-line-head-where { padding-left: 2px; }
/* The dot is part of the line's first column, so the word above it starts where the dot does — and the name
   under it starts after the dot. Without this the heading sits 16px left of every name it is heading. */
.stock-line-head > span:first-child { padding-left: 10px; }

/* One item, one panel. The gap between them is the separation: a shelf read as hairlines under one heading
   is a wall, and the eye has nowhere to stop. */
.stock-lines { display: grid; gap: 8px; padding-bottom: 4px; }
.stock-line {
  display: grid; grid-template-columns: var(--stock-cols); gap: 4px;
  min-width: var(--stock-min, 680px);
  align-items: stretch;
  /* One item, one panel — and the figures in frames inside it.
     The owner has asked for both halves of this, four days apart, and they are not in conflict: each figure sits in
     its own box so "how much" and "where" never run together as one line of type (20 October 2026: "make each
     section in stock with items, ID, etc separate boxes"), and those boxes are held inside the item's own panel so a
     row reads as one thing rather than five that happen to be level (23 October 2026: "the boxes need to stay
     together per item, keeping the name and supplier etc together but just in separate boxes within the boxes").
     So: the row is the outer box, the fields are the inner ones, and the gap between ROWS is what separates items —
     no two items anywhere near each other, no two fields inside an item drifting apart. */
  padding: 4px;
  /* A shade stronger than the hairlines the app uses between blocks: this edge has to be findable in a list of twenty
     rows without becoming a frame drawn round every item. */
  border: 1px solid rgba(255, 255, 255, 0.09); border-radius: 12px; background: var(--surface);
  color: inherit; text-decoration: none;
}
/* The inner boxes are quieter than the outer one: one hairline each, a shade lighter, so nesting reads at a glance
   and the panel's own edge is still the first line the eye finds. */
.stock-line > span,
.stock-line > a {
  min-width: 0; padding: 4px 9px;
  border: 1px solid rgba(255, 255, 255, 0.05); border-radius: 8px; background: rgba(255, 255, 255, 0.028);
}
/* Everything that qualifies the figure, on its own line where it can wrap without pushing a column out. It is
   a box too, and the only one that spans the row. */
.stock-line > .stock-line-meta {
  grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 6px 8px; align-items: center;
  margin-top: 0; padding: 4px 9px;
  border: 1px solid rgba(255, 255, 255, 0.05); border-radius: 8px; background: rgba(255, 255, 255, 0.028);
}
/* Hovering one item lifts the whole panel, not one cell of it: the row is the thing being pointed at. */
.stock-line:hover { border-color: var(--border-solid); }
.stock-line:hover > span, .stock-line:hover > a,
.stock-line:focus-visible > span, .stock-line:focus-visible > a { border-color: rgba(255, 255, 255, 0.09); }
.stock-line:hover > span, .stock-line:hover > a { background: rgba(255, 255, 255, 0.045); }
.stock-line:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.stock-line-name {
  display: flex; gap: 8px; align-items: flex-start; min-width: 0;
  /* Between words, never inside one: "BUY-BOLT-" on one line and "A4" on the next reads as two different
     things. A long name may take two lines and the row grows — that is cheaper than a name nobody can read. */
  font-size: 15px; font-weight: var(--w-ui); color: var(--text);
  overflow-wrap: break-word; word-break: normal;
}
.stock-line-name .dot { flex-shrink: 0; }
/* The name and its category, stacked: a name is words somebody said, a category is a label. Two type sizes and two
   colours do that without a divider, which is what stops the category reading as the end of the name. */
.stock-line-title { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.stock-line-title-name { min-width: 0; }
.stock-line-cat {
  font-size: 10.5px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-4);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.stock-line-code {
  font-size: 11.5px; color: var(--text-3); padding-top: 2px;
  /* A code is one thing, not two: it stays on one line and the column is wide enough to hold it. If a longer
     code ever turns up it is cut with an ellipsis rather than broken in half — the whole of it is still in the
     line's own name for anybody who asks a screen reader. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.stock-line-qty { text-align: right; white-space: nowrap; }
.stock-line-qty strong { display: block; font-size: 15px; font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }
.stock-line-qty small { display: block; font-size: 11px; color: var(--text-4); }
/* Everything that qualifies the figure, on its own line where it can wrap without pushing a column out. */
.stock-line-meta { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 6px 8px; align-items: center; margin-top: 3px; }
.stock-line-words { flex: 1 1 100%; font-size: 12.5px; color: var(--text-3); overflow-wrap: anywhere; }
/* Where it is kept and what it is worth: columns of their own, so a person can read down the shelf for either
   one instead of hunting for it in a sentence. Long place names stop at the column and say "+2" rather than
   pushing the worth off the end of the row. */
.stock-line-where {
  min-width: 0; font-size: 11.5px; color: var(--text-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.stock-line-where small { margin-left: 3px; color: var(--text-4); }
.stock-line-worth {
  text-align: right; font-family: var(--mono); font-size: 12px; color: var(--text-3);
  white-space: nowrap; font-variant-numeric: tabular-nums;
}
.stock-line-quiet { color: var(--text-4); }


/* A row wider than the phone, kept in one shape: the place rows and the movements keep their columns and the
   end of the line is a swipe away, with the fade and the chevron saying so. Without the overflow the row just
   runs off the edge and the figure at the end may as well not exist. */
.wide-rows { overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
.wide-rows::-webkit-scrollbar { display: none; }
.wide-rows > .row { min-width: 460px; }

/* The density setting has to reach the new lines too: somebody who asked for a tighter shelf gets a tighter
   shelf, not a compact page with one card-shaped exception in the middle of it. */
.stock-page.stock-compact .stock-line { padding: 2px; gap: 3px; }
.stock-page.stock-compact .stock-line > span,
.stock-page.stock-compact .stock-line > a,
.stock-page.stock-compact .stock-line > .stock-line-meta { padding: 2px 8px; }
/* Compactness now lives in the boxes, not in the row: the row has no padding of its own any more, so a density
   setting changes the frame's inset and the gap between frames. */
.stock-page.stock-compact .stock-line-name { font-size: 14px; }
.stock-page.stock-compact .stock-line-meta { gap: 3px 6px; margin-top: 2px; }
.stock-page.stock-compact .stock-line-words { font-size: 11.5px; }
/* Full screen: the same list, repositioned over the page rather than rebuilt, so the row somebody was
   reading stays under their thumb. The tab bar goes with it — "full screen" with five icons of somebody else's
   navigation down one edge is not full screen, and on a phone it is another half a row of shelf. Back, at the top
   of the list, is the way out and it never scrolls away. */
body.stock-list-big .tabbar { display: none; }
body.stock-list-big .stock-list-view { padding-bottom: 0; }
.stock-page.list-big .stock-list-view {
  position: fixed; inset: 0; z-index: 30; background: var(--canvas);
  display: flex; flex-direction: column;
  overflow: hidden; overscroll-behavior: contain;
  padding: 0 14px 10px;
}
/* In full screen the table is the screen: the wrapper takes whatever is left after the grip and the words, and
   the table inside it does both the scrolling, so the column bar is always the top row of what is moving. It is
   the wrapper that is the flex item, not the table — sizing the table left it three thousand pixels tall. */
.stock-page.list-big .stock-table-wrap { flex: 1; min-height: 0; display: flex; }
.stock-page.list-big .stock-table { flex: 1; min-height: 0; max-height: none; }
/* In full screen the search and the Filter button scroll away and the column bar stays: the words at the top
   of the columns are worth more of a small screen than a search box nobody is typing in. */
.stock-page.list-big .stock-filters { position: static; padding-top: 2px; }
/* One line, never two. On a narrow phone (and a 360 one is narrower than the 390 the row was measured on) the four
   buttons plus the magnifier came to 356px against 332 available, and Back dropped onto a second line — a whole
   row of the shelf gone to a wrap. In full screen the row is now tighter, the gaps are smaller and it refuses to
   wrap at all; "Add item" is "Add" there, because the ✚ already says the rest. */
.stock-page.list-big .stock-filters .btn-row { flex-wrap: nowrap; gap: 8px; }
.stock-page.list-big .stock-filters .btn-row > .btn { padding: 6px 6px; min-width: 0; }

/* The search row is a row, and its ✕ is a circle. The button used to be a 36px square stretched to the height of
   the field it sits beside — an oval with a big ✕ pushed against one edge. Now the field takes the width and the
   button keeps its own shape. */
.stock-page .search-row { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.stock-page .search-row input { flex: 1 1 auto; min-width: 0; width: auto; }
/* The phone's own clear button inside a search field, if it draws one, is a second ✕ with its own idea of where
   the middle is — and it sits against the field's edge, which is exactly what "still not centered" looks like.
   There is one cross on this row and it is the round button beside the box. */
.stock-page .search-row input[type='search']::-webkit-search-cancel-button,
.stock-page .search-row input[type='search']::-webkit-search-decoration {
  -webkit-appearance: none; appearance: none; display: none;
}
/* The cross is two bars of this button, not a glyph and not a child element. Each bar is pinned to the button's
   exact centre and turned ±45°, so its centre is the button's centre whatever the button's display, font, padding
   or the layout of anything around it. */
.stock-page .search-close {
  position: relative;
  width: 40px; height: 40px; flex: 0 0 auto; border-radius: 50%;
  color: var(--text-2); padding: 0;
  font-size: 0; line-height: 0;
}
.stock-page .search-close::before,
.stock-page .search-close::after {
  content: ''; position: absolute; left: 50%; top: 50%;
  width: 15px; height: 2px; border-radius: 1px; background: currentColor;
}
.stock-page .search-close::before { transform: translate(-50%, -50%) rotate(45deg); }
.stock-page .search-close::after { transform: translate(-50%, -50%) rotate(-45deg); }

/* The search box in full screen is something you ask for. The magnifier sits in the button row next to Filter;
   the box it opens has its own ✕, and closing it clears the words so the list never filters itself for a reason
   nobody can see. */
.stock-page .stock-search-btn { display: none; }
.stock-page.list-big .stock-search-btn { display: inline-flex; }
.stock-page .search-close { display: none; }
.stock-page.list-big .search-close { display: inline-flex; }
.stock-page.list-big.search-open .stock-filters .search-row { display: flex; }

/* Full screen means the shelf, not a page with a shelf in it. The summary line and the search box are the two
   things that can wait: the summary is repeated by the tabs, and searching is a thing you do before you go to
   the shelf. What is left above the list is one row of buttons and the column bar — measured on the phone, that
   is five rows of items turned into seven (20 October 2026). */
.stock-page.list-big .stock-list-view > .hint { display: none; }
.stock-page.list-big .stock-filters .search-row { display: none; }
.stock-page.list-big .stock-filters { padding-top: 8px; }
.stock-page.list-big .stock-line { padding: 0; gap: 4px; }
.stock-page.list-big .stock-line-meta { gap: 4px 8px; }
@media (max-width: 380px) {
  /* The columns keep their width rather than squeezing: five columns on a 360 phone is what the sideways
     scroll is for, and a squeezed column is a column nobody can read even after finding it. */
  .stock-line-head, .stock-line { gap: 4px; }
  .stock-line-name { font-size: 14.5px; }
}

/* One page of the stock page. Nothing here is decoration: the gap is what stops two blocks' borders meeting. */
.stock-view { display: flex; flex-direction: column; gap: 2px; }
.stock-list-view { padding-bottom: 8px; }
/* The Short page draws its list bare — the tab is the lid — so the card needs the space the lid would have taken. */
.stock-bare > .card { margin: 0 0 10px; }

/* One line of a sheet that can be added or taken away: the fields share the row, the way out sits at the end of
   it, and on a phone the row wraps rather than squeezing the number box to nothing. */
.sheet-line {
  display: grid; grid-template-columns: minmax(0, 1fr) 84px auto; gap: 8px; align-items: center;
  margin-bottom: 8px;
}
.sheet-block {
  border: 1px solid var(--border-subtle); border-radius: var(--r-sm);
  padding: 12px; margin-bottom: 10px;
}
.sheet-block > .btn { margin-top: 8px; }
@media (max-width: 380px) {
  .sheet-line { grid-template-columns: minmax(0, 1fr) 70px auto; }
}

/* The page's own headline: name and count on one row. A phone keeps the name in the bar above (which also holds the
   menu and the search box), so only the count is drawn here; a window brings the name down and drops the bar. */
.stock-headline { display: block; margin: 0 0 8px; }
.stock-headline-name { display: none; }

/* The list's own controls, pinned under the tabs. */
.stock-filters {
  position: sticky; top: 46px; z-index: 5;
  background: var(--canvas); padding: 6px 0 8px; margin: 0 0 6px;
  border-bottom: 1px solid var(--border-subtle, #232629);
}

.stock-strip {
  position: sticky; top: 46px; z-index: 5;
  /* Two by two on a phone, one row on anything wider. Four columns at 390px leaves about 90px a cell, and a
     label that has to be cut to fit is a label nobody can read — the numbers fit either way, so the grid is
     what gives. */
  display: grid; grid-template-columns: 1fr 1fr; gap: 1px;
  margin: 0 0 12px; border: 1px solid var(--border-subtle); border-radius: var(--r);
  overflow: hidden; background: var(--border-subtle);
}
/* Five or more figures: the grid would grow a row and push the work down the screen, so it becomes one row
   that swipes, the same two cells wide as the grid it replaces. */
.stock-strip.many {
  display: flex; overflow-x: auto; scroll-snap-type: x proximity;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.stock-strip.many::-webkit-scrollbar { display: none; }
/* 44%, not 48%: two cells and a sliver of the third. The sliver is the point — a strip of exactly two cells
   looks like a complete strip of two figures, and the shop said it could not tell there was more to the side.
   A third cell cut by the edge says it without a word. */
.stock-strip.many .stock-strip-cell { flex: 0 0 44%; scroll-snap-align: start; }

.stock-strip-cell {
  display: flex; flex-direction: column; gap: 2px; align-items: flex-start;
  padding: 10px 10px 11px; background: var(--surface); border: 0; text-align: left;
  color: inherit; font: inherit; cursor: pointer; min-width: 0; text-decoration: none;
}
.stock-strip-cell.static { cursor: default; }
.stock-strip-cell:active { background: var(--hover); }
.stock-strip-num {
  font-size: 16px; font-weight: 600; letter-spacing: -0.01em; color: var(--text);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.stock-strip-label {
  font-size: 11px; line-height: 1.3; color: var(--text-4);
}
.stock-strip-cell.quiet .stock-strip-num { color: var(--text-3); font-weight: 500; }
@media (min-width: 520px) { .stock-strip { grid-template-columns: repeat(4, 1fr); } }
.stock-coverage { margin: -4px 0 12px; }
.stock-coverage .btn { padding-left: 0; }
.stock-digest { margin: 0 0 10px; }

/* The lid of a block carries what is inside it, so the shut version still answers the question. */
.stock-fold { margin-bottom: 8px; }
.stock-fold > .fold-head h2 { flex: 0 1 auto; }
.stock-fold > .fold-head .fold-sub {
  flex: 1; min-width: 0; font-size: 11.5px; color: var(--text-4);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.stock-fold > .fold-head .fold-count { font-size: 12.5px; color: var(--text-3); }
.stock-fold > .fold-head .fold-count.warn { color: var(--amber, #e8c877); }
/* While a long list scrolls, its own name stays at the top: the page has ten lids and losing track of which one
   you are reading is the cost of having ten. */
.stock-fold > .fold-head { position: sticky; top: 0; z-index: 4; }

/* The same lid, on the two screens that are lists of lists: Today and Jobs. The head carries the count, and
   the count in red is the shut version's whole job — it says how much is behind the lid and whether any of it
   is late, so a shut lid is never a secret. */
.list-fold { margin: 0 0 8px; }
.list-fold > .fold-head h2 { flex: 0 1 auto; }
.list-fold > .fold-head .fold-sub {
  flex: 1; min-width: 0; margin-left: 6px; font-size: 11.5px; color: var(--text-4);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.list-fold > .fold-head .fold-count { font-size: 12.5px; color: var(--text-3); }
.list-fold > .fold-head .fold-count.warn { color: var(--amber, #e8c877); }
.list-fold > .fold-head .fold-count.bad { color: var(--red, #e8837a); }
.list-fold > .fold-body { margin-top: 8px; }
.list-fold > .fold-body > .card:last-child { margin-bottom: 0; }

/* Compact rows: the same rows with less air. Nothing is hidden, because a density setting that drops information
   is a different screen pretending to be a preference. */
.stock-page.stock-compact .row { padding-top: 7px; padding-bottom: 7px; }
.stock-page.stock-compact .row-sub { font-size: 11px; gap: 4px; }
.stock-page.stock-compact .row-title { font-size: 13.5px; }
/* 44 pixels, not 36: this is a target somebody hits repeatedly with a thumb, and a near miss reorders the page. */
.big-tap { width: 44px; height: 44px; min-width: 44px; }
.btn.big-tap { width: auto; padding: 0 12px; }
/* A card with no heading still gets its actions, along the foot. Without this the buttons were written and
   never drawn — a card of rows that looked finished. */
.card-foot {
  display: flex; align-items: center; justify-content: flex-end; gap: 10px; flex-wrap: wrap;
  padding: 9px 14px; border-top: 1px solid var(--border-subtle);
}
.card-foot .hint { margin-left: auto; }

/* ---- the time of day: a clock you press, and the time beside it -----------------------------------
   A bare `type=time` box on a phone is "-- : --" and a little system clock, and nothing on it says the clock
   is the thing to press. This bar is the whole control: the time on the left as big mono figures once one has
   been chosen, a clock drawn at the end as the way in, and an × to take the time back off. The phone's own
   picker still does the choosing, so the gesture is the one this person already knows. */
.time-bar {
  flex: 1; display: flex; align-items: center; gap: 10px; min-height: 56px; padding: 8px 12px;
  background: var(--raised); border: 1px solid var(--border); border-radius: 12px;
  cursor: pointer; text-align: left; color: inherit;
}
.time-bar:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.time-bar:active { border-color: var(--accent); }
.time-bar-input {
  flex: 1; min-width: 0; background: none; border: 0; padding: 0;
  color: var(--text-3); font-family: var(--mono); font-size: 21px; letter-spacing: 0.04em;
  -webkit-appearance: none; appearance: none;
  /* The bar is the button. The box is read out from the bar's own name, so it never eats the tap. */
  pointer-events: none;
}
.time-bar-input::-webkit-calendar-picker-indicator { opacity: 0; width: 0; margin: 0; }
.time-bar.is-set .time-bar-input { color: var(--text); }
.time-bar-clock {
  flex: 0 0 auto; display: grid; place-items: center; width: 34px; height: 34px; border-radius: 50%;
  background: rgba(255, 255, 255, 0.06); color: var(--text-2);
}
.time-bar.is-set .time-bar-clock { color: var(--accent); background: rgba(94, 106, 210, 0.16); }
.time-bar-clear {
  flex: 0 0 auto; width: 34px; height: 34px; padding: 0; border-radius: 50%;
  background: none; border: 1px solid var(--border); color: var(--text-3);
  font-size: 17px; line-height: 1; cursor: pointer;
}
.time-bar-clear:hover { color: var(--text); border-color: var(--border-solid); }
.time-bar-clear:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.time-bar-clear[hidden] { display: none; }

/* ---- holidays: the calendar -------------------------------------------------------------------------------
   Seven columns at 360px is about 44px a cell, so the numbers stay small and the dots carry the meaning. The
   grid draws no outer frame: each day is its own quiet square, and only a day with somebody away is filled —
   a calendar where every cell is bordered reads as a table of boxes rather than a month. */
.cal-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 2px 0 10px; }
.cal-title { font-size: 14.5px; font-weight: var(--w-strong); color: var(--text-2); letter-spacing: 0.01em; }
.cal-arrow {
  flex: 0 0 auto; width: 36px; height: 36px; padding: 0; border-radius: var(--r-sm);
  display: grid; place-items: center; font-size: 19px; line-height: 1; color: var(--text-2);
}
.cal-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 3px; }
.cal-dow { margin-bottom: 4px; }
.cal-dow-cell {
  /* 10.5px and --text-3, not 9.5px and --text-4: at phone size the weekday letters vanished into the background,
     and a calendar whose columns cannot be named is a grid of numbers. */
  text-align: center; font-size: 10.5px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-3);
  padding-bottom: 3px;
}
.cal-blank { min-height: 38px; }
.cal-day {
  position: relative; min-height: 38px; padding: 3px 0 4px; border: 0; border-radius: var(--r-xs);
  background: transparent; color: var(--text-2); font: inherit; font-size: 12.5px;
  display: flex; flex-direction: column; align-items: center; justify-content: flex-start; gap: 3px;
  font-variant-numeric: tabular-nums;
}
.cal-day.is-weekend { color: var(--text-4); }
.cal-day.is-off { background: var(--accent-soft); color: var(--text); }
.cal-day.is-closed { background: rgba(255, 255, 255, 0.045); color: var(--text-3); }
/* Today is a ring rather than a fill, so it cannot be mistaken for a day somebody is away. */
.cal-day.is-today { box-shadow: inset 0 0 0 1px var(--accent-bright); }
.cal-num { line-height: 1.25; }
/* The day already chosen when the picker opens. A ring, like today, but stronger — the two can be the same day, and
   then the ring IS the answer: this is where you are. Filled would read as "somebody is off", which is what the
   same class means on the page's own calendar. */
.cal-day.is-picked { background: var(--accent-soft); box-shadow: inset 0 0 0 1px var(--accent); color: var(--text); }
.cal-day.is-picked .cal-num { font-weight: var(--w-strong); }
/* A date on a form: the look of a text box, the behaviour of a button that opens the calendar. Left-aligned words
   with the caret at the far end, so it reads as a field and not as a chip. */
.input-btn { display: flex; align-items: center; justify-content: space-between; gap: 8px; text-align: left; cursor: pointer; }
.cal-dots { display: flex; gap: 2px; height: 5px; align-items: center; }
.cal-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--accent-bright); }
.cal-dot-sick { background: var(--amber); }
.cal-dot-other { background: var(--text-4); }

/* The month in the calendar's header is a button — the way to any month, not just the next one. It keeps the
   weight of a heading (it IS the heading) and gains only a caret, so nothing about the bar moves. */
.cal-title-btn {
  display: inline-flex; align-items: center; gap: 5px; padding: 4px 8px; margin: 0 -8px;
  background: transparent; border: 0; border-radius: var(--r-sm); color: var(--text-2);
  font: inherit; font-size: 14.5px; font-weight: var(--w-strong); letter-spacing: 0.01em;
}
.cal-title-btn:active { background: var(--hover); }
.cal-title-caret { font-size: 10px; color: var(--text-4); line-height: 1; }

/* Year and months in one sheet: a stepper for the year (any year at all) and twelve targets for the month. */
.mp-yearbar { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.mp-year {
  flex: 1; text-align: center; font-size: 20px; font-weight: var(--w-strong); color: var(--text);
  font-variant-numeric: tabular-nums; letter-spacing: 0.01em;
}
.mp-months { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
.mp-month {
  min-height: 44px; border: 1px solid var(--border-subtle); border-radius: var(--r-sm);
  background: rgba(255, 255, 255, 0.02); color: var(--text-2); font: inherit; font-size: 13px;
}
.mp-month.is-on { background: var(--accent-soft); border-color: rgba(94, 106, 210, 0.5); color: var(--text); }
.mp-month:active { background: var(--hover); }

/* ---- The stock room at a glance (views-kpis.js) ------------------------------------------------
   Nine figures on one screen. Two columns on a phone because a single column of nine numbers is a
   list nobody reads to the end; the grid keeps each figure big enough to see from arm's length. */
.kv-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}
.kv-value { font-size: 1.45rem; font-weight: 650; line-height: 1.15; letter-spacing: -0.01em; }
.kv-value.is-warn { color: var(--warn, #d9a13b); }
.kv-label { font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-3, #8b8b93); margin-top: 2px; }
.kv-sub { font-size: 0.86rem; color: var(--text-2, #b6b6be); margin-top: 4px; }
.kv-grid > .card { min-width: 0; }
.hero { display: flex; flex-direction: column; gap: 2px; }
.hero-figure { font-size: 2.3rem; font-weight: 680; letter-spacing: -0.02em; line-height: 1.1; }
.is-tappable { cursor: pointer; }
@media (min-width: 520px) {
  .kv-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* ---- The shelf on a phone: one box an item, in columns, scrolled both ways ------------------------
   Three passes got here. It began as a 680px grid of columns inside a 62vh box — two scrolls in two
   directions inside one frame. Then a page-scrolling column of separate little boxes: readable, but the
   figures floated and nothing said which number was which. Then one card an item with the facts joined
   inside it — but still unlabelled, and the cells did not line up from row to row, so a shelf of nine
   items had nine different column positions.

   What he asked for, in order: "make each item one box but with the other boxes connected inside of it
   and the same as in the bench app make the 2 axis option to scroll both ways", then "line up the stuff
   with item, ID, on hand etc etc so everything lines up and is easy to [see] what is for what".

   So: the row is a real grid again, on the same column tracks as the column bar above it, inside a single
   card. Item and its warnings span the row; ID, on hand, kept and worth sit in their own columns under
   their own headings, and because every row shares one track set, they line up from the first item to the
   last. The card is wider than the phone on purpose and the list scrolls sideways, with the bar under the
   headings saying how much is in sight. Wide screens are untouched. */
@media (max-width: 760px) {
  .stock-table-wrap {
    overflow-x: auto;
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
  }
  .stock-table {
    max-height: none;
    overflow: visible;
    border-top: 0;
    padding-bottom: 6px;
  }
  .stock-lines { gap: 6px; }

  /* The column bar: the same tracks as the rows, scrolling with them, so a heading is always over its own
     column. Not pinned — the search and the buttons above it are already pinned, and three stacked bars on
     a phone is most of the screen before the shelf starts. */
  /* Stuck under the filter strip: the headings stay put while the shelf slides under them. --stock-pin is measured
     in views-stock (46px of app bar plus whatever the strip is tall today), so the two never overlap. */
  .stock-head-wrap {
    position: sticky;
    top: var(--stock-pin, 150px);
    z-index: 4;
    padding: 0;
    border-bottom: 1px solid var(--border-subtle);
    /* A hair of shadow is what separates "stuck" from "part of the page" when a row slides under it. */
    box-shadow: 0 6px 10px -8px rgba(0, 0, 0, 0.9);
  }
  .stock-head-scroll { min-width: 0; }
  /* Full screen turns the filter strip static, so there is nothing above the headings to stick under — stuck at a
     fixed offset they would float in the middle of the shelf. */
  .stock-page.list-big .stock-head-wrap { position: static; box-shadow: none; }
  /* Four headings over four tracks. The item's own heading goes: its name already runs across the top of
     every card, and a heading over a track no figure uses was what pushed the rest one column out. */
  .stock-line-head {
    display: grid;
    min-width: 0;
    /* No gap and one pixel in: the row's first pixel is its card border, so without this the headings sat
       four to eleven pixels to the right of the figures they name, drifting further with every column. */
    gap: 0;
    /* Five pixels a side, the row card's own 1px border and 4px padding: the heading boxes now sit exactly over the
       figure frames they name instead of a few pixels off, which is only visible now the headings have edges. */
    padding: 0 5px 4px;
    /* Opaque, not "none": the strip has padding around the boxes, and while that padding was see-through the row
       sliding under the bar ghosted through it — the sliver the owner photographed above the boxes (25 October
       2026: "It's still see through above the lines"). The boxes are opaque; now the strip they sit on is too. */
    background: var(--canvas);
  }
  .stock-line-head > span { padding: 0 6px; }
  /* The tracks touch on a phone (no gap, on purpose: that is how a heading stays over its figures), so a ring all
     the way round every box would draw two lines in every seam and a doubled corner at every join. Each box draws
     its left edge and its top and bottom instead, and only the last one closes the right-hand end — one clean grey
     line between neighbours, which is what the boxes were asked for in the first place. */
  .stock-line-head > span {
    border-radius: 0;
    box-shadow: inset 1px 0 0 var(--border-solid), inset 0 1px 0 var(--border-solid), inset 0 -1px 0 var(--border-solid);
  }
  .stock-line-head > span:last-child {
    box-shadow: inset 0 0 0 1px var(--border-solid);
  }
  /* Bigger and plainer than the desk version: 5px, no canvas halo, a brighter thumb. At 4px with a halo the bar
     read as an underline under the first heading rather than as something that moves. */
  .stock-scroll-cue {
    display: block;
    height: 5px;
    margin: 3px 0 5px;
    /* Solid grey here too — the phone drew it as a 14% wash, and that is the bar the owner could see through. */
    background: var(--border-solid);
    box-shadow: none;
  }
  .stock-scroll-thumb { height: 5px; background: var(--text-2); }

  /* One box an item, on the shared tracks. */
  /* Five columns — Item, ID, On hand, Kept, Worth — at sizes a person can read come to about 434px, and no phone
     screen holds that. Squeezing them to fit is what makes a column unreadable, so the row is a real table again:
     its true widths, the headings pinned above it, and the sideways scroll for the rest, exactly as the Bench app
     does it. Measured on the live shelf: ID needs 92px (`ZZ-LABEL-CHK-M`), On hand 70 (`126.2kg`), Kept and Worth
     76 each (`no place`, `£403.84`); the name takes what is left, never less than 120. */
  .stock-line {
    display: grid;
    grid-template-columns: var(--stock-cols);
    min-width: var(--stock-min, 380px);
    gap: 0;
    border: 1px solid var(--border-subtle);
    border-radius: 10px;
    background: var(--surface);
    overflow: hidden;
  }
  /* Six pixels a side instead of nine: at nine, four columns need 356px and a 360 screen cuts the fourth. The
     phone widths in STOCK_COLUMNS are measured against this padding. */
  .stock-line > span {
    min-width: 0;
    padding: 5px 6px;
    border: 0;
    border-radius: 0;
    background: none;
  }
  /* The item is the first column now, not a line across the card: that is what lets the headings run Item, ID, On
     hand, Kept, Worth across the top and every row answer in the same places. It wraps rather than clipping, and
     the category goes under it in small letters so the name keeps most of the column. */
  .stock-line > .stock-line-name {
    grid-column: auto;
    display: block;
    padding: 6px;
    border-bottom: 0;
    font-size: 12.5px;
    line-height: 1.25;
    white-space: normal;
  }
  .stock-line-title { flex-direction: column; align-items: flex-start; gap: 1px; }
  .stock-line-cat { font-size: 9.5px; letter-spacing: 0.03em; }
  /* The warnings line closes the bottom of the card. */
  .stock-line > .stock-line-meta {
    grid-column: 1 / -1;
    padding: 5px 10px;
    border: 0;
    border-top: 1px solid var(--border-subtle);
    border-radius: 0;
    background: none;
    gap: 4px 6px;
  }
  .stock-line-meta:not(:has(.pill)):not(:has(.stock-line-words)) { display: none; }
  /* One hairline between neighbouring columns, and none at the card's own edges. */
  /* The worth column is drawn but not given a track on a phone unless it has been switched on (see views-stock:
     four columns fit a 390 screen, five do not). `grid-column` is what takes it out of the flow rather than a
     different markup for phones. */
  [data-phone-cols]:not([data-phone-cols~='worth']) .stock-line > .stock-col-worth,
  [data-phone-cols]:not([data-phone-cols~='worth']) .stock-col-worth { display: none; }
  .stock-line > span:not(.stock-line-meta) { border-left: 1px solid var(--border-subtle); }
  .stock-line > span:not(.stock-line-meta):first-child { border-left: 0; }
  /* Nothing clips. A short figure never wraps; a long code wraps inside its own column rather than being cut
     with an ellipsis — the point of the ask was to be able to read what is what. */
  .stock-line > span { white-space: normal; overflow-wrap: anywhere; }
  .stock-line > .stock-col-qty, .stock-line > .stock-col-worth { white-space: nowrap; overflow-wrap: normal; }
  .stock-line > .stock-col-id { font-size: 11px; }
  .stock-line-title { flex-direction: row; flex-wrap: wrap; align-items: baseline; gap: 6px; }
  .stock-line-cat { font-size: 10px; }
  /* One class and an element, (0,1,1) — anything heavier outranks the (0,3,0) rules that hide switched-off
     columns, which is how Lasts / On order / Moved came back on every phone row. The name cell sets its own
     display below. */
  .stock-line > span { display: flex; align-items: center; }
  .stock-line > .stock-col-id,
  .stock-line > .stock-col-where,
  .stock-line > .stock-col-spot,
  .stock-line > .stock-col-worth,
  .stock-line > .stock-col-lasts,
  .stock-line > .stock-col-onorder,
  .stock-line > .stock-col-moved { font-size: 12px; }
  .stock-line > .stock-col-qty { font-size: 13.5px; }
  .stock-line > .stock-col-qty strong { display: inline; font-size: 14px; }
  .stock-line > .stock-col-qty small { display: inline; margin-left: 2px; font-size: 11px; }
  .stock-line .pill-xs { font-size: 10.5px; padding: 1px 6px; }
  .stock-line-words { flex: 0 1 auto; font-size: 11.5px; }
}

/* ---- The code on its own ----------------------------------------------------------------------------------
   The QR in the item card and on the label is drawn at the size those cards can spare. On its own sheet it is
   fitted to the phone: the canvas keeps its 640 dots and CSS brings it down, so the pattern stays crisp when it is
   blown up to half a screen and a second phone's camera has something to read. White behind it, because a QR on a
   dark card is a QR that scans badly in a workshop. */
.qr-big {
  display: grid;
  place-items: center;
  padding: 14px;
  border-radius: 12px;
  background: #fff;
}
.qr-big canvas {
  width: min(74vw, 320px);
  height: auto;
  /* A pixel pattern that has been scaled by the browser: nearest-neighbour keeps the modules square. */
  image-rendering: pixelated;
}

/* ---- A photographed sheet, in its viewer ---------------------------------------------------
   The hazard and COSHH sheets are photographs of paper, and the viewer had no rule for either of its two classes at
   all: an untamed <img> in a sheet draws at its own pixel size, so a 12-megapixel phone picture was a wall of paper
   with the buttons somewhere off the bottom. Fitted here, and dark behind it because a sheet photographed in an
   aisle is often a page on a dark bench. */
.photo-frame {
  display: grid;
  place-items: center;
  min-height: 120px;
  margin: 4px 0 12px;
  border-radius: 12px;
  background: #0a0c10;
  overflow: hidden;
}
.photo-frame img,
.photo-img {
  display: block;
  width: 100%;
  height: auto;
  /* Tall enough to read a warning diamond, short enough to leave the buttons reachable. */
  max-height: 62vh;
  object-fit: contain;
}

/* A sheet that IS the screen: the recipe list rose to 92dvh and left a strip of the page above it, which on a
   390px phone was two rows of parts and no more (owner, 21 October 2026: "a way to go full screen on the item
   list as it's not showing much in the list"). */
.sheet-full {
  height: 100dvh;
  max-height: 100dvh;
  border-radius: 0;
  border-bottom: 0;
  padding-top: calc(12px + var(--safe-top));
}

/* A search box with a button beside it: the box takes the width, the button keeps its words. */
.search-with-full { display: flex; gap: 8px; align-items: center; }
.search-with-full > input { flex: 1 1 auto; min-width: 0; }
.picker-full-btn { flex: 0 0 auto; white-space: nowrap; }

/* The product picker as the screen: a box to search in, what it found, and the list with every row the window can
   hold — no heading, no form, nothing else competing for the height. Back at the top returns to the sheet underneath,
   which is stacked rather than replaced, so a movement already half filled in is still there (the 190px list in that
   sheet shows three items, which is enough only when you already know the name). */
.sheet.sheet-picker {
  max-width: none;
  height: 100dvh;
  max-height: 100dvh;
  border-radius: 0;
  border: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  padding: calc(12px + var(--safe-top)) 16px calc(12px + var(--safe-bottom));
}
.sheet-picker .sheet-head { flex: 0 0 auto; margin-bottom: 10px; }
.sheet-picker .picker-search { flex: 0 0 auto; }
.sheet-picker .picker-search input { width: 100%; }
.sheet-picker .picker-said { flex: 0 0 auto; margin: 8px 0 0; }
.sheet-picker .picker-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; margin-top: 8px; padding-bottom: 4px; }

@media (min-width: 780px) {
  /* On a desk the list is the point: the room above it goes back to the rows, and the column is wide enough to use the
     window without running a product name the whole way across a 27-inch screen. */
  .sheet-picker .sheet-head,
  .sheet-picker .picker-search,
  .sheet-picker .picker-said,
  .sheet-picker .picker-list { width: 100%; max-width: 1180px; margin-left: auto; margin-right: auto; }
}

/* The copies kept on this machine, and where the other copy goes. Rows rather than cards: five dates and five sizes
   are a list, and a list is read faster than five boxes. The off-site half is one select and the boxes that belong to
   the answer, so switching between a bucket and a folder cannot leave the wrong half on the screen. */
.backup-copies { display: flex; flex-direction: column; gap: 6px; }
.backup-copy {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border: 1px solid var(--border-subtle); border-radius: var(--r-sm);
}
.backup-copy-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; font-size: 13.5px; }
.offsite { border-top: 1px solid var(--border-subtle); margin-top: 14px; }
.offsite-details { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }
.offsite select { width: 100%; }

/* Two buttons, side by side, that both mean "a picture" and mean different ways of getting one: the camera in
   the shop's hand, or the gallery where the screenshot of the emailed sheet is. They wrap rather than squash. */
.picture-chooser { display: inline-flex; gap: 8px; flex-wrap: wrap; }

/* The picker: the whole stock list, a box beside each part, and a foot that stays put while the list scrolls.
   It is a full screen because choosing nine parts is a piece of work, not a detail on another screen. */
.bom-pick-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  margin-top: 4px;
}
.bom-pick-row {
  display: flex; align-items: center; gap: 10px;
  width: 100%; padding: 11px 10px; margin-bottom: 6px;
  background: none; border: 1px solid var(--border-subtle); border-radius: var(--r-sm);
  color: inherit; font: inherit; text-align: left;
}
.bom-pick-row.is-chosen { border-color: var(--accent, #4f8cff); background: rgba(79, 140, 255, 0.10); }
.bom-pick-row.is-taken { opacity: 0.45; }
.bom-pick-box {
  flex: 0 0 22px; height: 22px; width: 22px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--border); border-radius: 5px; font-size: 14px; line-height: 1;
}
.bom-pick-row.is-chosen .bom-pick-box { background: var(--accent, #4f8cff); border-color: var(--accent, #4f8cff); color: #fff; }
.bom-pick-main { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.bom-pick-name { font-size: 14px; }
.bom-pick-foot {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 0 calc(6px + var(--safe-bottom));
  border-top: 1px solid var(--border-subtle);
  margin-top: 8px;
}

/* One part per line. The amount and the stage are shown, not boxed: a recipe of eight parts has to be readable
   without scrolling past two labelled inputs per row. Tapping the line opens it. */
.bom-line {
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-sm);
  margin-bottom: 6px;
}
.bom-line.is-open { border-color: var(--border); background: rgba(255, 255, 255, 0.02); }
.bom-line-head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; padding: 9px 10px;
  background: none; border: 0; color: inherit; font: inherit; text-align: left;
}
.bom-line-main { min-width: 0; }
.bom-line-title { font-size: 14px; font-weight: var(--w-ui); overflow-wrap: anywhere; }
.bom-line-end { display: flex; align-items: center; gap: 6px; flex: none; }
.bom-line-end .mono { font-size: 13px; color: var(--text-2); }
.bom-line-end .chev { font-size: 13px; color: var(--text-3); }
.bom-line-body { padding: 2px 10px 10px; }

/* The shape of a price over time, on the price sheet: one line, oldest on the left. Colour comes from the
   element so the dark and light themes do not each need their own rule. */
.spark-wrap { margin: 2px 0 12px; padding: 10px 12px 8px; border: 1px solid var(--border-subtle); border-radius: var(--r); background: var(--panel); }
.spark { display: block; width: 100%; height: 44px; color: var(--accent, #7aa2f7); }
.spark-wrap .hint { margin: 6px 0 0; }

/* The code in letters, under its own QR: big enough to read across a bench, because a phone can be dead, scratched
   or out of signal and a number that only a camera can read stops working when the camera does. */
.qr-code-word { margin: 10px 0 4px; text-align: center; }
.qr-code-word .mono { font-size: 26px; letter-spacing: 0.16em; font-weight: 600; color: var(--text); }
.qr-code-label { font-size: 11.5px; letter-spacing: 0.04em; color: var(--text-4); text-transform: uppercase; margin-bottom: 4px; }

/* ---- the scan station ----------------------------------------------------------------------------------------
   The camera is the screen: a rounded frame with the video filling it, portrait shaped, so the phone is held upright
   and one label fills most of it. Black inside whatever the theme is, because a camera preview is not a panel. */
.scan-stage { margin: 0 0 10px; }
.scan-frame { position: relative; aspect-ratio: 4 / 3; border-radius: var(--r); overflow: hidden; background: #000; border: 1px solid var(--border-subtle); }
.scan-frame video { width: 100%; height: 100%; object-fit: cover; display: block; }
.scan-shut { display: grid; place-items: center; height: 100%; color: #9aa4b2; font-size: 13px; }
/* The app's own scanner: no video element here, because the camera is a native screen. The panel is the target, so it
   is as tall as the video it replaces — the same screen, one fewer thing to aim at. */
/* A block of buttons that leads a sheet — one wide answer before the list of things that already exist. */
.sheet-actions-lead { flex-direction: column; align-items: stretch; margin: 4px 0 12px; }
.sheet-actions-lead .btn { width: 100%; }
.scan-lens { display: grid; place-items: center; gap: 10px; aspect-ratio: 4 / 3; border-radius: var(--r); background: #0b0f14; border: 1px dashed var(--border-subtle); padding: 16px; text-align: center; }
.scan-lens-mark { font-size: 30px; line-height: 1; color: #5c6b7a; }
.scan-lens-say { margin: 0; font-size: 13px; color: #9aa4b2; max-width: 24ch; }
.btn-lens { width: 100%; max-width: 260px; }
.scan-keep { display: flex; align-items: center; gap: 10px; padding: 10px 2px; font-size: 13.5px; color: var(--text-2); }
.scan-keep input { width: 18px; height: 18px; }
.scan-head { font-size: 13px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-4); margin: 16px 2px 8px; }
/* The number between the two buttons is the biggest thing on the sheet: it is what the thumb is about to change. */
.scan-amount { display: grid; grid-template-columns: 56px 1fr 56px; gap: 8px; align-items: center; margin: 12px 0 0; }
.scan-amount input { text-align: center; font-size: 22px; font-variant-numeric: tabular-nums; padding: 10px 6px; }
.btn-step { font-size: 20px; padding: 10px 0; }
.scan-asks { display: grid; gap: 8px; }

/* The one case where the browser wants a gesture before it will draw the camera: a hint over the black frame, not an
   error, and it goes away the moment the picture starts. */
.scan-tap { position: absolute; inset: 0; display: grid; place-items: center; background: rgba(0, 0, 0, 0.55); color: #fff; font-size: 14px; letter-spacing: 0.02em; }

/* Saying the shop is not trading normally. Solid, like the bars: a strip you can read through is a strip you can miss. */
.trading-strip {
  position: sticky; top: 0; z-index: 60; display: flex; gap: 10px; align-items: baseline;
  padding: 9px 14px; font-size: 13px; line-height: 1.35;
  background: var(--panel); border-bottom: 1px solid var(--line); color: var(--text-2);
}
.trading-strip strong { font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--text); }
.trading-strip.reading { border-left: 3px solid var(--warn, #d19a2a); }
.trading-strip.stopped { border-left: 3px solid var(--bad, #c0392b); }
@media (max-width: 520px) { .trading-strip { flex-direction: column; gap: 2px; } }
