:root {
  --bg: #f5f6f8;
  --card: #ffffff;
  --ink: #1b1f24;
  --muted: #5d6b79;
  --line: #dde2e8;
  --accent: #23506b;
  --accent-ink: #ffffff;
  --warn: #8a3b2e;
  --warn-bg: #fbeae6;
  /* Nothing on the page meant "this worked": a save and an error looked alike at a glance. */
  --ok: #1d6b4b;
  --ok-ink: #ffffff;
  --notice-bg: #eaf0f6;
  --notice-line: #b9cbdc;
  --mono: ui-monospace, SFMono-Regular, Menlo, monospace;
  --gutter: 16px;
  /* The smallest a finger reliably hits, and the size every button is held to. */
  --touch: 44px;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #14181c;
    --card: #1d2228;
    --ink: #e8edf2;
    --muted: #93a1af;
    --line: #2c343d;
    --accent: #6ea8cc;
    --accent-ink: #0f1a22;
    --warn: #f0a794;
    --warn-bg: #3a211b;
    --ok: #4f9e77;
    --ok-ink: #0f1a22;
    --notice-bg: #1f2a33;
    --notice-line: #3c5163;
  }
}
* { box-sizing: border-box; }
html, body { margin: 0; }
body {
  background: var(--bg);
  color: var(--ink);
  font: 15px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  padding: env(safe-area-inset-top) 0 0;
  display: flex;
  flex-direction: column;
  min-height: 100svh;
}
.hidden { display: none !important; }

.top { padding: 10px var(--gutter) 8px; border-bottom: 1px solid var(--line); background: var(--card); position: relative; }
.top-row { display: flex; gap: 8px; align-items: center; }
.top-row + .top-row { margin-top: 8px; }
.updated { font-size: 12px; color: var(--muted); flex: 1; font-family: var(--mono); justify-content: flex-start; padding-left: 0; }

/* A thin indeterminate bar while the app is waiting on the repository: it has no measurable
   pieces to report, only whether it is still going. */
.progress { position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; overflow: hidden; background: var(--line); }
.progress span { display: block; width: 40%; height: 100%; background: var(--accent); animation: slide 1.1s linear infinite; }
@keyframes slide { from { transform: translateX(-100%); } to { transform: translateX(350%); } }
@media (prefers-reduced-motion: reduce) { .progress span { width: 100%; animation: none; } }

input, select, textarea, button {
  font: inherit; color: inherit; border: 1px solid var(--line); border-radius: 9px;
  background: var(--card); padding: 8px 10px; min-width: 0;
}
/* Never below 16px: iOS zooms the page into any field smaller than that, and the zoom is
   what then lets the page be panned sideways. The label above a field stays small. */
input, select, textarea { font-size: 16px; line-height: 1.25; }
/* Every button is at least a finger wide and a finger tall. The old ones were 39 px, and
   the settings gear was 31 px across, which is a miss waiting to happen (A-3). */
button {
  cursor: pointer; background: var(--bg); white-space: nowrap;
  min-height: var(--touch); min-width: var(--touch);
  display: inline-flex; align-items: center; justify-content: center;
}
button.primary { background: var(--accent); color: var(--accent-ink); border-color: transparent; font-weight: 600; }
button.ghost { border-color: transparent; background: transparent; color: var(--muted); }
button:disabled, input:disabled, select:disabled { opacity: .55; cursor: default; }
/* A locked value is one a person is being asked to read and confirm, so it keeps full-
   strength ink and says it is locked with a tint instead (A-4). */
input[readonly], textarea[readonly] {
  background: color-mix(in srgb, var(--muted) 14%, var(--card));
  color: var(--ink); opacity: 1; cursor: default;
}
.search { flex: 1; }

main { flex: 1; padding: 10px var(--gutter) 0; }
.view { max-width: 900px; margin-inline: auto; }

.list-tools { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
.list-tools .count { margin-left: auto; font-size: 12px; color: var(--muted); }
.chips { display: flex; gap: 6px; flex-wrap: wrap; }
.chip { font-size: 12px; background: var(--card); border: 1px solid var(--line); border-radius: 999px; padding: 0 14px; }
.menu-item { display: flex; width: 100%; justify-content: flex-start; margin-bottom: 6px; }

.list-inset {
  background: var(--card); border: 1px solid var(--line); border-radius: 12px;
  overflow-x: auto; overflow-y: visible; -webkit-overflow-scrolling: touch;
}
table.list { border-collapse: separate; border-spacing: 0; width: max-content; min-width: 100%; }
.list th, .list td {
  text-align: left; padding: 9px 12px; border-bottom: 1px solid var(--line);
  white-space: nowrap; font-size: 14px; background: var(--card);
}
.list th {
  font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted);
  position: sticky; top: 0; cursor: pointer; user-select: none;
}
.list th .sort-marker { font-size: 10px; margin-left: 3px; color: var(--accent); }
.list tbody tr { cursor: pointer; }
.list tbody tr:last-child td { border-bottom: 0; }
.list .col-id { font-family: var(--mono); }
.list .col-stockLevel { text-align: right; font-variant-numeric: tabular-nums; }
.list td.col-stockLevel.low { color: var(--warn); font-weight: 600; }
.list td.col-stockLevel.empty { color: var(--warn); }
/* why a row is in the results when no column shows it */
.list .why { font-size: 11px; color: var(--muted); font-family: var(--mono); }

/* Scrolled sideways, footprint and value hold the left edge, the second sitting where the
   first ends. They are cut short while pinned so the pair stays narrow — the point is to
   know which row you are on, not to read the whole value (row 68). A real border divides
   them from what slides underneath; the one-pixel shadow it replaces left a sliver of the
   hidden column showing beside the header (D-12). */
.list-inset.pinned .col-footprint { position: sticky; left: 0; }
.list-inset.pinned .col-value {
  position: sticky; left: var(--pinned-offset, 0px); border-right: 1px solid var(--line);
}
.list-inset.pinned td.col-footprint, .list-inset.pinned td.col-value { z-index: 2; }
.list-inset.pinned th.col-footprint, .list-inset.pinned th.col-value { z-index: 3; }
.list-inset.pinned .col-footprint,
.list-inset.pinned .col-value { max-width: 5.5rem; overflow: hidden; text-overflow: ellipsis; }

.paging { display: flex; gap: 8px; align-items: center; justify-content: center; padding: 10px 0 4px; }
.paging .page-info { font-size: 12px; color: var(--muted); }

.foot { text-align: center; font-size: 11px; color: var(--muted); padding: 14px 0 calc(28px + env(safe-area-inset-bottom)); }

dialog {
  border: 1px solid var(--line); border-radius: 14px; padding: 0; background: var(--card); color: var(--ink);
  width: min(560px, 92vw); max-width: 92vw; max-height: 86svh;
  overflow-y: auto; overflow-x: hidden;   /* a form has no business scrolling sideways */
}
/* On a phone a dialogue is a sheet on the bottom edge: that is where the thumb is, and a
   card floating in the middle puts Save out of reach on a tall screen (A-6). */
@media (max-width: 599px) {
  dialog {
    position: fixed; inset: auto 0 0 0; margin: 0;
    width: auto; max-width: none; max-height: 92svh;
    border-radius: 14px 14px 0 0; border-bottom: 0;
  }
}
dialog * { max-width: 100%; }
dialog:focus, .sheet:focus { outline: none; }
dialog::backdrop { background: rgba(0,0,0,.45); }
.sheet { padding: 16px 16px calc(16px + env(safe-area-inset-bottom)); }
/* A dialogue's title is a title. It was inheriting the small grey upper-case style the page
   uses for section labels, which made every sheet look unnamed (A-5). */
.sheet h2 {
  margin: 0 0 12px; font-size: 17px; font-weight: 600;
  text-transform: none; letter-spacing: normal; color: var(--ink);
}
.sheet h3 { margin: 16px 0 6px; font-size: 12px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
.grid { display: flex; flex-direction: column; gap: 8px; }
label { display: block; font-size: 12px; color: var(--muted); }
label input, label select { width: 100%; margin-top: 3px; color: var(--ink); }
label { min-width: 0; }
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.row > * { min-width: 0; max-width: 100%; }
/* …but not a button: the rule above is there to let text shrink, and it was quietly
   undoing the 44 px target on everything laid out in a row. */
.row > button { min-width: var(--touch); }
/* Cancel, Save and Save-and-print used to sit under 1,396 px of form in a 696 px dialogue
   (U-26). They stay on the bottom edge instead, where the thumb already is. */
.actions {
  display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; flex-wrap: wrap;
  position: sticky; bottom: calc(-1 * env(safe-area-inset-bottom));
  background: var(--card); padding: 8px 0 calc(8px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--line); z-index: 4;
}
.pill { font-size: 11px; color: var(--muted); border: 1px solid var(--line); border-radius: 999px; padding: 2px 8px; }

/* A part that has been chosen is one row: the person came here to add a reel, and what they
   need from the part is confirmation that it is the right one (U-25). */
.part-summary {
  display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
  background: var(--bg); border: 1px solid var(--line); border-radius: 10px;
  padding: 8px 10px; margin-bottom: 8px;
}
.part-summary .grow { flex: 1; font-family: var(--mono); font-size: 13px; min-width: 100%; }
.restore-row {
  display: flex; gap: 8px; align-items: center; flex-wrap: wrap; font-size: 13px;
  background: var(--notice-bg); border: 1px solid var(--notice-line); border-radius: 10px;
  padding: 8px 10px; margin-bottom: 12px;
}
.restore-row .grow { flex: 1; min-width: 100%; }
details > summary {
  min-height: var(--touch); display: flex; align-items: center;
  cursor: pointer; font-size: 13px; color: var(--muted);
}
.note { font-size: 12px; color: var(--muted); }
.warn { background: var(--warn-bg); color: var(--warn); border-radius: 8px; padding: 8px 10px; font-size: 13px; }

/* The match list belongs under the field being typed in, overlaying what is below rather
   than pushing it down — it used to appear above and shift the field mid-word (U-27). */
.grid > label, .grid > div > label { position: relative; }
.matches {
  border: 1px solid var(--line); border-radius: 10px; max-height: 170px; overflow-y: auto;
  background: var(--card); position: absolute; left: 0; right: 0; top: 100%; z-index: 5;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .18);
}
/* A match is a real button, so a screen reader announces it as something to activate rather
   than reading out two spans of text (A-1). */
.match {
  width: 100%; padding: 8px 10px; border: 0; border-bottom: 1px solid var(--line); border-radius: 0;
  background: var(--card); display: flex; gap: 10px; align-items: baseline; justify-content: flex-start;
}
.match:last-child { border-bottom: 0; }
.match .mid { font-family: var(--mono); font-weight: 600; }
.match .meta { font-size: 12px; color: var(--muted); }

/* A reel holds thousands, so the steps are hundreds, tens and ones rather than one at a
   time. Six of them across the narrowest phone is what sets the sizes here: 44 px each and
   a 3 px gap fit inside 320 px with a little to spare (U-20). */
.steppers {
  display: grid; grid-template-columns: repeat(6, minmax(var(--touch), 1fr));
  gap: 3px; margin: 4px 0 10px;
}
.steppers button { padding: 4px 2px; font-size: 13px; font-variant-numeric: tabular-nums; }
.field-label { display: block; font-size: 12px; color: var(--muted); margin-top: 8px; }
.change-row { flex-wrap: nowrap; }
.change-row .change-field { width: 6rem; flex: 0 0 auto; }
.change-row .field-label { margin: 0 0 0 auto; }
.change-row output {
  font-family: var(--mono); font-size: 18px; font-weight: 600;
  min-width: 5ch; text-align: right; flex: 0 0 auto;
}
.fields { display: grid; grid-template-columns: max-content 1fr; gap: 4px 14px; margin: 0 0 16px; }
.fields dt { color: var(--muted); font-size: 12px; }
.fields dd { margin: 0; font-size: 14px; }
.reel-card { border: 1px solid var(--line); border-radius: 10px; margin-bottom: 8px; background: var(--card); }
.reel-open {
  display: flex; flex-direction: column; gap: 2px; padding: 10px 12px;
  color: inherit; text-decoration: none; min-height: var(--touch); justify-content: center;
}
.reel-card .rid { font-family: var(--mono); font-weight: 600; }
.reel-card .meta { font-size: 12px; color: var(--muted); }
/* the reel a search landed on, so it does not have to be hunted for among twenty cards */
.reel-card.found { outline: 2px solid var(--accent); outline-offset: 2px; }
.reel-expand {
  width: 100%; border: 0; border-top: 1px solid var(--line); border-radius: 0 0 10px 10px;
  background: transparent; color: var(--muted); font-size: 12px; gap: 6px; justify-content: flex-start;
  padding: 0 12px;
}
.reel-expand .chevron { font-size: 14px; }
.reel-detail { padding: 2px 12px 12px; border-top: 1px solid var(--line); }
.reel-detail h4 { margin: 12px 0 4px; font-size: 12px; color: var(--muted); font-weight: 600; }
.reel-detail .fields { margin-bottom: 0; }
.reel-detail .log { margin: 0; padding-left: 18px; font-size: 12px; }
.reel-detail .log li { margin-bottom: 2px; font-variant-numeric: tabular-nums; }
.reel-photo { display: block; width: 120px; height: 90px; object-fit: cover; border-radius: 8px; margin-top: 10px; cursor: pointer; }
.retired-actions { padding: 0 12px 12px; }
.part-totals { margin: 0 0 10px; font-size: 13px; color: var(--muted); font-variant-numeric: tabular-nums; }
.show-retired { justify-content: flex-start; padding-left: 0; font-size: 12px; }

.filter-group { border-bottom: 1px solid var(--line); }
.filter-group > summary {
  display: flex; align-items: center; min-height: var(--touch);
  cursor: pointer; font-weight: 600; font-size: 14px;
}
.filter-row {
  display: flex; gap: 10px; align-items: center; min-height: var(--touch);
  font-size: 14px; color: var(--ink); padding: 0 2px;
}
.filter-row input[type="checkbox"] { width: 22px; height: 22px; flex: 0 0 auto; margin: 0; }
.filter-row .grow { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.filter-row .count { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }

.toast {
  position: fixed; left: var(--gutter); right: var(--gutter); bottom: calc(18px + env(safe-area-inset-bottom));
  background: var(--ink); color: var(--bg); padding: 8px 8px 8px 14px; border-radius: 22px; font-size: 13px;
  margin: 0; z-index: 9; display: flex; gap: 8px; align-items: center; justify-content: center;
}
.toast.ok { background: var(--ok); color: var(--ok-ink); }
.toast .toast-action {
  background: transparent; border-color: currentColor; color: inherit;
  font-size: 13px; font-weight: 600; padding: 0 12px; min-height: 36px;
}

/* A sentence the store wants read is not a failure. It used to borrow the error box, which
   made a recovered write look like a broken one. */
/* Above the toast, not over it: a recovered write puts both on screen at once, and the
   toast is the one carrying Undo. */
.notice-box {
  position: fixed; left: var(--gutter); right: var(--gutter); bottom: calc(76px + env(safe-area-inset-bottom));
  background: var(--notice-bg); color: var(--ink); border: 1px solid var(--notice-line);
  border-radius: 12px; padding: 10px 12px; font-size: 13px; z-index: 10;
  display: flex; flex-direction: column; gap: 6px; max-height: 60svh; overflow-y: auto;
}
.notice-box p { margin: 0; }
.notice-box button { align-self: flex-end; }

/* An error waits to be read: the person is often holding a reel, and five seconds is not
   long enough to read anything while doing that. */
.error-box {
  position: fixed; left: var(--gutter); right: var(--gutter); bottom: calc(18px + env(safe-area-inset-bottom));
  background: var(--warn-bg); color: var(--warn); border: 1px solid var(--warn);
  border-radius: 12px; padding: 10px 12px; font-size: 13px; z-index: 10;
  display: flex; flex-direction: column; gap: 6px; max-height: 60svh; overflow-y: auto;
}
.error-box p { margin: 0; }
.error-box summary { font-size: 12px; cursor: pointer; }
.error-box [data-testid="error-detail"] { font-family: var(--mono); font-size: 11px; word-break: break-word; }
.error-box button { align-self: flex-end; }
.warn-text { color: var(--warn); font-weight: 600; }
.ok-text { color: var(--ok); font-weight: 600; }
h1 { font-size: 20px; margin: 6px 0 2px; font-family: var(--mono); }
h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); margin: 18px 0 8px; }
.back { padding-left: 0; justify-content: flex-start; }

/* The identifier cell holds the address the row opens, so the row is announced as something
   that goes somewhere and can be opened in its own tab (A-1). */
.list .col-id a { color: inherit; text-decoration: none; }

/* A token field that reads as masked without being a password field: browsers offer to
   save passwords whenever they see type="password", which is not what this is. */
.masked { -webkit-text-security: disc; text-security: disc; font-family: var(--mono); }

.print-group { border-bottom: 1px solid var(--line); margin-bottom: 8px; }
.print-group > summary { font-weight: 600; font-size: 14px; color: var(--ink); }
.print-group .queue-set { width: 100%; margin-bottom: 10px; }
.label-option { border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; margin-bottom: 10px; }
/* One line: wrapping would put Queue on a line of its own, and the name is the only thing
   here that can give way, which it does by being cut short. */
.label-option .head { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; flex-wrap: nowrap; }
.label-option .head input[type="checkbox"] { width: 22px; height: 22px; flex: 0 0 auto; margin: 0; }
.label-option .head select { width: auto; flex: 0 0 auto; }
/* The name gives way rather than pushing Queue onto a line of its own. */
.label-option .name {
  font-weight: 600; text-transform: capitalize;
  flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.label-option .size { font-size: 12px; color: var(--muted); font-family: var(--mono); flex: 0 0 auto; }
button.queued { background: var(--ok); color: var(--ok-ink); border-color: transparent; }

.queue-row { border-bottom: 1px solid var(--line); padding: 8px 0; font-size: 13px; }
.queue-row .head { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.queue-row .what { font-family: var(--mono); }
.queue-row .when { font-size: 11px; color: var(--muted); font-family: var(--mono); }
.queue-row .state { margin-left: auto; font-size: 11px; text-transform: uppercase; letter-spacing: .05em; }
.queue-row .state-printed { color: var(--ok); font-weight: 600; }
.queue-row .state-failed { color: var(--warn); font-weight: 600; }
.queue-row .state-abandoned { color: var(--muted); }
.queue-row .detail { margin: 4px 0 0; font-size: 12px; color: var(--warn); }
.label-strip { overflow-x: auto; background: #ffffff; border: 1px solid var(--line); border-radius: 6px; padding: 6px; }
.label-strip canvas { display: block; height: 58px; width: auto; image-rendering: pixelated; }
.label-option .warn { margin-top: 8px; }

.photo-status {
  display: flex; gap: 10px; align-items: center; margin-bottom: 12px; font-size: 13px;
  background: var(--bg); border: 1px solid var(--line); border-radius: 10px; padding: 9px 11px;
}
.photo-status.done .spinner { display: none; }
.photo-status .grow { flex: 1; }
.photo-status .thumbnail { width: 44px; height: 44px; object-fit: cover; border-radius: 8px; cursor: pointer; }
.full-photo { display: block; width: 100%; height: auto; border-radius: 8px; }

/* A field the photograph filled, until the person edits it. */
label.from-label input { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 7%, var(--card)); }
.from-label-note { display: block; font-size: 11px; color: var(--accent); margin-top: 2px; }
.subtitle { margin: -6px 0 12px; font-size: 13px; color: var(--muted); }
.spinner {
  width: 15px; height: 15px; flex: 0 0 auto; border-radius: 50%;
  border: 2px solid var(--line); border-top-color: var(--accent); animation: spin 0.9s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinner { animation-duration: 3s; } }

/* The count of changes this phone is holding, and the line saying the inventory on screen is
   the one it last saw. Both live in the header because both are true of the whole page. */
.top .kept { font-size: 12px; font-family: var(--mono); }
.banner {
  margin: 8px 0 0; padding: 7px 10px; border-radius: 8px; font-size: 12px;
  background: var(--notice-bg); color: var(--ink); border: 1px solid var(--notice-line);
}
.kept-row { border-bottom: 1px solid var(--line); padding: 8px 0; font-size: 13px; }
.kept-row .head { display: flex; gap: 8px; align-items: baseline; }
.kept-row .what { font-family: var(--mono); flex: 1; min-width: 0; overflow-wrap: anywhere; }
.kept-row .state { font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
.kept-row .state-refused { color: var(--warn); font-weight: 600; }
.kept-row .detail { margin: 4px 0 0; font-size: 12px; color: var(--warn); }

/* The scanner is the one sheet that wants the whole screen: the viewfinder is the thing
   being aimed, and a strip of it is not enough to aim with. */
@media (max-width: 599px) {
  dialog.scanner { max-height: 100svh; height: 100svh; border-radius: 0; }
}
dialog.scanner .sheet { display: flex; flex-direction: column; height: 100%; }
.viewfinder {
  flex: 1; min-height: 240px; background: #000; border-radius: 10px; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
.viewfinder video { width: 100%; height: 100%; object-fit: cover; }
