/*
 * One stylesheet for every screen.
 *
 * Each page used to carry its own copy of the palette, tabs and buttons —
 * seven templates, seven copies, so a colour change meant seven edits and
 * dark mode meant seven more. Everything below is shared; a page only adds
 * what's genuinely unique to it.
 *
 * Colours all come from tokens, and every token is declared in the bare
 * :root block before the dark overrides, so nothing renders one theme's
 * text on the other theme's background.
 */

:root {
  --ground:      #f1f0ed;
  --surface:     #ffffff;
  --surface-2:   #f7f6f3;
  --ink:         #1d1c1a;
  --ink-soft:    #6a675f;
  --ink-faint:   #97938a;
  --line:        #e4e2dc;
  --line-strong: #cfccc3;

  --accent:      #175d4f;
  --accent-deep: #0f4439;
  --accent-soft: #e4efeb;
  --on-accent:   #ffffff;

  --good:        #17805f;
  --good-soft:   #e2f2ea;
  --warn:        #b06a00;
  --warn-soft:   #fdf0dc;
  --handed:      #e6b422;   /* yellow: handed out, not packed yet */
  --size:        #5d48b5;   /* violet: a size within a grouped product — used nowhere else */
  --size-soft:   #e6e0fa;
  --on-size:     #ffffff;
  --bad:         #b23b2c;
  --bad-soft:    #fceae6;
  --alt:         #4a5a8f;
  --alt-soft:    #eaedf7;

  --radius:      12px;
  --radius-sm:   8px;
  --shadow:      0 1px 2px rgba(20,20,43,.04), 0 18px 40px -22px rgba(20,20,43,.22);

  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, monospace;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ground:      #16181a;
    --surface:     #1f2124;
    --surface-2:   #26292c;
    --ink:         #eceae6;
    --ink-soft:    #a8a49c;
    --ink-faint:   #7d7972;
    --line:        #313438;
    --line-strong: #43474c;

    --accent:      #4cb79c;
    --accent-deep: #6ccdb3;
    --accent-soft: #1b2b28;
    --on-accent:   #0d1614;

    --good:        #4cbd91;
    --good-soft:   #162b23;
    --warn:        #e0a353;
    --warn-soft:   #2e2416;
    --handed:      #d8b243;
    --size:        #b9aaf3;
    --size-soft:   #2e2750;
    --on-size:     #140f26;
    --bad:         #ef8b78;
    --bad-soft:    #331e1b;
    --alt:         #8fa0d8;
    --alt-soft:    #1e2334;

    --shadow:      0 1px 2px rgba(0,0,0,.3), 0 18px 40px -22px rgba(0,0,0,.6);
  }
}

:root[data-theme="dark"] {
  --ground:      #16181a;
  --surface:     #1f2124;
  --surface-2:   #26292c;
  --ink:         #eceae6;
  --ink-soft:    #a8a49c;
  --ink-faint:   #7d7972;
  --line:        #313438;
  --line-strong: #43474c;

  --accent:      #4cb79c;
  --accent-deep: #6ccdb3;
  --accent-soft: #1b2b28;
  --on-accent:   #0d1614;

  --good:        #4cbd91;
  --good-soft:   #162b23;
  --warn:        #e0a353;
  --warn-soft:   #2e2416;
  --handed:      #d8b243;
  --size:        #b9aaf3;
  --size-soft:   #2e2750;
  --on-size:     #140f26;
  --bad:         #ef8b78;
  --bad-soft:    #331e1b;
  --alt:         #8fa0d8;
  --alt-soft:    #1e2334;

  --shadow:      0 1px 2px rgba(0,0,0,.3), 0 18px 40px -22px rgba(0,0,0,.6);
}

* { box-sizing: border-box; }

body {
  font-family: var(--font);
  background: var(--ground);
  color: var(--ink);
  margin: 0;
  min-height: 100vh;
  font-size: 15px;
  line-height: 1.55;
  -webkit-text-size-adjust: 100%;
}

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

.shell { max-width: 560px; margin: 0 auto; padding: 32px 18px 56px; }
.shell.wide { max-width: 1180px; }
.shell.narrow { max-width: 720px; }

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 28px;
}

.stack { display: flex; flex-direction: column; gap: 16px; }
.stack.tight { gap: 10px; }
.row { display: flex; gap: 10px; align-items: center; }
.row.between { justify-content: space-between; }
.row.wrap { flex-wrap: wrap; }
.grow { flex: 1; min-width: 0; }

/* Without this, [hidden] loses to any display-setting class (.stack,
   .row, ...) that shares its specificity and happens to be declared
   later in the cascade — the element stays visibly on screen even
   though its hidden attribute is set and JS thinks it's toggled off. */
[hidden] { display: none !important; }

/* ------------------------------------------------------------- typography */

h1 { font-size: 22px; letter-spacing: -.02em; margin: 0; }
h2 { font-size: 16px; letter-spacing: -.01em; margin: 0; }
.sub { color: var(--ink-soft); font-size: 14px; margin: 0; }
.label {
  font-size: 11.5px; font-weight: 700; letter-spacing: .07em;
  text-transform: uppercase; color: var(--ink-faint);
}
.num { font-variant-numeric: tabular-nums; }
.mono { font-family: var(--mono); }
.muted { color: var(--ink-soft); }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ------------------------------------------------------------- nav ----- */

.tabs {
  display: flex; gap: 4px; margin-bottom: 22px;
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 10px; padding: 4px;
}
.tabs a {
  flex: 1; text-align: center; padding: 8px 6px; border-radius: 7px;
  font-size: 12.5px; font-weight: 600; color: var(--ink-soft); text-decoration: none;
}
.tabs a.on { background: var(--surface); color: var(--ink); box-shadow: 0 1px 2px rgba(20,20,43,.08); }
.tabs a:hover:not(.on) { color: var(--ink); }

/* ------------------------------------------------------------- controls  */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 12px 18px; border: 1px solid transparent; border-radius: var(--radius-sm);
  background: var(--accent); color: var(--on-accent);
  font: inherit; font-weight: 700; cursor: pointer; text-decoration: none;
  transition: transform .08s ease, filter .15s ease;
  -webkit-tap-highlight-color: transparent;
}
.btn:hover { filter: brightness(1.08); }
/* A packer on a warehouse floor is tapping a touchscreen, not hovering a
   mouse — :hover's brightness bump often never fires for them, so the
   press itself needs its own real, visible change rather than relying on
   the 1px shift alone. brightness(.85) darkens whatever the button's
   current fill is, which reads clearly against both themes since it's
   relative to the button's own color, not the page behind it. */
.btn:active { filter: brightness(.85); transform: translateY(1px); }
.btn:disabled, .btn.off { opacity: .45; pointer-events: none; }
.btn.wide { width: 100%; }
.btn.big { padding: 16px; font-size: 16px; }
.btn.sm { padding: 7px 12px; font-size: 13px; }
.btn.quiet { background: var(--accent-soft); color: var(--accent-deep); }
.btn.plain { background: var(--surface); color: var(--ink); border-color: var(--line-strong); }
.btn.good { background: var(--good); color: #fff; }
.btn.warn { background: var(--warn); color: #fff; }
.btn.bad  { background: var(--bad);  color: #fff; }
.btn.alt  { background: var(--alt);  color: #fff; }

/* The exception buttons sit next to the one people press all day. Filling
   all four makes the common action no easier to find than the rare ones,
   so these stay outlines and only pick up their colour on press. */
.btn.ghost {
  background: transparent; color: var(--ink-soft);
  border-color: var(--line-strong); font-weight: 600;
}
.btn.ghost:hover { background: var(--surface-2); color: var(--ink); }
.btn.ghost.on { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
.btn.ghost.warn-on:hover { border-color: var(--warn); color: var(--warn); }
.btn.ghost.bad-on:hover  { border-color: var(--bad);  color: var(--bad); }
.btn.ghost.alt-on:hover  { border-color: var(--alt);  color: var(--alt); }

input[type=text], input[type=password], input[type=number], select, textarea {
  width: 100%; padding: 12px 13px; font: inherit;
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink);
}
textarea { resize: vertical; min-height: 4.5em; }
input:focus-visible, select:focus-visible, .btn:focus-visible, a:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
input.pin {
  text-align: center; letter-spacing: .5em; font-size: 24px;
  font-weight: 700; font-family: var(--mono);
}

/* ------------------------------------------------------------- pieces -- */

.pill {
  font-size: 10.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  padding: 3px 8px; border-radius: 999px; white-space: nowrap;
}
.pill.good { background: var(--good-soft); color: var(--good); }
.pill.warn { background: var(--warn-soft); color: var(--warn); }
.pill.bad  { background: var(--bad-soft);  color: var(--bad); }
.pill.alt  { background: var(--alt-soft);  color: var(--alt); }
.pill.flat { background: var(--surface-2); color: var(--ink-faint); border: 1px solid var(--line); }

.notice {
  padding: 12px 14px; border-radius: var(--radius-sm); font-size: 13.5px;
  border: 1px solid var(--line); background: var(--surface-2); color: var(--ink-soft);
}
.notice.warn { background: var(--warn-soft); border-color: var(--warn); color: var(--warn); }
.notice.bad  { background: var(--bad-soft);  border-color: var(--bad);  color: var(--bad); }
.notice.good { background: var(--good-soft); border-color: var(--good); color: var(--good); }

.bar { height: 8px; border-radius: 999px; background: var(--surface-2); overflow: hidden; }
.bar { display: flex; }
.bar > i { display: block; height: 100%; background: var(--good); flex: none; }
.bar > i.handed { background: var(--handed); }
.bar > i.short { background: var(--bad); }   /* out of stock, can't find, or waiting on one */

.tile-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 1px;
  background: var(--line); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.tile { background: var(--surface); padding: 14px 16px; }
.tile .label { display: block; margin-bottom: 2px; }
.tile b { font-size: 21px; font-weight: 700; }

.list { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--surface); }
.list > * + * { border-top: 1px solid var(--line); }
.item { padding: 14px 16px; display: flex; flex-direction: column; gap: 10px;
         box-shadow: inset 4px 0 0 transparent; transition: box-shadow .15s ease, background .15s ease; }

/* Each outcome gets its own edge colour and tint, so a glance down the
   list tells you what happened without reading any of it. */
.item[data-status="packed"]       { box-shadow: inset 4px 0 0 var(--good); background: var(--good-soft); }
.item[data-status="out_of_stock"] { box-shadow: inset 4px 0 0 var(--warn); background: var(--warn-soft); }
.item[data-status="not_found"]    { box-shadow: inset 4px 0 0 var(--alt);  background: var(--alt-soft); }
.item[data-status="damaged"]      { box-shadow: inset 4px 0 0 var(--bad);  background: var(--bad-soft); }

/* connection state — has to be impossible to miss, because the dangerous
   failure is taps that look saved and aren't */
.conn {
  position: sticky; top: 0; z-index: 20; padding: 9px 14px;
  font-size: 13px; font-weight: 600; text-align: center;
  background: var(--bad); color: #fff;
}
.conn[hidden] { display: none !important; }

/* Who you are and how you're doing stays reachable without eating screen
   the whole time you're scrolling down a long list — it slides out of the
   way going down and snaps right back the moment you scroll up, which is
   also exactly when you'd want to check it (you're heading back to the
   top, or just paused). Layout space is untouched either way; only the
   header's own position moves. */
.pack-header {
  position: sticky; top: 0; z-index: 15;
  background: var(--ground);
  padding: 14px 0 12px;
  transform: translateY(0);
  transition: transform .2s ease;
}
.pack-header.hide { transform: translateY(-100%); }

/* Which product group you're actually in right now — sticks to the top
   the same way .pack-header does, but at a lower z-index: while
   .pack-header is showing (scrolled up, near the top) it sits on top of
   this and wins outright; the moment it tucks away on a downward scroll
   is exactly when this needs to take over the strip, so the two never
   have to coordinate an offset between them. Bigger and bolder than a
   normal h2 on purpose — the point is that crossing into a new group
   reads as a real change, not a label you'd have to stop and read. */
.group-header {
  position: sticky; top: 0; z-index: 12;
  background: var(--ground);
  padding: 10px 14px; margin: 0 -14px 4px;
  border-bottom: 2px solid var(--line);
}
.group-header h2 { font-size: 21px; font-weight: 800; letter-spacing: -.01em; }

/* A resolved box shrinks down to just a name and an Undo — the list
   stays scannable as things get done instead of staying full-height for
   every box all day. Damaged never reaches this: it doesn't touch
   status at all (see the click handler), so that box stays full-size
   and in the way on purpose until the reprint actually happens. */
.item .collapsed-summary { display: none; }
.item.collapsed > .swipe-track { display: none; }
.item.collapsed .collapsed-summary {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  cursor: pointer;
}
/* Tapping the collapsed bar re-opens the full card in place — to check a
   note, re-swipe to the product info, whatever — without touching its
   status. Undo is a separate, deliberate button inside that same bar; a
   tap anywhere else on it just toggles this. */
.item.collapsed.expanded > .swipe-track { display: flex; }
.collapsed-summary .chevron { display: inline-block; transition: transform .15s ease; color: var(--ink-faint); }
.item.expanded .collapsed-summary .chevron { transform: rotate(180deg); }

/* Waiting on a reprint, there's exactly one thing to do — nothing here
   ships until a new label exists, so Packed/No stock/Can't find would
   all be a wrong answer to tap right now. Damaged label takes over
   Packed's spot as the one live action (a second tap is a harmless
   no-op — see flag_damaged_label), and Undo — the only way out of this
   state — stays directly under it instead of buried in a row of
   buttons that no longer apply. */
.item.reprint-open .pack-cta { display: none; }
.item.reprint-open .exceptions { grid-template-columns: 1fr; }
.item.reprint-open .exceptions [data-mark="out_of_stock"],
.item.reprint-open .exceptions [data-mark="not_found"] { display: none; }
.item.reprint-open .exceptions [data-mark="damaged"] { padding: 16px; font-size: 16px; }

/* Fixed in the corner rather than in the flow, so it's reachable after
   any amount of scrolling without hunting for it — a long list is
   exactly when jumping ahead (or back to a label that still needs
   reprinting) actually saves a trip. */
.jumpbar {
  position: fixed; right: 16px; bottom: 16px; z-index: 25;
  display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
}
.jumpbar .btn { box-shadow: var(--shadow); }

@media (max-width: 560px) {
  .shell { padding: 18px 12px 48px; }
  .card { padding: 20px 18px; }
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* Saved-ness is a separate question from outcome — a row can be packed and
   still unsaved — so it gets its own channel rather than another colour
   competing with the status edge. */
.item.waiting { position: relative; outline: 2px dashed var(--ink-faint); outline-offset: -2px; }
.item.waiting::after {
  content: "not saved yet";
  position: absolute; top: 6px; right: 10px;
  font-size: 9.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-faint);
}

/* Three secondary actions across a phone — an even grid that doesn't wrap
   each label onto two lines the way three flexed buttons did. */
.exceptions { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.exceptions .btn { padding: 11px 4px; font-size: 12.5px; white-space: nowrap; }
.exceptions .btn.span { grid-column: 1 / -1; font-size: 13px; }
@media (max-width: 400px) { .exceptions .btn { font-size: 11.5px; letter-spacing: -.01em; } }

/* ---- app bar ------------------------------------------------------- */
/*
 * The nav bar every admin/tool page shares. Sticky, because several of
 * these pages (the buy list, tracking, the archive) run long, and losing
 * the way back to Today by scrolling past it is the kind of thing that
 * makes an app feel like a stack of documents instead of a tool.
 *
 * Everything that isn't reached for constantly — eight destinations that
 * used to sit in the bar as bare text — collapses into the "Floor" menu.
 * A bar that names four things and groups the rest reads as considered;
 * one that names all twelve reads as a list that grew.
 */
/* The bar itself always spans the full page — its width is independent of
   whichever .shell size the page below it happens to use. A page that
   picks a narrow content column (a single-task form) or a wide one (a
   dashboard) never changes how much room the nav gets, which is what
   made Floor's items get squeezed out on narrow pages before. Only
   .appbar-inner is capped, and centered, so the bar's content lines up
   the same way on every page regardless of that page's own width. */
.appbar {
  position: sticky; top: 0; z-index: 30;
  margin: 0 0 22px;
  background: color-mix(in srgb, var(--ground) 92%, transparent);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--line);
}
.appbar-inner {
  max-width: 1180px; margin: 0 auto; padding: 0 18px;
  display: flex; align-items: center; gap: 4px; height: 52px;
}
@media (max-width: 620px) { .appbar-inner { padding: 0 12px; } .appbar { margin-bottom: 18px; } }

.brand {
  display: flex; align-items: center; gap: 8px; text-decoration: none;
  color: var(--ink); font-weight: 700; font-size: 14.5px;
  padding: 6px 8px 6px 4px; border-radius: 8px; flex: none; max-width: 30%;
}
.brand:hover { background: var(--surface-2); }
.brand-mark {
  display: flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 7px; flex: none;
  background: var(--accent); color: var(--on-accent);
}
.brand-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.primarynav {
  display: flex; align-items: center; gap: 2px; margin-left: 6px;
  overflow-x: auto; scrollbar-width: none; min-width: 0;
  /* flex: 1 1 0%, not the default auto basis. A nested flex container
     that scrolls mis-sizes itself under an auto basis — some engines
     resolve "auto" for a scrollable flex item from its post-layout
     clientWidth rather than true content size, which settles short of
     where it should. A zero basis sidesteps that and lets it claim
     exactly the room between the brand and the account menu, using
     its own scroll only once a real narrow width forces it to. */
  flex: 1 1 0%;
}
/* Only when _navbar.html's own script has measured real overflow — see
   there. Applying this unconditionally would fade the last item's own
   edge even on the common case where everything already fits, which
   reads as a rendering glitch, not a hint. */
.primarynav.scrollable {
  mask-image: linear-gradient(to right, #000 calc(100% - 20px), transparent);
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 20px), transparent);
}
.primarynav::-webkit-scrollbar { display: none; }
.primarynav a, .navmenu-btn, .accountbtn {
  display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
  padding: 7px 11px; border-radius: 7px; text-decoration: none;
  font-size: 13.5px; font-weight: 600; color: var(--ink-soft);
  background: none; border: none; cursor: pointer; font-family: inherit;
  flex: none;   /* keep full label width — an overflowing bar SCROLLS, it doesn't clip text */
}
.appbar-inner > .navmenu { flex: none; }
.primarynav a:hover, .navmenu-btn:hover, .accountbtn:hover { background: var(--surface-2); color: var(--ink); }
.primarynav a.on, .navmenu-btn.on, .accountbtn.on { background: var(--accent); color: var(--on-accent); }

.appbar-right { display: flex; align-items: center; gap: 8px; margin-left: auto; flex: none; }
.accountbtn-name { max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* One pattern for both dropdowns: a button that toggles a `.open` class
   on its wrapper, and a panel positioned off that wrapper — see the small
   script in _navbar.html. Hidden by default via visibility+opacity rather
   than display:none, so the open/close can transition instead of jump. */
.navmenu { position: relative; }
.chev { transition: transform .15s ease; flex: none; }
.navmenu.open .chev { transform: rotate(180deg); }
.navmenu-panel {
  position: absolute; top: calc(100% + 6px); left: 0; min-width: 190px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-sm);
  box-shadow: var(--shadow); padding: 6px; display: flex; flex-direction: column; gap: 1px;
  opacity: 0; visibility: hidden; transform: translateY(-4px);
  transition: opacity .12s ease, transform .12s ease;
  z-index: 40;
}
.navmenu-panel-right { left: auto; right: 0; }
.navmenu.open .navmenu-panel { opacity: 1; visibility: visible; transform: translateY(0); }
.navmenu-panel a {
  display: block; padding: 8px 10px; border-radius: 6px; text-decoration: none;
  font-size: 13.5px; font-weight: 500; color: var(--ink);
}
.navmenu-panel a:hover { background: var(--surface-2); }
.navmenu-switch { display: block; width: 100%; text-align: left; background: none; border: 0;
  font: inherit; font-size: 13.5px; font-weight: 500; color: var(--ink); cursor: pointer; padding: 8px 10px; border-radius: 6px; }
.navmenu-switch:hover, .navmenu-switch:focus-visible { background: var(--surface-2); }
.navmenu-panel a.on { background: var(--accent-soft); color: var(--accent-deep); font-weight: 600; }
.navmenu-sep { height: 1px; background: var(--line); margin: 5px 2px; }

/* The QR itself is drawn as near-black modules on a transparent ground.
   On the light card surface that's fine, but on the dark-mode surface
   it's dark-on-dark and barely scans. invert() flips the near-black
   modules to near-white while leaving the transparent background alone
   (invert doesn't touch alpha), so it reads against the dark card
   without needing a separate image or a boxed-in white background that
   would look out of place on an otherwise dark page. Printing forces it
   back off — ink on paper is always light, regardless of what the
   screen looked like when "Print this" was clicked. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .qr-img { filter: invert(1); }
}
:root[data-theme="dark"] .qr-img { filter: invert(1); }
@media print { .qr-img { filter: none !important; } }
.navmenu-who {
  padding: 6px 10px 8px; font-size: 12px; color: var(--ink-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (max-width: 620px) {
  .brand-name { display: none; }                 /* the mark alone is enough once space is tight */
  .accountbtn-name { max-width: 84px; }
}

/* ---- file pickers, shared by every upload screen ---------------------- */
.dropzone {
  position: relative; display: flex; align-items: center; gap: 12px;
  border: 1.5px dashed var(--line-strong); border-radius: var(--radius);
  background: var(--surface-2); padding: 16px; cursor: pointer;
  transition: border-color .15s ease, background .15s ease;
}
.dropzone:hover, .dropzone.dragover { border-color: var(--accent); background: var(--accent-soft); }
.dropzone input[type=file] { position: absolute; inset: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer; }
.dz-icon {
  flex-shrink: 0; width: 36px; height: 36px; border-radius: 10px;
  background: var(--accent-soft); color: var(--accent);
  display: flex; align-items: center; justify-content: center;
}
.dz-text { min-width: 0; }
.dz-title { font-size: 14px; font-weight: 600; }
.dz-sub { font-size: 12.5px; color: var(--ink-soft); margin-top: 2px;
          overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.field-label { display: block; font-size: 12.5px; font-weight: 600;
               color: var(--ink-soft); margin-bottom: 6px; }
.divider {
  display: flex; align-items: center; gap: 12px; color: var(--ink-faint);
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em;
}
.divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.footnote { font-size: 12px; color: var(--ink-faint); line-height: 1.6;
            padding-top: 16px; border-top: 1px solid var(--line); }

/* ---- dashboard ------------------------------------------------------- */
.pagehead { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; flex-wrap: wrap; }
.pagehead h1 { font-size: 26px; }

/* Status you read, actions you click — two different jobs, so they don't
   get the same treatment or share a grid that leaves a hole at the end. */
.statline { display: flex; gap: 28px; flex-wrap: wrap; align-items: baseline; }
.stat b { display: block; font-size: 30px; font-weight: 700; letter-spacing: -.02em; line-height: 1.1; }
.stat span { font-size: 12px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: var(--ink-faint); }

.actionrow { display: flex; gap: 8px; flex-wrap: wrap; }
.chip {
  display: inline-flex; align-items: center; gap: 8px; text-decoration: none;
  padding: 9px 14px; border-radius: 999px; font-size: 13.5px; font-weight: 600;
  border: 1px solid var(--line-strong); background: var(--surface); color: var(--ink-soft);
}
.chip:hover { border-color: var(--ink-faint); color: var(--ink); }
.chip b { font-variant-numeric: tabular-nums; }
.chip.live { border-color: transparent; }
.chip.live.warn { background: var(--warn-soft); color: var(--warn); }
.chip.live.bad  { background: var(--bad-soft);  color: var(--bad); }
.chip.live.calm { background: var(--accent-soft); color: var(--accent); }

.batchcard { border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); overflow: hidden; }
.batchcard > header { padding: 18px 20px 14px; display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; align-items: flex-start; }
.batchcard > header h2 { font-size: 17px; }
.batchcard .bar { margin: 0 20px; }
.batchcard .barnote { padding: 8px 20px 16px; font-size: 13px; color: var(--ink-soft); }
.batchcard .list { border: 0; border-top: 1px solid var(--line); border-radius: 0; }

.quiet-section > h2 { font-size: 13px; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-faint); margin-bottom: 10px; }

/* A checkbox with an explanation under it. The label wraps both so the
   whole block is a click target, not just the 13px box. */
.checkrow {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--card);
  cursor: pointer;
}
.checkrow input[type="checkbox"] {
  width: 17px;
  height: 17px;
  margin: 1px 0 0;
  flex: none;
  accent-color: var(--accent);
  cursor: pointer;
}
.checkrow > span { display: flex; flex-direction: column; gap: 3px; }
.checkrow .sub { font-size: 12.5px; line-height: 1.5; }
.checkrow:hover { border-color: var(--line-strong, var(--line)); }

/* The "Get started" checklist on Today — a step you haven't done yet is a
   plain outlined circle and a live link; done fills the circle in and
   fades the label, so the eye goes straight to what's still open. */
.checklist-row {
  flex-direction: row;
  align-items: center;
  gap: 12px;
  text-decoration: none;
  color: inherit;
}
.checklist-row:hover { background: var(--surface-2); }
.checklist-row .check-dot {
  flex: none;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 2px solid var(--line-strong);
  position: relative;
}
.checklist-row.done .check-dot { border-color: var(--good); background: var(--good); }
.checklist-row.done .check-dot::after {
  content: '';
  position: absolute;
  left: 5px;
  top: 1px;
  width: 5px;
  height: 9px;
  border: solid var(--on-accent);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
.checklist-row.done b { color: var(--ink-soft); text-decoration: line-through; text-decoration-color: var(--line-strong); }

/* The one-time page tour (tour.js). The overlay is transparent — it only
   exists to swallow clicks on the rest of the page. The dimming itself is
   the spotlighted element's own box-shadow: a huge spread radius paints
   "everything outside my own box" in translucent black, which is what
   makes the target look cut out of the dark without any clip-path math. */
.tour-overlay { position: fixed; inset: 0; z-index: 9997; background: transparent; }
.tour-target {
  position: relative !important;
  z-index: 9999 !important;
  pointer-events: none !important;
  border-radius: var(--radius-sm);
  box-shadow: 0 0 0 4px var(--accent), 0 0 0 9999px rgba(10, 9, 8, .6) !important;
}
.tour-card {
  position: fixed;
  z-index: 10000;
  width: 300px;
  max-width: calc(100vw - 24px);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.tour-card .tour-step { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-faint); }
.tour-card .tour-title { margin: 2px 0 0; font-size: 16px; }
.tour-card .tour-text { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--ink-soft); }
.tour-card .tour-controls { display: flex; align-items: center; gap: 8px; margin-top: 6px; }

/* Destructive action. Reads as a warning without shouting — cancelling an
   order is routine, not an emergency, but it shouldn't sit next to Save
   looking identical. */
.btn.danger {
  background: var(--bad-soft);
  color: var(--bad);
  border-color: color-mix(in srgb, var(--bad) 32%, transparent);
}
.btn.danger:hover {
  background: var(--bad);
  color: #fff;
  border-color: var(--bad);
}

/* The catalog card on a packer's list: a photo, where it lives, and a way
   through to the listing. Sized for a phone held at arm's length in a
   warehouse, not for a desk — which means the photo is the point, big
   enough to actually check against what's on the shelf, not a decorative
   thumbnail next to the real content. Stacked (photo on top, details
   below) rather than side-by-side, so the photo can use the card's full
   width instead of being squeezed down to leave room for text beside it. */
.prodcard {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}
/* overflow-wrap:anywhere so one long unbroken run (a SKU, a pasted URL, a
   word with no spaces to break on) wraps instead of overflowing sideways
   and getting silently clipped by the card's overflow:hidden (see
   .item.swipeitem) — it would otherwise just look like the text vanished. */
.prodcard .grow { min-width: 0; overflow-wrap: anywhere; }
.prodcard img {
  width: 100%;
  max-height: 260px;
  /* contain, not cover — cropping in a photo meant for "does this match
     what I'm holding" risks cutting off exactly the label text or detail
     that would settle it. */
  object-fit: contain;
  border-radius: 8px;
  background: var(--line);
}
.prodcard .shelf {
  display: inline-block;
  font-weight: 700;
  font-size: 19px;
  letter-spacing: 0.5px;
  font-variant-numeric: tabular-nums;
  padding: 2px 9px;
  border-radius: 6px;
  background: var(--accent-soft, var(--good-soft));
  color: var(--accent, var(--good));
}
.prodcard a.sub { display: inline-block; margin-top: 2px; }

/* A box's own card, swiped sideways to reveal its catalog info instead of
   that sitting as a permanent block above the list — see pack_list.html
   for the touch handling. touch-action:pan-y is what lets the page keep
   scrolling vertically while a horizontal drag is captured as a swipe;
   a card with nothing set up for its product (no .has-info) just never
   gets the touch handler attached, so it behaves exactly as a plain
   .item always did. */
/* .item's own 14px/16px padding has to move onto the panes themselves
   (border-box, so it's included in their 50% rather than added on top)
   rather than staying on .item — left there, it sits outside the
   track's 200%/-50% math entirely, so a full pane-width shift lands
   short of the card's actual edge and leaves exactly that padding
   gutter still showing a sliver of whichever pane just slid out. */
.item.swipeitem { overflow: hidden; position: relative; padding: 0; }
.swipe-track { display: flex; width: 200%; touch-action: pan-y; }
.swipeitem:not(.has-info) .swipe-track { width: 100%; }
.swipe-pane {
  flex: 0 0 50%; width: 50%; min-width: 0; box-sizing: border-box;
  padding: 14px 16px; display: flex; flex-direction: column; gap: 10px;
}
.swipeitem:not(.has-info) .swipe-pane { flex: 0 0 100%; width: 100%; }
.swipe-pane.info-pane { justify-content: center; }

/* A hint that a card can be swiped for its photo/description — nothing
   in the pack-pane otherwise looks touchable that way. A real flex child
   in the header row (see pack_list.html), not an absolutely-positioned
   overlay: a fixed-position badge looked right on a tall card and sat
   straight on top of the button row on a short one, since card height
   varies with how much is in the exceptions row. Living in the row
   means it's always beside the status pill, never on top of anything.
   A plain white chip on purpose, not a theme color — it has to read as
   "grab this" against a pack-pane whose own background already IS the
   surface color in light mode, where a themed chip would all but
   disappear. */
.swipe-hint {
  flex: none;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, .3);
  position: relative;
}
.swipe-hint::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 6px;
  height: 6px;
  margin: -5px 0 0 -7px;
  border-top: 2px solid #444;
  border-right: 2px solid #444;
  transform: rotate(45deg);
}

/* A wide table that scrolls inside its own box. The page itself must never
   scroll sideways — a column-mapping sheet can be forty columns wide. */
.scroll-x { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); }
table.grid { border-collapse: collapse; width: max-content; min-width: 100%; font-size: 13px; }
table.grid th, table.grid td {
  padding: 8px 10px;
  border-right: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  text-align: left;
  vertical-align: top;
  max-width: 220px;
}
table.grid th { background: var(--surface); position: sticky; top: 0; }
table.grid th > div { font-weight: 600; margin-bottom: 6px; }
table.grid th select { width: 100%; font-size: 12.5px; padding: 5px 7px; }
table.grid td { color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
table.grid tr > *:last-child { border-right: none; }
table.grid tbody tr:last-child > * { border-bottom: none; }

/* The sign-in code, shown when there's no mail server to send it through.
   Big and spaced because it's read off the screen and typed in below it. */
.codebox {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 32px;
  font-weight: 700;
  letter-spacing: 8px;
  text-align: center;
  padding: 16px 12px 16px 20px;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--accent-soft);
  color: var(--accent-deep, var(--accent));
  user-select: all;
}

/* Status and reprint sit in a column together. The reprint badge is a
   separate fact from the status, so it must be able to show at the same
   time as "packed" or "no stock", not fight it for the same slot. */
.statecol { display: flex; flex-direction: column; gap: 4px; align-items: flex-end; flex: none; }

/* How many units go in this box.
   Shipping one of a pair is the expensive mistake on a packing floor, so a
   quantity above one is not a footnote — it gets weight, colour and its own
   space. A single unit stays quiet so the loud ones actually stand out. */
.qty {
  flex: none;
  min-width: 46px;
  padding: 6px 8px;
  border-radius: 10px;
  text-align: center;
  line-height: 1.1;
  border: 1px solid var(--line);
}
.qty b { display: block; font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; }
.qty span { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.4px; color: var(--ink-soft); }
.qty.one { background: transparent; }
.qty.one b { font-weight: 600; color: var(--ink-soft); }
.qty.many {
  background: var(--warn-soft);
  border-color: color-mix(in srgb, var(--warn) 40%, transparent);
}
.qty.many b { font-size: 24px; color: var(--warn); }
.qty.many span { color: var(--warn); font-weight: 700; }

/* A multi-product box: several different products sharing one label, so
   they get one set of buttons but still need to read as clearly separate
   things to grab — a rule between each line does that without repeating
   the whole item-card treatment (border, shadow, own buttons) per product,
   which would wrongly imply each one is its own box. */
.mp-products { display: flex; flex-direction: column; }
.mp-product-row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border-top: 1px solid var(--line);
  font-size: 14px;
}
.mp-product-row:first-child { border-top: 1px solid var(--line); margin-top: 2px; }
/* A product line an admin has individually confirmed (or that's still
   sitting under its box-mate's suspicion) gets its own shading — the
   same idea as .item[data-status] on a whole card, just scoped to one
   row so a box that isn't uniformly resolved doesn't look resolved. */
.mp-product-row[data-status="out_of_stock"] {
  background: var(--warn-soft); border-radius: var(--radius-sm);
  box-shadow: inset 3px 0 0 var(--warn);
}

/* ---- match listings wizard (admin, phone-first) ------------------------- */
.match-row { border: 1px solid var(--line); border-radius: var(--radius); padding: 10px 12px;
  display: flex; flex-direction: column; gap: 8px; background: var(--surface); }
.match-pick { display: flex; gap: 10px; align-items: flex-start; cursor: pointer; }
.match-pick input[type=checkbox] { width: 22px; height: 22px; flex: none; margin-top: 2px; }
.match-title { display: block; font-size: 14px; line-height: 1.35; overflow-wrap: anywhere; }
.match-fields { display: flex; gap: 8px; align-items: flex-end; }
.match-fields label { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.match-fields input { width: 100%; }
.match-row:has(input[type=checkbox]:not(:checked)) .match-fields { opacity: .45; }

/* ---- several sizes of one product in one packing group ------------------ */
.size-divider { display: flex; justify-content: space-between; align-items: center; gap: 10px;
  margin: 14px 0 4px; padding: 10px 14px; border-radius: 10px;
  background: var(--size-soft); color: var(--size); border-left: 4px solid var(--size);
  font-size: 16px; }
.size-divider:first-child { margin-top: 0; }
.size-divider span { font-size: 13px; opacity: .85; white-space: nowrap; }

/* Products → Photos: marketplace photo options, tap one to use it. */
.photo-picks { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.photo-pick { display: flex; flex-direction: column; gap: 4px; align-items: flex-start; margin: 0; min-width: 0; }
.photo-pick button {
  width: 100%; aspect-ratio: 1; padding: 6px; border-radius: var(--radius-sm);
  border: 2px solid var(--line); background: #fff; cursor: pointer;
}
.photo-pick button:hover, .photo-pick button:focus-visible { border-color: var(--accent); }
.photo-pick img { width: 100%; height: 100%; object-fit: contain; display: block; }
.photo-pick-title {
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
  font-size: 12px; line-height: 1.3;
}
