/* Inventory viewer — phone, tablet, and desktop/Mac.
 *
 * Touch is the primary design intent: every interactive target is at least
 * 2.75rem (44px at default text size — Apple HIG), nothing depends on hover
 * to be discoverable, and no right-click menus. Mouse and trackpad on a Mac
 * get the same components; hover states are polish, never the only route.
 *
 * TEXT SIZE FOLLOWS THE OPERATING SYSTEM. That is a deliberate constraint
 * and it dictates the units used throughout:
 *
 *   - The root font size is NEVER set in px. An earlier revision had
 *     `body { font: 15px/1.5 ... }`, which silently overrode the user's OS
 *     text-size setting and made the whole feature a no-op.
 *   - Every text size and every touch target is in `rem`, so all of it
 *     scales together from that root. Sizing targets in px while text scales
 *     is how you end up with clipped labels at large text sizes.
 *   - iOS needs an explicit opt-in: Safari does not apply Dynamic Type to
 *     web content unless the page asks for the `-apple-system-body` font.
 *     See the body rule below.
 *   - Android Chrome scales web text from its Accessibility font-size
 *     setting on its own, provided the page doesn't pin a px root.
 *
 * Three breakpoints, one component set:
 *   default   phone      single column, filters in a bottom sheet
 *   >=48rem   tablet     two-column cards, wider centred sheet
 *   >=64rem   desktop    three columns, wider gutters
 *
 * Breakpoints are in rem so they respond to text size too: at a large OS
 * setting a phone correctly stays in the single-column layout instead of
 * cramming two columns of oversized text.
 */

:root {
  --bg: #0f172a;
  --surface: #1a2436;
  --surface-2: #212d43;
  --border: #2a3650;
  --text: #e6ebf5;
  --text-dim: #8b98b4;
  --accent: #4f8cff;
  --ok: #3ac17c;
  --warn: #e0a72e;
  --danger: #e2574c;

  --radius: 0.625rem;
  --tap: 2.75rem;       /* 44px at default text size — HIG minimum */
  --gap: 0.75rem;
  --pad: 1rem;

  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
}

/* ── Light theme ───────────────────────────────────────────────────────────
 *
 * Three states, not two: System (follow the OS), Light, Dark. Dark is the
 * base defined on :root above, so an explicit dark choice needs no tokens of
 * its own -- it only has to STOP the OS-light rule from applying, which is
 * what the :not([data-theme="dark"]) guard does.
 *
 * The light tokens are written twice on purpose. The alternative -- one block
 * selected by a variable -- cannot work, because a media query cannot be
 * switched on by a custom property. Duplication of six lines is the cheaper
 * mistake; if you edit one block, edit the other.
 *
 * The choice is applied in <head> before first paint (see app.html), so
 * switching to light does not flash a dark screen on every cold start. */

/* Following the OS, and the OS says light. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    color-scheme: light;
    --bg: #f4f6fa;
    --surface: #ffffff;
    --surface-2: #eef1f7;
    --border: #d5dbe6;
    --text: #16202f;
    --text-dim: #5d6a80;
  }
}

/* Explicitly chosen light -- wins whatever the OS says. */
:root[data-theme="light"] {
  color-scheme: light;
  --bg: #f4f6fa;
  --surface: #ffffff;
  --surface-2: #eef1f7;
  --border: #d5dbe6;
  --text: #16202f;
  --text-dim: #5d6a80;
}

/* Explicitly chosen dark. The tokens are already the :root defaults; this
   only needs to tell the browser so form controls and scrollbars follow. */
:root[data-theme="dark"] { color-scheme: dark; }

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

/* The `hidden` attribute is only a UA-stylesheet `display:none`, so ANY later
 * rule setting display (e.g. `.segmented { display:flex }`) silently
 * overrides it and the element paints anyway. This bit — hidden elements
 * rendering on the enrolment screen — was only visible by looking at the
 * running app, not from the DOM, where .hidden correctly read `true`. */
[hidden] { display: none !important; }

html {
  /* No px root: inherit whatever the browser/OS says a default is. This one
   * line is what makes OS text sizing work at all. */
  font-size: 100%;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  padding: 0;
  min-height: 100%;
  background: var(--bg);
  color: var(--text);

  /* iOS Dynamic Type opt-in. Safari only scales web text with the system
   * setting when the page requests the -apple-system-body font; this
   * shorthand does that, and the font-family immediately after restores the
   * intended stack without discarding the Dynamic Type binding. Browsers
   * that don't know the keyword ignore the line and fall through to the
   * normal stack. */
  font: -apple-system-body;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.5;

  overscroll-behavior-y: none;
}

button, input, select { font: inherit; color: inherit; }
button { cursor: pointer; }

/* ── App shell ─────────────────────────────────────────────────────────── */

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

.topbar {
  position: sticky; top: 0; z-index: 20;
  padding: calc(var(--safe-top) + 0.625rem) var(--pad) 0.625rem;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.topbar-row { display: flex; align-items: center; gap: var(--gap); }

.spacer { flex: 1; }

/* The signed-in person. Smaller than a normal .btn -- it is a label you
   glance at and only rarely tap (to sign out), so it should not carry the
   same weight as the actions beside it. The 44px tap height is KEPT: only
   the text and padding shrink, because a control that is hard to hit is a
   worse problem than one that looks large. */
/* Initials in a circle. Fixed square so the top row never reflows when the
   signed-in person changes, and round so it reads as an identity chip rather
   than another action button next to the three icons. */
.btn.person {
  width: var(--tap); min-width: var(--tap);
  padding: 0;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 0.8125rem; font-weight: 700; letter-spacing: .02em;
  color: var(--text-dim);
}

/* ── Section switcher (Rolls / Sheeted / Misc) ─────────────────────────── */

.segmented {
  display: flex;
  margin-top: 0.625rem;
  background: var(--surface-2);
  border-radius: var(--radius);
  padding: 0.1875rem;
  gap: 0.1875rem;
  /* Horizontal swipe between sections is handled in JS; keep the browser's
   * own horizontal panning out of the way here. */
  touch-action: pan-y;
}

.segmented button {
  flex: 1;
  min-height: var(--tap);
  border: none;
  border-radius: calc(var(--radius) - 0.1875rem);
  background: transparent;
  color: var(--text-dim);
  font-weight: 600;
  font-size: 0.875rem;
}

.segmented button[aria-selected="true"] {
  background: var(--accent);
  color: #fff;
}

/* ── Search ────────────────────────────────────────────────────────────── */

.search-row { display: flex; gap: 0.5rem; margin-top: 0.625rem; align-items: center; }

/* The four product-code slots. They share the row with the Filter button and
   must be the part that gives way, so the bar carries min-width:0 (see the
   note on .search-row input below) and the button does not shrink at all. */
.pcbar {
  flex: 1 1 auto;
  min-width: 0;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0.25rem;
}

.search-row input {
  /* min-width:0 is the fix, not a tidy-up. A flex item defaults to
     min-width:auto, which refuses to shrink below its intrinsic content
     width -- so at a larger phone text size the input stayed ~263px wide and
     pushed the Filter button clean off the right edge (48px off at 320px
     with a 20px root font). Measured, not theorised. */
  min-width: 0;
  width: 100%;
  min-height: var(--tap);
  padding: 0 0.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text);
  text-align: center;
  /* >=1rem: iOS Safari zooms the viewport when focusing a smaller input. */
  font-size: 1rem;
}

/* Placeholders are the only labels these slots get, so they must survive a
   narrow phone rather than ellipsing to "G...". */
.search-row input::placeholder { font-size: 0.8125rem; opacity: .6; }

/* Rejected input: the desktop flashes the border red for 600ms and keeps
   focus. Same idea, same duration -- see pcFlash() in app.js. */
.search-row input.pc-bad {
  border-color: var(--danger);
  box-shadow: 0 0 0 1px var(--danger) inset;
}

.search-row input:focus {
  outline: 2px solid var(--accent);
  outline-offset: -1px;
  border-color: transparent;
}

.filter-btn {
  /* Never shrink and never be shrunk out of the row -- it is the only way to
     reach every other criterion. */
  flex: 0 0 auto;
  min-width: var(--tap); min-height: var(--tap);
  padding: 0 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  font-weight: 600;
  position: relative;
}

.filter-btn .badge {
  position: absolute; top: 0.25rem; right: 0.25rem;
  min-width: 1.125rem; height: 1.125rem;
  border-radius: 0.5625rem;
  background: var(--accent); color: #fff;
  font-size: 0.6875rem; font-weight: 700; line-height: 1.125rem;
}

.sync-row {
  display: flex; align-items: center;
  gap: var(--gap);
  margin-top: 0.5rem;
}

.sync-status {
  color: var(--text-dim);
  font-size: 0.8125rem;
}

.icon-btn {
  display: flex; align-items: center; justify-content: center; gap: 0.375rem;
  min-height: var(--tap);
  padding: 0 0.875rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--text);
  font-size: 0.8125rem; font-weight: 600;
}

/* Icon-only: square, and the label lives in aria-label/title instead. */
.icon-btn.icon-only {
  position: relative;
  width: var(--tap); min-width: var(--tap);
  padding: 0;
}
.icon-btn.icon-only svg { width: 1.25rem; height: 1.25rem; display: block; }

/* The active-criteria count, on the funnel. Same treatment it had on the old
   text button -- it is the only thing telling you a filter is on now that the
   word "Filter" is gone. */
.icon-btn .badge {
  position: absolute; top: 0.125rem; right: 0.125rem;
  min-width: 1.125rem; height: 1.125rem;
  border-radius: 0.5625rem;
  background: var(--accent); color: #fff;
  font-size: 0.6875rem; font-weight: 700; line-height: 1.125rem;
  text-align: center;
}

/* Disabled means "nothing to clear", not "broken" — dimmed rather than
   hidden so the control keeps its place and the row does not reflow every
   time the last criterion goes. */
.icon-btn:disabled { opacity: 0.4; }

/* ── Pull to refresh ───────────────────────────────────────────────────── */

.ptr {
  position: absolute; left: 0; right: 0; top: 0;
  display: flex; align-items: center; justify-content: center;
  gap: 0.5rem;
  height: 0;
  overflow: hidden;
  color: var(--text-dim);
  font-size: 0.8125rem;
  pointer-events: none;
}

.ptr .arrow { transition: transform .15s ease; display: inline-block; }
.ptr.armed .arrow { transform: rotate(180deg); }

/* ── Results ───────────────────────────────────────────────────────────── */

main {
  flex: 1;
  padding: var(--gap) var(--pad) 6rem;
  position: relative;
  /* Vertical panning only: the horizontal section-swipe is JS-driven, and
   * letting the browser also pan horizontally makes the gesture fight the
   * scroller. */
  touch-action: pan-y;
}

.cards { display: grid; gap: var(--gap); grid-template-columns: 1fr; }

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

/* Column headings exist only for the tablet/desktop table layout; a phone
   stacks each card's values with no columns to label. */
.col-head { display: none; }

/* End-of-page marker. Scrolling it into view appends the next page (see
   app.js's observeSentinel), so in normal use it is only glimpsed. It stays
   tappable as the fallback for browsers without IntersectionObserver. */
.more {
  padding: 1rem var(--pad) 1.25rem;
  text-align: center;
  color: var(--text-dim);
  font-size: 0.8125rem;
}

.card-head {
  display: flex; align-items: center; gap: var(--gap);
  width: 100%;
  min-height: var(--tap);
  padding: 0.75rem var(--pad);
  border: none;
  background: transparent;
  text-align: left;
}

.card-head:active { background: var(--surface-2); }

.card-title { flex: 1; min-width: 0; }

/* display:block matters and is not cosmetic: these are <span>s, and on an
   INLINE box overflow/text-overflow do nothing at all — the text simply
   spills out of the flex item. That went unnoticed while the spec line was
   short, then showed up the moment width gained 3 decimals and a "x length":
   on a two-column tablet the spec ran underneath the big metric number
   instead of ellipsising. */
/* The cell itself must NOT clip: overflow:hidden here truncated the whole
   thing, pill included, so a long grade lost its status letter entirely.
   Only .gtxt truncates; the pill is pinned and always survives. */
.card-title .primary {
  display: flex; align-items: center; gap: 0.375rem;
  min-width: 0;
  font-weight: 600; font-size: 0.9375rem;
}

.card-title .primary .gtxt {
  min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Never shrink, never be ellipsised away -- it is a single character and the
   thing people scan the column for. */
.card-title .primary .pill { flex: 0 0 auto; }

/* Phone: the spec and context values wrap as flowing dim text under the
   grade. Each value is its own .fld element (so the tablet layout can align
   them into columns) — the "·" separators are therefore drawn in CSS, and
   only between values that actually have content. */
.card-title .secondary,
.card-title .tags {
  display: flex; flex-wrap: wrap;
  column-gap: 0.35rem;
  color: var(--text-dim);
  overflow-wrap: anywhere;
}

.card-title .secondary { font-size: 0.8125rem; }
.card-title .tags { font-size: 0.75rem; opacity: 0.85; }

/* A blank value takes no room on a phone. On tablet it must still hold its
   grid cell, or every later column on that row shifts — see the 48rem block. */
.fld:empty { display: none; }

/* Separator before any value that has a non-empty value before it, so the
   first one never gets a leading dot and blanks never leave a stray one. */
.card-title .secondary .fld:not(:empty) ~ .fld:not(:empty)::before,
.card-title .tags .fld:not(:empty) ~ .fld:not(:empty)::before {
  content: '· ';
  opacity: 0.6;
}

/* A column instead of two blocks so `order` can float the leading number to
   the top on a phone. The DOM order stays qty-then-wt because the tablet
   layout dissolves this wrapper into the column grid, where position IS the
   column and headerRow's labels depend on it. */
.card-metric {
  display: flex; flex-direction: column; align-items: flex-end;
  text-align: right; white-space: nowrap;
}

/* Emphasis follows the section, not the column — see groupCard(). Rolls are
   sold by weight, sheeted by sheet count, so whichever one that is gets the
   full-contrast text and the large size, and the other drops to the dim
   supporting line it used to be. */
.card-metric .lead {
  order: 0;
  color: var(--text);
  font-size: 1.375rem; font-weight: 700; line-height: 1.15;
  font-variant-numeric: tabular-nums;
}
.card-metric .lead i {
  font-style: normal; font-weight: 600;
  color: var(--text-dim); font-size: 0.75rem;
}

.card-metric .sub {
  order: 1;
  color: var(--text-dim);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
}
.card-metric .sub b { font-weight: 600; }
.card-metric .sub i { font-style: normal; opacity: 0.8; }

.chev { color: var(--text-dim); transition: transform .15s ease; }
.card[open] .chev { transform: rotate(90deg); }

.card-body { border-top: 1px solid var(--border); background: var(--surface-2); }

.detail-row {
  display: flex; gap: var(--gap); align-items: center;
  padding: 0.625rem var(--pad);
  font-size: 0.8125rem;
  border-bottom: 1px solid var(--border);
}

.detail-row:last-child { border-bottom: none; }
.detail-row .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.detail-row .dim { color: var(--text-dim); }

/* Child rows are columnar, matching the desktop app's expanded sub-table:
   Inv No / GSM / Bin / [Qty] / Weight. Numeric columns are right-aligned and
   tabular so figures line up down the column, as they do there. */
.detail-row.rolls   { display: grid; grid-template-columns: 1fr 3.5rem 3.5rem 5rem; }
.detail-row.sheeted { display: grid; grid-template-columns: 1fr 3.5rem 3.5rem 5rem 5rem; }
.detail-row.misc    { display: grid; grid-template-columns: 1fr 5rem 4rem; }

.detail-row.rolls > span:not(:first-child),
.detail-row.sheeted > span:not(:first-child),
.detail-row.misc > span:not(:first-child) {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.detail-row.head {
  color: var(--text-accent, var(--text-dim));
  font-size: 0.6875rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.02em;
  background: var(--bg);
  padding-top: 0.375rem; padding-bottom: 0.375rem;
}

/* Label/value list for the columns that don't fit the card headline. */
.fields {
  display: grid; grid-template-columns: auto 1fr;
  gap: 0.25rem 0.75rem;
  padding: 0.625rem var(--pad);
  font-size: 0.8125rem;
  border-bottom: 1px solid var(--border);
}

.fields .f { display: contents; }
.fields .k { color: var(--text-dim); white-space: nowrap; }
.fields .v { min-width: 0; overflow-wrap: anywhere; }

/* Misc level-2 (location group), itself expandable to level-3 inv-no rows. */
.subcard { border-bottom: 1px solid var(--border); }
.subcard:last-child { border-bottom: none; }

.group-head {
  width: 100%;
  min-height: var(--tap);
  border: none;
  background: transparent;
  text-align: left;
  color: inherit;
  font: inherit;
  font-size: 0.8125rem;
}

.group-head:active { background: var(--surface); }
.group-head .spacer { flex: 1; }
.subcard[open] .group-head .chev { transform: rotate(90deg); }
.subcard-body { background: var(--bg); }

.pill {
  display: inline-block; padding: 0.125rem 0.5rem; border-radius: 99px;
  font-size: 0.6875rem; font-weight: 700; text-transform: uppercase;
}
.pill.available { background: rgba(58,193,124,.15); color: var(--ok); }
.pill.committed { background: rgba(224,167,46,.15); color: var(--warn); }
.pill.quote     { background: rgba(79,140,255,.15); color: var(--accent); }
.pill.damaged   { background: rgba(226,87,76,.15);  color: var(--danger); }
/* Order-status codes other than C/Q (P, W — see statusPill's note on why
   they are shown as raw codes rather than expanded into words). */
.pill.pending   { background: rgba(230,168,53,.15); color: var(--warn, #e6a835); }

/* ── Sticky summary ────────────────────────────────────────────────────── */

.summary {
  position: fixed; left: 0; right: 0; bottom: 0;
  z-index: 15;
  padding: 0.75rem var(--pad) calc(0.75rem + var(--safe-bottom));
  background: var(--surface);
  border-top: 1px solid var(--border);
  display: flex; gap: var(--gap); justify-content: space-between;
  font-size: 0.8125rem;
}

.summary .stat { display: flex; gap: 0.375rem; align-items: baseline; }
.summary .stat b { font-size: 0.9375rem; font-variant-numeric: tabular-nums; }
.summary .stat span { color: var(--text-dim); }

/* ── Filter sheet ──────────────────────────────────────────────────────── */

.sheet-backdrop {
  position: fixed; inset: 0; z-index: 30;
  background: rgba(0,0,0,.5);
  opacity: 0; pointer-events: none;
  transition: opacity .2s ease;
}
.sheet-backdrop.open { opacity: 1; pointer-events: auto; }

.sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 31;
  /* 92dvh, up from 85: the criteria set grew from four fields to eleven, and
     the extra 7% is a whole field's worth of visible content on a phone.
     Kept BELOW 100 so the sheet still reads as a sheet with the list behind
     it, and so the drag-to-dismiss grip has somewhere to live. */
  max-height: 92dvh;
  display: flex; flex-direction: column;
  background: var(--surface);
  border-radius: var(--radius) var(--radius) 0 0;
  border-top: 1px solid var(--border);
  transform: translateY(100%);
  transition: transform .25s cubic-bezier(.2,.8,.2,1);
  padding-bottom: var(--safe-bottom);
}
.sheet.open { transform: translateY(0); }

/* While a finger is dragging the sheet, JS drives transform directly and the
 * transition must not fight it — otherwise the sheet lags behind the thumb. */
.sheet.dragging { transition: none; }

.sheet-grip {
  width: 2.5rem; height: 0.25rem; border-radius: 0.125rem;
  background: var(--border);
  margin: 0.625rem auto 0.25rem;
}

/* The whole head area is the drag handle, not just the little grip bar —
 * a 4px target would be unusable. touch-action:none so the browser doesn't
 * try to scroll while the sheet is being dragged. */
.sheet-head, .sheet-grip { touch-action: none; }

.sheet-head {
  display: flex; align-items: center; gap: var(--gap);
  padding: 0.375rem var(--pad) 0.75rem;
  border-bottom: 1px solid var(--border);
}
.sheet-head h2 { font-size: 0.9375rem; margin: 0; flex: 1; }

.sheet-body { overflow-y: auto; padding: var(--pad); flex: 1; overscroll-behavior: contain; }

/* Two columns as soon as there is room. Eleven stacked criteria is a long
   scroll on anything wider than a phone, and the sheet is capped at 35rem on
   tablet so a single column just leaves half of it empty. The option lists
   span both columns -- they hold the widest content and are the one place a
   narrow column would force chips to wrap badly. */
@media (min-width: 30rem) {
  .sheet-body {
    display: grid;
    /* minmax(0, 1fr), NOT 1fr. A grid track sized 1fr still floors at the
       intrinsic width of its content, so the two columns came out 422px each
       inside a 560px sheet and the right-hand one was clipped off the edge.
       Same min-width:auto trap as the flex row above, different axis. */
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: 1rem;
    align-content: start;
  }
  .sheet-body > .facet { grid-column: 1 / -1; }
}

/* A short viewport -- a phone in landscape, or a small tablet with the
   keyboard up -- is the case where the sheet stops fitting at all. Give the
   option lists less room rather than letting them push the Clear / Show
   results footer off the bottom. */
@media (max-height: 34rem) {
  .facet-opts { max-height: 7rem; }
  .field { margin-bottom: 0.75rem; }
  .field .hint, .facet .hint { display: none; }
}
.sheet-foot {
  padding: 0.75rem var(--pad);
  border-top: 1px solid var(--border);
  display: flex; gap: 0.5rem;
}

.field { margin-bottom: 1.125rem; }
/* Descendant, not child: Width and Length wrap their label in .field-head
   to make room for the swap button, and a `>` selector left exactly those
   two fields un-styled while every other label shouted. */
.field label {
  display: block; margin-bottom: 0.5rem;
  font-size: 0.75rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .04em;
  color: var(--text-dim);
}

/* Units are NOT uppercased: "µm" uppercases to "MM", silently turning
   microns into millimetres, and "g/m²" into "G/M²". A label may shout; a
   unit must stay exactly what it is. */
.field label .unit { text-transform: none; letter-spacing: 0; }

.chips { display: flex; flex-wrap: wrap; gap: 0.5rem; }

.chip {
  min-height: var(--tap);
  padding: 0 1rem;
  border: 1px solid var(--border);
  border-radius: 99px;
  background: var(--surface-2);
  color: var(--text-dim);
  font-size: 0.875rem; font-weight: 600;
}

.chip[aria-pressed="true"] {
  background: var(--accent); border-color: var(--accent); color: #fff;
}

.field > .field-head {
  display: flex; align-items: baseline; gap: 0.5rem;
  margin-bottom: 0.5rem;
}
.field > .field-head > label { margin-bottom: 0; }

/* A criterion's fine print — the desktop panel carries these as tooltips,
   which a touch device has no way to summon. */
.field .hint, .facet .hint {
  margin: 0.5rem 0 0;
  font-size: 0.75rem; line-height: 1.4;
  color: var(--text-dim);
}

.linkbtn {
  background: none; border: 0; padding: 0.25rem 0;
  color: var(--accent); font-size: 0.75rem; font-weight: 600;
}

/* ── Collapsible option lists (Grade / Origin / Code) ───────────────────
   Collapsed by default because at real scale the Code list alone runs to
   hundreds of values, and a sheet that opens on three walls of chips is
   worse than no advanced filter at all. */
.facet {
  margin-bottom: 1.125rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.facet > summary {
  display: flex; align-items: center; gap: 0.5rem;
  min-height: var(--tap);
  padding: 0 0.875rem;
  cursor: pointer;
  font-size: 0.75rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .04em;
  color: var(--text-dim);
  list-style: none;               /* Firefox */
}
.facet > summary::-webkit-details-marker { display: none; }

/* The twisty is drawn rather than inherited: the native marker sits outside
   the flex row and cannot be aligned with a 44px tap target. */
.facet > summary::after {
  content: "▾";
  margin-left: auto;
  transition: transform .15s ease;
}
.facet[open] > summary::after { transform: rotate(180deg); }

.facet > summary .badge {
  min-width: 1.125rem; height: 1.125rem;
  padding: 0 0.25rem;
  border-radius: 0.5625rem;
  background: var(--accent); color: #fff;
  font-size: 0.6875rem; font-weight: 700; line-height: 1.125rem;
  text-align: center;
}

.facet .facet-search { padding: 0 0.625rem; }

/* Capped and scrollable: the sheet itself must stay scrollable past a long
   list, so the list scrolls inside its own box rather than pushing Width and
   GSM off the bottom. */
.facet-opts {
  max-height: 13rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0.625rem;
}

.num-row input:disabled {
  opacity: .5;
}

.num-row { display: flex; gap: 0.5rem; align-items: center; }

.num-row input {
  flex: 1; min-width: 0;
  min-height: var(--tap);
  padding: 0 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  font-size: 1rem;                    /* iOS zoom guard */
  font-variant-numeric: tabular-nums;
}

.btn {
  min-height: var(--tap);
  padding: 0 1.125rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  font-weight: 600;
}
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn.grow { flex: 1; }

/* ── States ────────────────────────────────────────────────────────────── */

.state {
  text-align: center;
  padding: 3rem var(--pad);
  color: var(--text-dim);
}
.state h2 { color: var(--text); font-size: 1.0625rem; margin: 0 0 0.5rem; }
.state p { margin: 0 0 1.25rem; }

.banner {
  display: flex; align-items: center; gap: var(--gap);
  padding: 0.75rem var(--pad);
  font-size: 0.8125rem;
  border-bottom: 1px solid var(--border);
}
.banner.warn   { background: rgba(224,167,46,.12); color: var(--warn); }
.banner.danger { background: rgba(226,87,76,.12);  color: var(--danger); }
.banner.update { background: rgba(79,140,255,.12); color: var(--accent); }

.banner .update-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--gap);
}
.banner .update-row .btn { min-height: 2.25rem; padding: 0 0.875rem; }

.spinner {
  width: 1.75rem; height: 1.75rem; margin: 0 auto 1rem;
  border: 3px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* ── Tablet ────────────────────────────────────────────────────────────── */
/* 48rem = 768px at default text size, but scales: at a large OS text size a
 * phone stays single-column instead of cramming two columns of big text. */

@media (min-width: 48rem) {
  :root { --pad: 1.5rem; }

  /* ONE column, and each item becomes a single aligned row — not a grid of
     stacked cards. A tablet has the width to show the desktop app's columns
     side by side, and reading down a column of aligned values is far easier
     than scanning stacked blocks. Two columns of cards was the earlier
     design and it wasted that width.

     The row is a CSS grid, and the phone's wrapper elements (.card-title,
     .secondary, .tags, .card-metric) are dissolved with display:contents so
     every .fld inside them becomes a direct grid cell. That way one set of
     markup serves both layouts — no per-breakpoint re-render in JS. */
  .cards { grid-template-columns: 1fr; gap: 0; }

  .card { border-radius: 0; border-bottom: none; }
  .card:first-child { border-top-left-radius: var(--radius); border-top-right-radius: var(--radius); }
  .card:last-child {
    border-bottom: 1px solid var(--border);
    border-bottom-left-radius: var(--radius); border-bottom-right-radius: var(--radius);
  }

  .card-head {
    display: grid;
    align-items: baseline;
    column-gap: 0.625rem;
    padding: 0.5rem var(--pad);
    font-size: 0.8125rem;
  }

  .card-title,
  .card-title .secondary,
  .card-title .tags,
  .card-metric { display: contents; }

  /* Every cell holds its column even when blank, or later values on that row
     slide left and the table stops lining up. */
  .fld:empty { display: block; }
  .fld { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

  /* Kill the phone's "·" separators — columns separate the values here. The
     selector has to mirror the phone rule's shape (:not(:empty) ~ ...), not
     just target .fld::before: the phone rule is more specific, so a plainer
     selector loses and the dots leak into the table cells. */
  .card-title .secondary .fld:not(:empty) ~ .fld:not(:empty)::before,
  .card-title .tags .fld:not(:empty) ~ .fld:not(:empty)::before {
    content: none;
  }

  .card-title .primary,
  .card-title .secondary,
  .card-title .tags { font-size: inherit; opacity: 1; }

  .card-title .primary {
    font-weight: 600;
    display: flex; align-items: center; gap: 0.375rem;
  }

  /* Right-align the numeric columns, as the desktop app's tree does. */
  .fld.gsmcal, .fld.dim, .fld.qty, .fld.wt {
    text-align: right; font-variant-numeric: tabular-nums;
  }
  /* One aligned column each, so the phone's size/emphasis split would only
     make the rows ragged — the header row already names both columns, and
     the reader is scanning down a column rather than picking a headline out
     of a card. */
  .card-metric .lead, .card-metric .sub {
    color: inherit; font-size: inherit; font-weight: inherit; line-height: inherit;
    /* MUST be reset: display:contents promotes these to grid items of the
       row, where `order` would move them out of their column. */
    order: 0;
  }
  .card-metric .lead b, .card-metric .sub b { font-weight: inherit; }
  .card-metric .fld i { display: none; }   /* the header row names the unit */

  /* Rolls / Sheeted, in the desktop app's column order.
     A PORTRAIT tablet (768px) cannot hold all 13 columns: fitting them all
     shrank every cell until the values themselves were ellipsised
     ("1270.000" showing as "127...", codes as "AA8..."), which defeats the
     point of the layout. So the four lowest-value columns — cat, pkd, ord#,
     customer — are dropped here and restored at 64rem, where there is room
     for them at full width. Nothing is lost: expanding the row still shows
     every field, and the phone layout shows them all inline. */
  /* Widths are a deliberate budget, not guesses: at 48rem the row has about
     41rem of usable width after padding and gaps, and these sum just under
     it. An earlier version gave the width column 1fr, which sounds flexible
     but is the opposite — fixed tracks are satisfied FIRST, so 1fr received
     only the 37px left over and every width rendered as "127...". The
     dimension column is the widest here because sheeted shows
     "900.000 x 780.000" in it.

     Budgeting in rem (not px) is what keeps this safe under OS text scaling:
     if the reader's text grows, the 48rem media query stops matching before
     the columns can overflow, and the layout falls back to stacked cards. */
  .card.paper .card-head {
    column-gap: 0.5rem;
    /* GRADE is max-content, not a fixed track. A grid max-content track sizes
       to the widest cell in the WHOLE column, so the grade reads in full
       whatever the ERP holds -- at 5.25rem "ARTBOARD" plus its status pill
       needed ~97px and rendered as "ARTBOA...", which is the one column you
       cannot afford to guess at.
       The dimension column is now the flexible one and gives back the space:
       it holds "900.000 x 780.000" so it is the widest fixed track and the
       only one with slack to spare. */
    grid-template-columns:
      minmax(5.25rem, max-content) minmax(0, 3.5rem) minmax(6rem, 1fr)
      minmax(0, 2.75rem) minmax(0, 3.5rem) minmax(0, 2.75rem)
      minmax(0, 2.5rem) minmax(0, 2.5rem) minmax(0, 3.75rem) 0.75rem;
  }
  .card.paper .fld.cat,
  .card.paper .fld.pkd,
  .card.paper .fld.ord,
  .card.paper .fld.cust { display: none; }

  /* Misc: item-id, description, cat, pkd, qoh, chevron. */
  .card.misc .card-head {
    /* Same reasoning as the grade column above: the item-id is the thing
       being looked up, so it is sized to content and the description -- which
       is prose and reads fine truncated -- absorbs the difference. */
    grid-template-columns:
      minmax(8rem, max-content) minmax(0, 1fr) minmax(0, 3rem) minmax(0, 3.5rem)
      minmax(0, 6rem) 0.75rem;
  }

  .chev { align-self: center; }

  /* Column headings. Sticky under the top bar so they stay readable while
     scrolling a long list — the whole point of a column layout is being able
     to tell which column you are looking at. */
  .col-head {
    display: block;
    position: sticky; top: 0; z-index: 5;
    background: var(--surface-2);
    border-bottom: 1px solid var(--border);
  }
  .col-head .card-head {
    min-height: 0;
    padding-top: 0.4375rem; padding-bottom: 0.4375rem;
    color: var(--text-dim);
    font-size: 0.6875rem; font-weight: 700;
    text-transform: uppercase; letter-spacing: 0.02em;
  }
  .col-head .fld { white-space: nowrap; }
  .col-head .fld.gsmcal, .col-head .fld.dim,
  .col-head .fld.qty, .col-head .fld.wt { text-align: right; }

  /* The sheet becomes a centred panel rather than a full-width slab — a
   * 768px-wide row of controls is mostly empty space.
   *
   * An earlier revision hid the Filter button here and docked a permanent
   * side panel. That shipped broken: nothing ever added the `docked` class,
   * so tablets lost filter access entirely. One consistent sheet on every
   * size is easier to reason about, and it's the pattern already proven on
   * phone. */
  .sheet {
    /* An explicit width, not just max-width. This element is position:fixed
       with left set and right:auto, so it shrink-to-fits its content -- and
       once the body grid used minmax(0,1fr) the content could shrink freely,
       collapsing the panel to 377px inside a 768px screen. max-width alone
       caps a width nothing was establishing. */
    width: min(35rem, 100vw);
    max-width: 35rem;
    left: 50%;
    right: auto;
    transform: translate(-50%, 100%);
  }
  .sheet.open { transform: translate(-50%, 0); }
}

/* ── Desktop / Mac ─────────────────────────────────────────────────────── */

@media (min-width: 64rem) {
  :root { --pad: 2rem; }

  .app-width { max-width: 87.5rem; margin: 0 auto; width: 100%; }

  /* Still one row per item — the extra width goes to columns, not to a
     second card. At this width the four columns dropped at 48rem fit, so
     the row now carries the desktop app's full set. */
  .card.paper .fld.cat,
  .card.paper .fld.pkd,
  .card.paper .fld.ord,
  .card.paper .fld.cust { display: block; }

  .card.paper .card-head {
    /* Grade to content here too; the customer column (1fr) is the absorber. */
    grid-template-columns:
      minmax(8rem, max-content) minmax(0, 3.5rem) minmax(0, 7.5rem) minmax(0, 4rem)
      minmax(0, 5rem) minmax(0, 3.25rem) minmax(0, 2.5rem) minmax(0, 3rem)
      minmax(0, 3.5rem) minmax(0, 4.5rem) minmax(0, 1fr)
      minmax(0, 4.5rem) minmax(0, 5rem) 0.75rem;
  }

  /* Hover is polish only — all of this is already reachable by tap. */
  .card-head:hover { background: var(--surface-2); }
  .chip:hover { border-color: var(--accent); }
  .btn:hover { filter: brightness(1.1); }
}


/* ── Settings ──────────────────────────────────────────────────────────────
   A real <dialog> rather than a second .sheet: the filter sheet is a
   singleton with drag-to-dismiss wired to that specific element, and reusing
   it would leave two features fighting over one open/closed state. */

.btn.icon-only {
  min-width: var(--tap);
  padding: 0;
  font-size: 1.125rem;
  line-height: 1;
}

dialog#settings-modal {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0;
  background: var(--surface);
  color: var(--text);
  width: calc(100% - 2rem);
  max-width: 26rem;
}

dialog#settings-modal::backdrop { background: rgba(0, 0, 0, .5); }

.settings-head {
  display: flex; align-items: center; gap: var(--gap);
  padding: 0.75rem var(--pad);
  border-bottom: 1px solid var(--border);
}
.settings-head h2 { font-size: 0.9375rem; margin: 0; flex: 1; }

.settings-body { padding: var(--pad); }

.settings-row {
  padding: 0.75rem 0;
  border-bottom: 1px solid var(--border);
}
.settings-row:last-child { border-bottom: none; }
.settings-row h3 { margin: 0 0 0.25rem; font-size: 0.875rem; font-weight: 600; }
.settings-row p { margin: 0 0 0.625rem; color: var(--text-dim); font-size: 0.8125rem; }
.settings-row .btn { width: 100%; }

.settings-facts { display: grid; grid-template-columns: auto 1fr; gap: 0.25rem 0.75rem; font-size: 0.8125rem; }
.settings-facts .k { color: var(--text-dim); white-space: nowrap; }
.settings-facts .v { overflow-wrap: anywhere; }
