/* ============================================================================
   INNOVOTE INVENTORY — COMPONENT LAYER
   References tokens.css only. No raw hex below this line except where a
   1-bit print path forces it.

   RTL: written with CSS logical properties throughout. There is not a single
   `left` or `right` in this file. Arabic is the default, not a mirror.

   PORTED from innovote-inventory-design/components.css. Sections 0 through 17
   are the design's, unaltered except where marked. Sections 18 onward are new
   here: the section scaffolding the screens used to carry inline, the
   authentication screens, the user switch, and the defensive minimum-size rule
   the design's own overflow harness argued for but only applied case by case.

   THE ONE RULE WORTH READING BEFORE EDITING THIS FILE.
   A grid or flex item defaults to `min-width: auto`, which resolves to
   min-content, which silently makes the page wider than the phone. It is not
   visible; it is only measurable. Four of the design's defects were this, and
   the fix was applied four times to four selectors. Section 18 applies it once,
   to every layout child, so the fifth one never ships. Anything that genuinely
   needs its intrinsic width opts back in with `min-inline-size: auto`.
   ========================================================================= */

/* --------------------------------------------------------------- 0. BASE -- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--fs-md);
  line-height: var(--lh-body);
  color: var(--ink-1);
  background: var(--surface-app);
  -webkit-font-smoothing: antialiased;
  /* Western digits site-wide (Brand Manual). Tabular so columns line up. */
  font-variant-numeric: tabular-nums lining-nums;
}
h1,h2,h3,h4,h5 { margin: 0; font-weight: var(--fw-black); line-height: var(--lh-tight); letter-spacing: var(--tracking-tight); }
p { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
button, input, select, textarea { font: inherit; color: inherit; }
img, svg { display: block; max-width: 100%; }
a { color: var(--ink-link); text-decoration: underline; text-underline-offset: 2px; }
a:hover { color: var(--ink-link-hover); }

:where(a, button, [tabindex], input, select, textarea, summary):focus-visible {
  outline: var(--focus-width) solid var(--focus-ring);
  outline-offset: var(--focus-offset);
  border-radius: var(--r-xs);
}

.u-vh { position:absolute; width:1px; height:1px; margin:-1px; padding:0; overflow:hidden; clip-path:inset(50%); white-space:nowrap; }
.u-wrap { max-width: var(--w-content); margin-inline: auto; padding-inline: var(--sp-4); }
.u-mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums lining-nums; }
.u-caps { font-size: var(--fs-micro); font-weight: var(--fw-bold); letter-spacing: var(--tracking-caps); text-transform: uppercase; }
.u-muted { color: var(--ink-3); }
.u-sec  { color: var(--ink-2); }
.u-end  { text-align: end; }
.u-stack { display: grid; gap: var(--sp-4); }
.u-row-flex { display: flex; align-items: center; gap: var(--sp-3); }

/* ================================================================
   1. BIDI ISOLATION — the load-bearing primitive
   Every Latin token that can sit inside Arabic text carries isolation.
   Three layers, deliberately redundant:
     a) <bdi> in the markup (the correct HTML primitive, no CSS needed)
     b) `unicode-bidi: isolate` on the class, so a template that forgot
        <bdi> still renders correctly
     c) `direction: ltr` so the token's own internal order is fixed
   Plain-text paths (thermal labels, CSV, SMS) use U+2068 FSI / U+2069 PDI
   instead — there is no HTML there.
   ================================================================ */
.iso,
.code, .qty, .addr, .lot, .pack, .datestamp, .docid, .ltr {
  unicode-bidi: isolate;
  direction: ltr;
}
/* An LTR block that must sit inside an RTL layout without dragging the row. */
.iso-block { unicode-bidi: isolate-override; direction: ltr; }
/* Deliberately broken, for the failure demo in index.html. Never ship this. */
.iso-off { unicode-bidi: normal !important; direction: inherit !important; }

/* ============================================ 2. LOGO + CLEAR SPACE ====== */
/* Clear space >= 1x the O-ring height, 0.5x hard minimum. Enforced in CSS so
   nobody has to remember: the wrapper's padding IS the clear space. */
.logo { display: inline-flex; align-items: center; gap: .55em; }
.logo--wordmark { min-width: 120px; }            /* brand minimum on screen */
.logo--wordmark svg { width: 100%; height: auto; }
.logo--mark { width: 24px; }                     /* brand minimum for the mark */
.logo--mark svg { width: 100%; height: auto; }
.logo-clear { padding: 1em; }                    /* 1x O-ring height */
.logo-clear--min { padding: .5em; }              /* the hard floor */
/* Lockup used in the app bar: mark + set wordmark is NOT allowed, so the
   app bar uses the mark plus plain type for the app name, kept visibly
   separate by a rule. Never a re-typed wordmark. */
.applock { display: flex; align-items: center; gap: var(--sp-3); }
.applock__div { width: 1px; align-self: stretch; background: var(--line-chrome); margin-block: .35rem; }
.applock__name { font-size: var(--fs-sm); font-weight: var(--fw-semi); color: var(--ink-on-chrome-2); letter-spacing: .01em; }

/* ================================================ 3. APP SHELL ========== */
.app { min-height: 100dvh; display: flex; flex-direction: column; background: var(--surface-app); }

.appbar {
  position: sticky; inset-block-start: 0; z-index: var(--z-appbar);
  min-height: var(--appbar-h);
  display: flex; align-items: center; gap: var(--sp-2);
  padding-inline: var(--sp-2) var(--sp-3);
  background: var(--surface-chrome); color: var(--ink-on-chrome);
  border-block-end: var(--border-hair) solid var(--line-chrome);
}
.appbar__title { font-size: var(--fs-lg); font-weight: var(--fw-bold); letter-spacing: var(--tracking-tight); min-width: 0; }
.appbar__title small { display: block; font-size: var(--fs-2xs); font-weight: var(--fw-medium); color: var(--ink-on-chrome-3); letter-spacing: 0; }
.appbar__spacer { flex: 1; }
.iconbtn {
  min-width: var(--tap); min-height: var(--tap);
  display: inline-grid; place-items: center;
  background: transparent; border: 0; border-radius: var(--r-sm);
  color: inherit; cursor: pointer;
}
.iconbtn:hover { background: rgba(255,255,255,.09); }
.iconbtn--onlight:hover { background: var(--surface-sunken); }
.iconbtn__glyph { font-size: 1.25rem; line-height: 1; }
/* Directional glyphs mirror. Object glyphs (box, printer, camera) never do. */
[dir="rtl"] .glyph-dir { transform: scaleX(-1); }

.main { flex: 1; padding-block-end: calc(var(--tabbar-h) + var(--sp-6)); }
.main--task { padding-block-end: calc(var(--actionbar-h) + var(--sp-6)); }

.tabbar {
  position: fixed; inset-block-end: 0; inset-inline: 0; z-index: var(--z-appbar);
  height: var(--tabbar-h);
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  background: var(--surface-chrome); border-block-start: var(--border-hair) solid var(--line-chrome);
  padding-block-end: env(safe-area-inset-bottom);
}
.tabbar__item {
  display: grid; place-items: center; gap: 2px;
  min-height: var(--tap); text-decoration: none;
  color: var(--ink-on-chrome-3); font-size: var(--fs-2xs); font-weight: var(--fw-semi);
}
.tabbar__item[aria-current="page"] { color: var(--ink-on-chrome); }
.tabbar__item[aria-current="page"] .tabbar__glyph { color: var(--amber-fill); }
.tabbar__glyph { font-size: 1.35rem; line-height: 1; }
/* The fifth slot is a <button>, so it needs the chrome's own reset. */
button.tabbar__item { background: none; border: 0; cursor: pointer; font: inherit; padding: 0; }
/* A destination that does not exist yet. Not a link, not tappable, and
   visibly not the same thing as the four that are. */
.tabbar__item--unbuilt { opacity: .45; cursor: default; }

/* The one primary action per screen. Bottom, thumb-height, full width. */
.actionbar {
  position: fixed; inset-block-end: 0; inset-inline: 0; z-index: var(--z-actionbar);
  background: var(--surface-card);
  border-block-start: var(--border-hair) solid var(--line-1);
  box-shadow: var(--elev-sheet);
  padding: var(--sp-3) var(--sp-4) calc(var(--sp-3) + env(safe-area-inset-bottom));
  display: grid; gap: var(--sp-2);
}
/* "What happens next" — a promise, above the button, always present. */
.actionbar__next { font-size: var(--fs-sm); color: var(--ink-2); display: flex; align-items: baseline; gap: var(--sp-2); }
.actionbar__next .glyph { color: var(--accent); flex: none; }
.actionbar__pair { display: grid; grid-template-columns: 1fr; gap: var(--sp-2); }
.actionbar__pair--two { grid-template-columns: auto 1fr; }

/* Connection strip. Sits above everything, one line, never covers content. */
.conn {
  position: sticky; inset-block-start: 0; z-index: var(--z-conn);
  display: none; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  font-size: var(--fs-sm); font-weight: var(--fw-semi);
  background: var(--amber-fill); color: var(--amber-fill-ink);
}
.conn[data-state="offline"], .conn[data-state="queued"], .conn[data-state="syncing"] { display: flex; }
.conn[data-state="syncing"] { background: var(--accent); color: var(--accent-ink); }
.conn__count { margin-inline-start: auto; font-family: var(--font-mono); }

/* ================================================ 4. BUTTONS ============ */
.btn {
  --btn-bg: var(--surface-card); --btn-ink: var(--ink-1); --btn-line: var(--line-3);
  min-height: var(--tap); min-width: var(--tap);
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  padding-inline: var(--sp-5); padding-block: var(--sp-3);
  font-size: var(--fs-md); font-weight: var(--fw-bold); text-align: center;
  background: var(--btn-bg); color: var(--btn-ink);
  border: var(--border-input) solid var(--btn-line); border-radius: var(--r-sm);
  text-decoration: none; cursor: pointer;
  transition: filter var(--dur-fast) var(--ease);
}
.btn:hover { filter: brightness(.96); }
.btn:active { filter: brightness(.92); }
.btn[disabled], .btn[aria-disabled="true"] { opacity: .5; cursor: not-allowed; filter: none; }
.btn--block { width: 100%; }
/* The single primary action. Pill, because the brand's pill IS the CTA shape. */
.btn--primary { --btn-bg: var(--accent); --btn-ink: var(--accent-ink); --btn-line: var(--accent); border-radius: var(--r-pill); }
/* Amber primary: fill only, graphite text. Reserved for "commit the count",
   "post the receipt" — the irreversible one. */
.btn--commit  { --btn-bg: var(--amber-fill); --btn-ink: var(--amber-fill-ink); --btn-line: var(--amber-fill); border-radius: var(--r-pill); }
.btn--ghost   { --btn-bg: transparent; --btn-ink: var(--ink-1); --btn-line: var(--line-3); }
/* Quiet = a text action. Underlined, because an unadorned label in a form
   reads as a heading and nobody taps it. */
.btn--quiet   { --btn-bg: transparent; --btn-ink: var(--ink-link); --btn-line: transparent; text-decoration: underline; text-underline-offset: 3px; padding-inline: var(--sp-2); }
.btn--danger  { --btn-bg: var(--st-bad-solid); --btn-ink: #FFF; --btn-line: var(--st-bad-solid); }
.btn--onchrome{ --btn-bg: transparent; --btn-ink: var(--ink-on-chrome); --btn-line: var(--line-chrome); }
.btn--xl { min-height: var(--tap-xl); font-size: var(--fs-lg); }
.btn--sm { min-height: var(--tap); padding-inline: var(--sp-4); font-size: var(--fs-sm); }
.btn__sub { display: block; font-size: var(--fs-2xs); font-weight: var(--fw-medium); opacity: .85; }
/* A BUSY BUTTON KEEPS ITS WORD. This rule used to be `color: transparent`,
   which is a 1:1 contrast ratio and a bare spinner — the exact thing three
   separate comments in this codebase forbid, including the one printed on the
   gallery page directly above the specimen. A spinner with no word is
   indistinguishable from a frozen screen, and the response to a frozen screen
   is to press the button again.

   So the label stays put and the spinner sits beside it, in the space the
   padding reserves. `padding-inline-start` rather than a flex gap because the
   spinner is positioned: reserving the room keeps the label from jumping
   sideways the instant it is pressed. */
.btn.is-busy { position: relative; padding-inline-start: calc(var(--sp-5) + 1.7em); }
.btn.is-busy::after {
  content: ""; position: absolute; inset-block-start: 50%; inset-inline-start: var(--sp-4);
  width: 1.1em; height: 1.1em; margin-block-start: -.55em;
  border: 2px solid currentColor; border-block-start-color: transparent; border-radius: 50%;
  color: var(--btn-ink); animation: spin .7s linear infinite;
}
/* A person who has asked for less motion still needs to see that something is
   happening; they just do not need it to spin. */
@media (prefers-reduced-motion: reduce) {
  .btn.is-busy::after { animation: none; border-block-start-color: currentColor; opacity: .55; }
}
@keyframes spin { to { transform: rotate(1turn); } }

/* Segmented control — the mode switch (count one-at-a-time vs list, etc.) */
.seg { display: inline-flex; background: var(--surface-sunken); border: var(--border-hair) solid var(--line-2); border-radius: var(--r-sm); padding: 3px; gap: 3px; }
.seg__btn {
  min-height: calc(var(--tap) - 8px); padding-inline: var(--sp-4);
  border: 0; border-radius: calc(var(--r-sm) - 3px); background: transparent;
  font-size: var(--fs-sm); font-weight: var(--fw-semi); color: var(--ink-2); cursor: pointer;
}
.seg__btn[aria-pressed="true"], .seg__btn[aria-selected="true"] { background: var(--surface-card); color: var(--ink-1); box-shadow: var(--elev-1); }
/* A segmented control made of REAL RADIO BUTTONS, for a form. `aria-pressed` is
   for a button; a choice that gets posted is an <input>, so that it survives
   with JavaScript off and arrives in the request body like everything else.
   The selected state is a sibling selector on `:checked` rather than `:has()`,
   which is not available on the Android WebViews this runs on. */
.seg__btn--radio { padding: 0; position: relative; }
.seg__btn--radio input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.seg__btn--radio > span { display: grid; place-items: center; min-height: calc(var(--tap) - 8px); padding-inline: var(--sp-4); border-radius: calc(var(--r-sm) - 3px); }
.seg__btn--radio input:checked + span { background: var(--surface-card); color: var(--ink-1); box-shadow: var(--elev-1); }
.seg__btn--radio input:focus-visible + span { outline: var(--focus-width) solid var(--focus-ring); outline-offset: var(--focus-offset); }

/* ================================================ 5. CARDS + GROUPS ===== */
.card {
  background: var(--surface-card);
  border: var(--border-hair) solid var(--line-1);
  border-radius: var(--r-md);
  box-shadow: var(--elev-1);
}
.card__pad { padding: var(--sp-4); }
.card__head { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-4); border-block-end: var(--border-hair) solid var(--line-1); }
.card__head h3 { font-size: var(--fs-md); font-weight: var(--fw-bold); }
.card__foot { padding: var(--sp-3) var(--sp-4); border-block-start: var(--border-hair) solid var(--line-1); background: var(--surface-sunken); border-end-start-radius: var(--r-md); border-end-end-radius: var(--r-md); }
.eyebrow { font-size: var(--fs-micro); font-weight: var(--fw-bold); letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--accent); display: inline-flex; align-items: center; gap: var(--sp-2); }
.eyebrow::before { content: ""; inline-size: 0; block-size: 0; border-inline-start: 7px solid currentColor; border-block: 5px solid transparent; flex: none; }
[dir="rtl"] .eyebrow::before { transform: scaleX(-1); }

.group { background: var(--surface-card); border: var(--border-hair) solid var(--line-1); border-radius: var(--r-md); overflow: clip; }
.group > * + * { border-block-start: var(--border-hair) solid var(--line-1); }
.group__label { padding: var(--sp-2) var(--sp-4); background: var(--surface-sunken); font-size: var(--fs-xs); font-weight: var(--fw-bold); color: var(--ink-2); position: sticky; inset-block-start: var(--appbar-h); z-index: var(--z-sticky); }

/* ================================================ 6. BADGES + CHIPS ===== */
/* State badge = colour + glyph + word. Never any one of the three alone. */
.badge {
  --bg: var(--st-idle-quiet); --fg: var(--st-idle-ink); --dot: var(--st-idle);
  display: inline-flex; align-items: center; gap: .4em;
  padding: .2em .55em; border-radius: var(--r-xs);
  background: var(--bg); color: var(--fg);
  font-size: var(--fs-2xs); font-weight: var(--fw-bold); line-height: 1.5;
  white-space: nowrap;
}
/* The glyph is a server-rendered inline SVG, for the same reason the charts
   are: a character is a request to a font we do not control. ✋ U+270B carries
   Emoji_Presentation=Yes and arrives as a full-colour hand with a skin tone;
   ⛒ and ⏾ are tofu on older Android WebViews; and ◌ is literally the dotted
   circle a font draws when it cannot place an Arabic diacritic, which on a
   screen full of Arabic reads as damage rather than as a state.
   `1em` so it scales with the OS text-size setting like everything else. */
.badge__glyph { inline-size: 1em; block-size: 1em; flex: none; }
.badge__glyph svg { inline-size: 100%; block-size: 100%; display: block; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.badge--ok    { --bg: var(--st-ok-quiet);   --fg: var(--st-ok-ink);   --dot: var(--st-ok); }
.badge--warn  { --bg: var(--st-warn-quiet); --fg: var(--st-warn-ink); --dot: var(--st-warn); }
.badge--bad   { --bg: var(--st-bad-quiet);  --fg: var(--st-bad-ink);  --dot: var(--st-bad); }
.badge--hold  { --bg: var(--st-hold-quiet); --fg: var(--st-hold-ink); --dot: var(--st-hold); }
.badge--idle  { --bg: var(--st-idle-quiet); --fg: var(--st-idle-ink); --dot: var(--st-idle); }
/* Never counted. A FILLED chip, where dormant is a quiet one — see the token
   comment for why these two had to stop looking alike. */
.badge--unknown { --bg: var(--st-unknown-quiet); --fg: var(--st-unknown-ink); --dot: var(--st-unknown); }
/* Hold and blocked also carry a pattern, so they survive a colour-blind
   viewer and a washed-out screen in sunlight. */
.badge--hold  { background-image: repeating-linear-gradient(135deg, transparent 0 5px, color-mix(in srgb, var(--st-hold) 22%, transparent) 5px 7px); }
.badge--bad   { background-image: repeating-linear-gradient(135deg, transparent 0 6px, color-mix(in srgb, var(--st-bad) 18%, transparent) 6px 8px); }
.badge--solid { --bg: var(--dot); --fg: #FFF; background-image: none; }
.badge--solid.badge--warn { --fg: var(--amber-fill-ink); }

.chip {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  min-height: var(--tap); padding-inline: var(--sp-4);
  border: var(--border-hair) solid var(--line-2); border-radius: var(--r-pill);
  background: var(--surface-card); color: var(--ink-2);
  font-size: var(--fs-sm); font-weight: var(--fw-semi); cursor: pointer;
}
.chip[aria-pressed="true"] { background: var(--accent-quiet); border-color: var(--accent); color: var(--accent-quiet-ink); }
.chip__x { font-size: 1.1em; opacity: .7; }
.chip--static { cursor: default; min-height: auto; padding: .25em .7em; font-size: var(--fs-2xs); }
.chiprow { display: flex; gap: var(--sp-2); overflow-x: auto; padding: var(--sp-2) var(--sp-4); scrollbar-width: none; }
.chiprow::-webkit-scrollbar { display: none; }
.chiprow > * { flex: none; }

/* ================================================================
   7. PRODUCT IDENTITY — .pid
   The most-used element in the app. One anatomy, five contexts.
   Anatomy:  code · name-ar (primary) · name-en (secondary) · pack · category
   Code is LE-04217-3 : dim fixed prefix, bold varying digits, medium pack tail.
   ================================================================ */
.pid { display: grid; gap: 2px; min-width: 0; }
/* The identity block is ALWAYS a grid, in every context. When it was scoped
   to .pid--row only, the picker rendered code + both names + meta as one
   inline paragraph and the bidi runs interleaved. */
.pid__body { display: grid; gap: 2px; min-width: 0; text-align: start; }
/* THE CODE IS NOT A CAPTION. It was --fs-xs (13 px) on a row whose quantity
   was --fs-qty (28 px) — but the quantity is the number you already know, and
   the code is what decides whether the drum in your hand is the right drum.
   Two of the strings on this row are read at arm's length off a label; the
   type scale now says so. */
.pid__code {
  font-family: var(--font-mono); font-size: var(--fs-sm); letter-spacing: .01em;
  font-weight: var(--fw-medium);
  unicode-bidi: isolate; direction: ltr; white-space: nowrap;
}
/* A grid item's box stretches to fill its track, and these three carry
   `direction: ltr` so that their CHARACTERS stay in order — which also makes
   their own `text-align: start` resolve to the LEFT. In an Arabic row that put
   the code, the shelf address and every stat value hard against the far edge,
   half a metre of screen away from the name they belong to. Invisible at 360
   px, where the row is too narrow for anything to drift; obvious at 1440.

   `justify-self` places the BOX against the GRID's start — the right, in
   Arabic — and leaves the characters inside running left to right, which is
   the split that was wanted all along. */
.pid__code, .pid__where, .stat__v { justify-self: start; }
.pid__code-fix  { color: var(--ink-3); font-weight: var(--fw-regular); }  /* LE- */
.pid__code-var  { color: var(--ink-1); font-weight: var(--fw-medium); }   /* 04217 */
.pid__code-pack { color: var(--ink-2); font-weight: var(--fw-regular); }  /* -3 */
.pid__name {
  font-size: var(--fs-md); font-weight: var(--fw-semi); color: var(--ink-1);
  line-height: var(--lh-snug);
  overflow-wrap: anywhere;
}
/* The secondary name is ALWAYS the other language's name — English when the
   UI is Arabic, Arabic when the UI is English. So it must not be forced to
   LTR; it carries dir="auto" in the markup and only isolates. Forcing ltr
   here made the English UI print the English name twice. */
.pid__name-alt {
  font-size: var(--fs-sm); color: var(--ink-3); line-height: 1.3;
  unicode-bidi: isolate; text-align: start;
}
.pid__meta { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; font-size: var(--fs-xs); color: var(--ink-2); }
.pid__meta .sep { color: var(--line-3); }
/* THE PACK SIZE IS PROSE, NOT A CODE, and this rule used to treat it as one.
   `direction: ltr` plus --font-mono was written for Latin strings like
   `500 ml`. But `pack_ar` holds Arabic — «١ كجم», «جالون أمريكي» — and under
   a forced LTR the number and its unit come out in the wrong order, while
   Fira Code, which has no Arabic coverage at all, drops the whole word into
   the system fallback face mid-row.

   `<bdi>` in the markup already isolates the token AND resolves its direction
   from its own content, which is exactly the behaviour wanted: `500 ml` runs
   left to right and «١ كجم» runs right to left, with no heuristic here.
   Tabular figures come from the body rule, so a Latin pack still aligns. */
.pid__pack { unicode-bidi: isolate; }
.pid__cat { color: var(--ink-3); }
/* The variant is what tells two products apart — «فراولة مستوي» against
   «فراولة أخضر». It sits with the name because it IS part of the name, and it
   is its own <bdi> because concatenating it on the server is the defect this
   whole component exists to avoid. */
.pid__variant { color: var(--ink-2); font-weight: var(--fw-medium); }
/* The maker. Restricted-origin data: inside the app only, never in an
   artifact, and only for somebody holding `party.read`. */
.pid__maker { color: var(--ink-2); font-weight: var(--fw-semi); unicode-bidi: isolate; }
.pid__gtin { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--ink-3); unicode-bidi: isolate; direction: ltr; justify-self: start; }
/* Search-match highlight — underline + weight, not a yellow wash (a wash
   fails contrast on cheap screens and reads as a state colour). */
.pid mark { background: none; color: inherit; font-weight: var(--fw-black); text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 2px; text-decoration-color: var(--accent); }

/* 7a. Phone row -------------------------------------------------------- */
.pid--row {
  display: grid; grid-template-columns: 1fr auto; align-items: center;
  gap: var(--sp-3); min-height: var(--tap-lg);
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface-card); text-decoration: none; color: inherit;
  border-inline-start: var(--border-state) solid transparent;
}
.pid--row:hover { background: var(--surface-sunken); }
.pid--row .pid__qty { text-align: end; display: grid; gap: 0; }
.pid__qtyval { font-family: var(--font-mono); font-size: var(--fs-qty); font-weight: var(--fw-medium); line-height: 1; color: var(--ink-1); unicode-bidi: isolate; direction: ltr; }
.pid__qtyunit { font-size: var(--fs-2xs); color: var(--ink-3); font-family: var(--font-mono); unicode-bidi: isolate; direction: ltr; }
/* State markers on a row: 4px inline-start bar + badge. Not a tinted row —
   a tint drops the body-text contrast on a mid-range Android in daylight. */
.pid--row[data-state="ok"]    { border-inline-start-color: transparent; }
.pid--row[data-state="warn"]  { border-inline-start-color: var(--st-warn); }
.pid--row[data-state="bad"]   { border-inline-start-color: var(--st-bad); }
.pid--row[data-state="hold"]  { border-inline-start-color: var(--st-hold); }
.pid--row[data-state="idle"]  { border-inline-start-color: var(--st-idle); border-inline-start-style: dotted; }
.pid--row[data-state="unknown"] { border-inline-start-color: var(--st-unknown); }

/* 7b. Search result ---------------------------------------------------- */
.pid--result {
  display: grid; grid-template-columns: auto 1fr auto; gap: var(--sp-3);
  align-items: start; padding: var(--sp-4);
  min-height: var(--tap-lg); background: var(--surface-card);
  text-decoration: none; color: inherit;
  border-inline-start: var(--border-state) solid transparent;
}
.pid--result .thumb { inline-size: 2.75rem; block-size: 2.75rem; }
.pid--result .pid__meta { margin-block-start: var(--sp-1); }
/* THE SHELF ADDRESS AT THE SIZE IT IS ACTUALLY READ AT. `--fs-addr` (22 px)
   exists in the token layer, is documented as "read from a metre away", and
   was used by exactly one selector. This is the other one: `AM-A-12-3` is the
   string that decides which aisle somebody walks down. */
.pid__where { font-size: var(--fs-addr); font-weight: var(--fw-medium); color: var(--ink-1); font-family: var(--font-mono); line-height: 1.15; unicode-bidi: isolate; direction: ltr; }
.pid--result .pid__where, .pid--row .pid__where { color: var(--ink-1); }

/* 7c. Picker inside a task flow ---------------------------------------- */
.pid--picker {
  display: grid; grid-template-columns: 1fr auto; align-items: center; gap: var(--sp-3);
  min-height: var(--tap-xl); padding: var(--sp-3) var(--sp-4);
  background: var(--surface-card); border: var(--border-input) solid var(--line-2);
  border-radius: var(--r-sm); inline-size: 100%; text-align: start; cursor: pointer;
}
.pid--picker[aria-pressed="true"], .pid--picker.is-chosen { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.pid--picker .pid__pick { display: grid; place-items: center; inline-size: var(--tap); block-size: var(--tap); border-radius: var(--r-pill); border: var(--border-input) solid var(--line-3); color: var(--ink-3); flex: none; }
.pid--picker.is-chosen .pid__pick { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }

/* 7d. Detail header ---------------------------------------------------- */
.pid--header { display: grid; gap: var(--sp-3); padding: var(--sp-5) var(--sp-4); background: var(--surface-card); border-block-end: var(--border-hair) solid var(--line-1); }
.pid--header .pid__top { display: grid; grid-template-columns: auto 1fr; gap: var(--sp-4); align-items: start; }
.pid--header .thumb { inline-size: 4.5rem; block-size: 4.5rem; }
.pid--header .pid__code { font-size: var(--fs-sm); }
.pid--header .pid__name { font-size: var(--fs-xl); font-weight: var(--fw-black); }
.pid--header .pid__name-alt { font-size: var(--fs-md); }
.pid--header .pid__flags { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.pid--header .pid__stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(6.5rem,1fr)); gap: var(--sp-3); border-block-start: var(--border-hair) solid var(--line-1); padding-block-start: var(--sp-3); }
.stat { display: grid; gap: 1px; }
.stat__k { font-size: var(--fs-2xs); color: var(--ink-3); font-weight: var(--fw-semi); }
.stat__v { font-family: var(--font-mono); font-size: var(--fs-lg); font-weight: var(--fw-medium); unicode-bidi: isolate; direction: ltr; }

/* 7e. Printed report row ---------------------------------------------- */
.pid--print {
  display: grid; grid-template-columns: 8ch 1fr 7ch 9ch;
  gap: 0 4mm; align-items: baseline;
  padding-block: 1.6mm; border-block-end: var(--hair, .5pt) solid #000;
  font-size: 9pt; line-height: 1.3; color: #000; background: #FFF;
  break-inside: avoid;
}
.pid--print .pid__code { font-size: 8.5pt; }
.pid--print .pid__code-fix { color: #666; }
.pid--print .pid__code-var { color: #000; font-weight: 700; }
.pid--print .pid__name { font-size: 9.5pt; font-weight: 600; }
.pid--print .pid__name-alt { font-size: 8pt; color: #333; }
.pid--print .pid__qtyval { font-size: 10pt; }
.pid--print .u-end { text-align: end; }

/* Thumb / photo slot */
.thumb { background: var(--surface-sunken); border: var(--border-hair) solid var(--line-2); border-radius: var(--r-sm); display: grid; place-items: center; color: var(--ink-3); font-size: var(--fs-xs); overflow: clip; flex: none; }
.thumb img { inline-size: 100%; block-size: 100%; object-fit: cover; }

/* ================================================ 8. FIELDS + KEYPAD === */
.field { display: grid; gap: var(--sp-2); }
.field__label { font-size: var(--fs-sm); font-weight: var(--fw-bold); color: var(--ink-1); }
.field__hint { font-size: var(--fs-xs); color: var(--ink-3); }
.field__err { font-size: var(--fs-sm); font-weight: var(--fw-semi); color: var(--st-bad-ink); display: flex; gap: var(--sp-2); }
/* min-inline-size:0 is not optional. An <input> has a default intrinsic
   width of about 20 characters (~460 px here). Inside any grid or flex
   track that is its min-content contribution, so it forces the whole page
   wider than 360 px even with inline-size:100% set. This was found by
   measuring element widths against a 360 px reference, not by looking. */
.input, .select, .textarea, .searchbar input, .stepper__in { min-inline-size: 0; }
.input, .select, .textarea {
  min-height: var(--tap-lg); inline-size: 100%;
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface-card); color: var(--ink-1);
  border: var(--border-input) solid var(--line-3); border-radius: var(--r-sm);
  font-size: var(--fs-md);
}
.input::placeholder { color: var(--ink-3); }
/* `direction: ltr` STAYS — a code and a quantity are instruments, and the
   caret and the digit order have to run the way the characters do. What went
   wrong was `text-align: start`, which resolves against the ELEMENT's own
   direction rather than the page's. Under `ltr` that is the left, so in an
   Arabic form the label sat on the right and its value sat 280 px away on the
   left, and every field on the screen read as two unrelated things.

   So the box keeps its LTR interior and is aligned to the PAGE's start. It is
   written as a physical `left`/`right` pair on purpose: this is one of the two
   places in this file where a logical property is the wrong tool, because the
   thing being aligned to is the ancestor's direction and not the element's. */
.input--mono, .input--qty { font-family: var(--font-mono); direction: ltr; text-align: left; }
[dir="rtl"] .input--mono, [dir="rtl"] .input--qty { text-align: right; }
.input--qty, [dir="rtl"] .input--qty { font-size: var(--fs-qty); font-weight: var(--fw-medium); min-height: var(--tap-xl); text-align: center; }
.input[aria-invalid="true"] { border-color: var(--st-bad); box-shadow: inset 0 0 0 1px var(--st-bad); }
.field__row { display: grid; grid-template-columns: 1fr auto; gap: var(--sp-2); align-items: end; }
.searchbar { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: var(--sp-2); background: var(--surface-card); border: var(--border-input) solid var(--line-3); border-radius: var(--r-pill); padding-inline: var(--sp-4); min-height: var(--tap-lg); }
.searchbar input { border: 0; background: none; min-height: var(--tap); inline-size: 100%; }
.searchbar input:focus { outline: none; }
.searchbar:focus-within { border-color: var(--focus-ring); box-shadow: 0 0 0 3px var(--focus-halo); }

/* Numeric keypad. 12 keys, each >= 64px, gaps >= 8px so a gloved thumb
   cannot hit two. Deliberately NOT pills — a grid of pills reads as toys
   and wastes 12% of the width to corner radius. */
/* NOTE: direction:ltr, deliberately, in both languages. A keypad is a
   physical instrument, not prose — every phone dialpad on earth, Arabic
   locales included, puts 1 at the top-left. Mirroring it in RTL breaks
   muscle memory and produces wrong quantities. The digits inside are
   Western (brand rule), so the grid follows the digits. */
.keypad { direction: ltr; display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-2); padding: var(--sp-3); background: var(--surface-sunken); border-radius: var(--r-md); }
.keypad__key {
  min-height: var(--tap-xl); font-family: var(--font-mono);
  font-size: var(--fs-xl); font-weight: var(--fw-medium);
  background: var(--surface-card); color: var(--ink-1);
  border: var(--border-hair) solid var(--line-2); border-radius: var(--r-sm);
  cursor: pointer;
}
.keypad__key:active { background: var(--accent-quiet); }
.keypad__key--fn { font-family: var(--font-ui); font-size: var(--fs-md); font-weight: var(--fw-bold); color: var(--ink-2); }
.keypad__key--del { color: var(--st-bad-ink); }
.readout { display: grid; gap: var(--sp-1); justify-items: center; padding: var(--sp-4); background: var(--surface-card); border: var(--border-hair) solid var(--line-1); border-radius: var(--r-md); }
.readout__val { font-family: var(--font-mono); font-size: var(--fs-qty-lg); font-weight: var(--fw-medium); line-height: 1; unicode-bidi: isolate; direction: ltr; }
.readout__unit { font-size: var(--fs-sm); color: var(--ink-2); font-family: var(--font-mono); unicode-bidi: isolate; direction: ltr; }
.readout__note { font-size: var(--fs-sm); color: var(--ink-2); text-align: center; }

/* Stepper — for list-mode counting and small adjustments */
/* Also LTR in both languages: − sits where the smaller numbers are. Same
   reasoning as the keypad. See DESIGN-DECISIONS §2 (things that stay LTR). */
.stepper { direction: ltr; display: grid; grid-template-columns: var(--tap) 1fr var(--tap); align-items: stretch; border: var(--border-input) solid var(--line-3); border-radius: var(--r-sm); overflow: clip; background: var(--surface-card); }
.stepper__btn { border: 0; background: var(--surface-sunken); font-size: var(--fs-xl); font-weight: var(--fw-bold); color: var(--ink-1); cursor: pointer; min-height: var(--tap); }
.stepper__in { border: 0; text-align: center; font-family: var(--font-mono); font-size: var(--fs-lg); min-height: var(--tap); inline-size: 100%; direction: ltr; }
.stepper__in:focus { outline: none; background: var(--accent-quiet); }

/* Scan affordance */
.scan { display: grid; gap: var(--sp-3); place-items: center; padding: var(--sp-6) var(--sp-4); border: var(--border-input) dashed var(--line-3); border-radius: var(--r-md); background: var(--surface-card); text-align: center; }
.scan__glyph { font-size: 2.5rem; line-height: 1; color: var(--accent); }

/* ================================================ 8a. PICKERS ============
   Where a primary key used to be. See resources/views/components/ivg/
   picker.blade.php for what this replaced and why the plain number box is
   still the thing the server renders.
   ======================================================================== */
.picker { display: grid; gap: var(--sp-2); }
.picker > *, .picker__plain > *, .picker__search > * { min-inline-size: 0; }
.picker__plain, .picker__search { display: grid; gap: var(--sp-2); }
.searchbar__glyph { inline-size: 1.15rem; block-size: 1.15rem; color: var(--ink-3); flex: none; }
.searchbar__glyph svg { inline-size: 100%; block-size: 100%; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
/* The results. A scroller, because six 64 px rows is 384 px and the field
   under it has to stay on the screen — and `overflow` here is also what keeps
   a long product name from widening the page. */
.picker__list { max-block-size: 17rem; overflow: auto; }
.picker__list > li { min-inline-size: 0; }
.picker__list > li + li { border-block-start: var(--border-hair) solid var(--line-1); }
.picker__list .pid--picker { border: 0; border-radius: 0; }
.picker__list > li.is-active .pid--picker { background: var(--accent-quiet); box-shadow: inset 0 0 0 2px var(--accent); }
/* What has been chosen. It is the confirmation and the undo in one. */
.picker__chosen .pid--picker { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.picker__note { font-size: var(--fs-sm); color: var(--ink-2); }

/* ================================================ 9. TASK FLOW ========= */
.task__head { padding: var(--sp-3) var(--sp-4) var(--sp-4); background: var(--surface-chrome-2); color: var(--ink-on-chrome); display: grid; gap: var(--sp-3); }
.task__steps { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-2xs); font-weight: var(--fw-bold); color: var(--ink-on-chrome-3); }
.task__steps .now { color: var(--amber-fill); }
.progress { block-size: 6px; background: rgba(255,255,255,.16); border-radius: var(--r-pill); overflow: clip; }
.progress__bar { block-size: 100%; background: var(--amber-fill); border-radius: var(--r-pill); }
.progress--onlight { background: var(--surface-sunken); }
.progress--onlight .progress__bar { background: var(--accent); }
.task__what { font-size: var(--fs-lg); font-weight: var(--fw-bold); color: var(--ink-on-chrome); }
.task__body { display: grid; gap: var(--sp-4); padding: var(--sp-4); }
.task__section-label { font-size: var(--fs-xs); font-weight: var(--fw-bold); color: var(--ink-2); letter-spacing: .02em; }

/* From → To, used by Move and Repack. The arrow is the only glyph that
   flips; the two cards keep their reading order. */
.fromto { display: grid; gap: var(--sp-2); align-items: stretch; }
.fromto__arrow { display: grid; place-items: center; font-size: 1.5rem; color: var(--accent); min-height: var(--tap); }
[dir="rtl"] .fromto__arrow .glyph-dir { transform: scaleX(-1); }
.slot { display: grid; gap: var(--sp-1); padding: var(--sp-4); background: var(--surface-card); border: var(--border-input) solid var(--line-2); border-radius: var(--r-md); text-align: start; inline-size: 100%; cursor: pointer; min-height: var(--tap-xl); }
.slot__k { font-size: var(--fs-2xs); font-weight: var(--fw-bold); color: var(--ink-3); letter-spacing: var(--tracking-caps); text-transform: uppercase; }
.slot__v { font-family: var(--font-mono); font-size: var(--fs-addr); font-weight: var(--fw-medium); color: var(--ink-1); unicode-bidi: isolate; direction: ltr; }
.slot__sub { font-size: var(--fs-sm); color: var(--ink-2); }
.slot--empty .slot__v { color: var(--ink-3); font-family: var(--font-ui); font-size: var(--fs-md); }
.slot--from { border-inline-start: var(--border-state) solid var(--ink-3); }
.slot--to   { border-inline-start: var(--border-state) solid var(--accent); }

/* Confirmation sentence — plain language, with isolated tokens */
.confirm { padding: var(--sp-4); background: var(--accent-quiet); border: var(--border-hair) solid var(--accent); border-radius: var(--r-md); color: var(--accent-quiet-ink); font-size: var(--fs-md); line-height: var(--lh-body); }
.confirm b { font-family: var(--font-mono); font-weight: var(--fw-medium); }
.confirm--hold { background: var(--st-hold-quiet); border-color: var(--st-hold); color: var(--st-hold-ink); }
.confirm--warn { background: var(--st-warn-quiet); border-color: var(--st-warn); color: var(--st-warn-ink); }

/* Repack arithmetic ledger */
.calc { display: grid; gap: var(--sp-2); padding: var(--sp-4); background: var(--surface-sunken); border-radius: var(--r-md); font-family: var(--font-mono); font-size: var(--fs-sm); }
.calc__line { display: grid; grid-template-columns: 1fr auto; gap: var(--sp-3); }
.calc__line span:first-child { font-family: var(--font-ui); }
.calc__line--total { border-block-start: var(--border-hair) solid var(--line-3); padding-block-start: var(--sp-2); font-weight: var(--fw-bold); font-size: var(--fs-md); }
.calc__line--rest { color: var(--st-warn-ink); font-weight: var(--fw-bold); }

/* Count session header — sticky, always visible, three numbers */
.countbar { position: sticky; inset-block-start: var(--appbar-h); z-index: var(--z-sticky); background: var(--surface-chrome-2); color: var(--ink-on-chrome); padding: var(--sp-3) var(--sp-4); display: grid; gap: var(--sp-2); }
.countbar__nums { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: var(--sp-3); }
.countbar__n { display: grid; gap: 0; }
.countbar__n b { font-family: var(--font-mono); font-size: var(--fs-lg); font-weight: var(--fw-medium); line-height: 1.1; unicode-bidi: isolate; direction: ltr; }
.countbar__n span { font-size: var(--fs-2xs); color: var(--ink-on-chrome-3); }
.countbar__n--over b { color: var(--amber-fill); }
.countbar__n--short b { color: var(--cherry-300); }
.countbar__saved { font-size: var(--fs-2xs); color: var(--ink-on-chrome-3); display: flex; gap: var(--sp-2); align-items: center; }

/* Blind-count reveal */
.blind { display: grid; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); background: var(--surface-sunken); border-radius: var(--r-sm); }
.blind__masked { font-family: var(--font-mono); color: var(--ink-3); letter-spacing: .2em; }
.variance { display: inline-flex; align-items: baseline; gap: .3em; font-family: var(--font-mono); font-weight: var(--fw-bold); unicode-bidi: isolate; direction: ltr; }
.variance--over  { color: var(--st-warn-ink); }
.variance--short { color: var(--st-bad-ink); }
.variance--nil   { color: var(--st-ok-ink); }

/* ================================================ 10. SHEETS + MODALS == */
.sheet { position: fixed; inset: 0; z-index: var(--z-sheet); display: grid; align-items: end; }
.sheet[hidden] { display: none; }
.sheet__scrim { position: absolute; inset: 0; background: rgba(8,25,26,.55); border: 0; }
.sheet__panel { position: relative; background: var(--surface-raised); border-start-start-radius: var(--r-lg); border-start-end-radius: var(--r-lg); box-shadow: var(--elev-sheet); max-block-size: 88dvh; display: grid; grid-template-rows: auto 1fr auto; padding-block-end: env(safe-area-inset-bottom); }
.sheet__grab { inline-size: 2.5rem; block-size: 4px; background: var(--line-3); border-radius: var(--r-pill); margin: var(--sp-3) auto 0; }
.sheet__head { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); border-block-end: var(--border-hair) solid var(--line-1); }
.sheet__head h2 { font-size: var(--fs-lg); }
.sheet__body { overflow: auto; padding: var(--sp-4); display: grid; gap: var(--sp-3); }
.sheet__foot { padding: var(--sp-3) var(--sp-4); border-block-start: var(--border-hair) solid var(--line-1); display: grid; gap: var(--sp-2); }
@media (min-width: 900px) {
  .sheet { place-items: center; }
  .sheet__panel { inline-size: min(34rem, 92vw); border-radius: var(--r-lg); max-block-size: 82dvh; }
  .sheet__grab { display: none; }
}

/* ================================================ 11. DATA TABLES ====== */
/* < 900px: record list. >= 900px: real table. Never a horizontally
   scrolling table by default — that gesture fails with gloves. */
.dt { display: grid; gap: var(--sp-3); }
.dt__toolbar { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.dt__count { font-size: var(--fs-sm); color: var(--ink-2); font-family: var(--font-mono); }
/* overflow lives on the base rule, not only in the desktop media query:
   this wrapper is also used below 900 px (with display:block inline) to
   host reference tables, and its cells are nowrap. */
.dt__table-wrap { display: none; overflow: auto; max-inline-size: 100%; }
.dt__list { display: grid; }
.dt__list > * + * { border-block-start: var(--border-hair) solid var(--line-1); }

/* The compact record row: identity + the fields this user pinned. */
.rec { display: grid; grid-template-columns: 1fr auto; gap: var(--sp-3); align-items: center; padding: var(--sp-3) var(--sp-4); background: var(--surface-card); border-inline-start: var(--border-state) solid transparent; text-decoration: none; color: inherit; min-height: var(--tap-lg); }
.rec[data-state="warn"] { border-inline-start-color: var(--st-warn); }
.rec[data-state="bad"]  { border-inline-start-color: var(--st-bad); }
.rec[data-state="hold"] { border-inline-start-color: var(--st-hold); }
.rec[data-state="idle"] { border-inline-start-color: var(--st-idle); border-inline-start-style: dotted; }
.rec[data-state="unknown"] { border-inline-start-color: var(--st-unknown); }
.rec__pins { display: flex; gap: var(--sp-3); flex-wrap: wrap; margin-block-start: var(--sp-1); font-size: var(--fs-xs); }
.rec__pin { display: inline-flex; gap: .35em; align-items: baseline; }
.rec__pin i { font-style: normal; color: var(--ink-3); }
.rec__pin b { font-family: var(--font-mono); font-weight: var(--fw-medium); color: var(--ink-1); unicode-bidi: isolate; direction: ltr; }

@media (min-width: 900px) {
  /* Scoped to a direct child of .dt on purpose. A record list used anywhere
     else — inside a dashboard card, say — has no table alternative to swap
     to, so it must keep rendering at desktop widths. */
  .dt > .dt__list { display: none; }
  .dt__table-wrap { display: block; overflow: auto; border: var(--border-hair) solid var(--line-1); border-radius: var(--r-md); background: var(--surface-card); max-block-size: 34rem; }
}
.tbl { inline-size: 100%; border-collapse: separate; border-spacing: 0; font-size: var(--fs-sm); }
.tbl th, .tbl td { padding: var(--sp-3); text-align: start; border-block-end: var(--border-hair) solid var(--line-1); white-space: nowrap; }
.tbl thead th { position: sticky; inset-block-start: 0; z-index: 2; background: var(--surface-sunken); font-size: var(--fs-2xs); font-weight: var(--fw-bold); color: var(--ink-2); text-transform: uppercase; letter-spacing: var(--tracking-caps); border-block-end: var(--border-input) solid var(--line-2); }
.tbl thead th button { display: inline-flex; align-items: center; gap: var(--sp-1); background: none; border: 0; font: inherit; color: inherit; cursor: pointer; min-height: var(--tap); text-transform: inherit; letter-spacing: inherit; }
.tbl [data-col="sticky"] { position: sticky; inset-inline-start: 0; z-index: 1; background: var(--surface-card); border-inline-end: var(--border-input) solid var(--line-2); }
.tbl thead [data-col="sticky"] { z-index: 3; background: var(--surface-sunken); }
/* Numeric cells are ALWAYS isolated LTR. Without this, a cell reading
   "50 × 30 mm ≈ 400 × 240 dots" reorders inside an Arabic table and the
   units migrate to the wrong end. Found by rendering, not by reasoning. */
.tbl td.num, .tbl th.num { text-align: end; font-family: var(--font-mono); unicode-bidi: isolate; direction: ltr; }
.tbl th.num { text-align: end; }
[dir="rtl"] .tbl td.num, [dir="rtl"] .tbl th.num { text-align: left; }
.tbl tbody tr:nth-child(even) td { background: var(--surface-sunken); }
.tbl tbody tr:nth-child(even) [data-col="sticky"] { background: var(--surface-sunken); }
.tbl tbody tr:hover td { background: var(--accent-quiet); }
.tbl__grouphead td { background: var(--surface-chrome-2); color: var(--ink-on-chrome); font-weight: var(--fw-bold); font-size: var(--fs-xs); position: sticky; inset-block-start: 2.4rem; z-index: 2; }
.tbl--dense th, .tbl--dense td { padding: var(--sp-2) var(--sp-3); }

/* Column picker rows */
.colpick { display: grid; gap: var(--sp-1); }
.colpick__row { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: var(--sp-3); min-height: var(--tap); padding-inline: var(--sp-2); border-radius: var(--r-sm); }
.colpick__row:hover { background: var(--surface-sunken); }
.colpick__row input { inline-size: 1.35rem; block-size: 1.35rem; accent-color: var(--accent); }
.colpick__pin { font-size: var(--fs-2xs); color: var(--ink-3); }

/* ================================================ 12. CHARTS =========== */
/* Server-rendered inline SVG. No runtime. Same renderer feeds screen, PDF
   and print. Always paired with a real <table> beneath. */
.chart { display: grid; gap: var(--sp-3); }
.chart__head { display: flex; align-items: baseline; gap: var(--sp-3); flex-wrap: wrap; }
.chart__title { font-size: var(--fs-md); font-weight: var(--fw-bold); }
.chart__sub { font-size: var(--fs-xs); color: var(--ink-3); }
.chart__plot { inline-size: 100%; block-size: auto; background: var(--chart-plot); border: var(--border-hair) solid var(--line-1); border-radius: var(--r-sm); overflow: visible; }
.chart svg text { font-family: var(--font-ui); font-size: 11px; fill: var(--chart-axis); }
.chart svg .ax-num { font-family: var(--font-mono); direction: ltr; unicode-bidi: isolate; }
.chart svg .grid { stroke: var(--chart-grid); stroke-width: 1; }
.chart svg .axis { stroke: var(--chart-axis); stroke-width: 1.5; }
.chart svg .s1 { fill: var(--chart-1); stroke: var(--chart-1); }
.chart svg .s2 { fill: var(--chart-2); stroke: var(--chart-2); }
.chart svg .s3 { fill: var(--chart-3); stroke: var(--chart-3); }
.chart svg .s4 { fill: var(--chart-4); stroke: var(--chart-4); }
.chart svg .s5 { fill: var(--chart-5); stroke: var(--chart-5); }
.chart svg .s6 { fill: var(--chart-6); stroke: var(--chart-6); }
.chart svg .line { fill: none; stroke-width: 2.5; stroke-linejoin: round; stroke-linecap: round; }
.chart svg .thresh { stroke: var(--st-bad); stroke-width: 2; stroke-dasharray: 5 4; }
.chart__legend { display: flex; flex-wrap: wrap; gap: var(--sp-3); font-size: var(--fs-xs); }
.chart__legend b { display: inline-flex; align-items: center; gap: var(--sp-2); font-weight: var(--fw-semi); color: var(--ink-2); }
.chart__legend i { inline-size: .85rem; block-size: .85rem; border-radius: 3px; flex: none; }
.chart__data { font-size: var(--fs-xs); overflow-x: auto; max-inline-size: 100%; }
.chart__data summary { min-height: var(--tap); display: flex; align-items: center; gap: var(--sp-2); cursor: pointer; font-weight: var(--fw-semi); color: var(--ink-link); }
/* A sparkline lives outside .chart, so it has to carry its own stroke rules —
   without fill:none the polyline paints as a solid blob. */
.spark { block-size: 1.75rem; inline-size: 5rem; overflow: visible; }
.spark polyline, .spark path { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.spark .s1 { stroke: var(--chart-1); }
.spark .s2 { stroke: var(--chart-2); }
.spark .s5 { stroke: var(--chart-5); }
/* RTL: mirror the whole plot frame, then un-mirror the glyphs so text and
   numbers stay legible. Time therefore runs oldest-at-the-start = right. */
[dir="rtl"] .chart__plot { transform: scaleX(-1); }
[dir="rtl"] .chart__plot text,
[dir="rtl"] .chart__plot .no-flip { transform: scaleX(-1); transform-box: fill-box; transform-origin: center; }

/* KPI tile */
.kpi { display: grid; gap: var(--sp-1); padding: var(--sp-4); }
.kpi__k { font-size: var(--fs-xs); color: var(--ink-2); font-weight: var(--fw-semi); }
.kpi__v { font-family: var(--font-mono); font-size: var(--fs-2xl); font-weight: var(--fw-medium); line-height: 1; unicode-bidi: isolate; direction: ltr; }
.kpi__d { font-size: var(--fs-xs); display: flex; gap: var(--sp-2); align-items: center; }

/* ================================================ 13. DOCUMENTS ======== */
.docstrip { display: flex; gap: var(--sp-3); overflow-x: auto; padding: var(--sp-1) var(--sp-4) var(--sp-3); scrollbar-width: thin; }
.docstrip > * { flex: none; }
.doccard { inline-size: 6.5rem; display: grid; gap: var(--sp-2); text-decoration: none; color: inherit; }
.doccard__thumb { aspect-ratio: 3/4; background: var(--surface-sunken); border: var(--border-hair) solid var(--line-2); border-radius: var(--r-sm); display: grid; place-items: center; position: relative; overflow: clip; }
.doccard__thumb img { inline-size: 100%; block-size: 100%; object-fit: cover; }
.doccard__type { position: absolute; inset-block-start: var(--sp-1); inset-inline-start: var(--sp-1); }
.doccard__pages { position: absolute; inset-block-end: var(--sp-1); inset-inline-end: var(--sp-1); font-family: var(--font-mono); font-size: var(--fs-micro); background: rgba(8,25,26,.78); color: #FFF; padding: 1px 5px; border-radius: 3px; unicode-bidi: isolate; direction: ltr; }
.doccard__name { font-size: var(--fs-2xs); color: var(--ink-2); line-height: 1.3; }
.doccard--add .doccard__thumb { border-style: dashed; color: var(--accent); font-size: 1.75rem; }
.doccard__offline { position: absolute; inset-block-end: var(--sp-1); inset-inline-start: var(--sp-1); }

/* Viewer: dark backdrop regardless of theme — a document reads best on dark */
.viewer { position: fixed; inset: 0; z-index: var(--z-modal); background: var(--dk-900); color: #FFF; display: grid; grid-template-rows: auto 1fr auto; }
.viewer[hidden] { display: none; }
.viewer__bar { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); background: rgba(0,0,0,.4); }
.viewer__bar h2 { font-size: var(--fs-sm); font-weight: var(--fw-semi); min-width: 0; }
.viewer__bar .u-caps { color: var(--dk-ink-3); }
.viewer__stage { display: grid; place-items: center; overflow: auto; padding: var(--sp-3); }
.viewer__page { background: #FFF; box-shadow: 0 8px 32px rgba(0,0,0,.5); max-inline-size: 100%; }
.viewer__foot { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3) calc(var(--sp-2) + env(safe-area-inset-bottom)); background: rgba(0,0,0,.4); }
.viewer__pages { display: flex; gap: var(--sp-2); overflow-x: auto; flex: 1; }
.viewer__pagethumb { inline-size: 2.6rem; aspect-ratio: 3/4; background: #FFF; border: 2px solid transparent; border-radius: 3px; opacity: .55; cursor: pointer; flex: none; }
.viewer__pagethumb[aria-current="true"] { border-color: var(--amber-fill); opacity: 1; }
.viewer__n { font-family: var(--font-mono); font-size: var(--fs-sm); unicode-bidi: isolate; direction: ltr; white-space: nowrap; }
.viewer__fit { display: flex; gap: var(--sp-1); }

/* ================================================ 14. DASHBOARD ======== */
.dash { display: grid; gap: var(--sp-3); padding: var(--sp-4); grid-template-columns: 1fr; }
.dcard { background: var(--surface-card); border: var(--border-hair) solid var(--line-1); border-radius: var(--r-md); box-shadow: var(--elev-1); display: grid; grid-template-rows: auto 1fr; overflow: clip; }
.dcard__head { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); border-block-end: var(--border-hair) solid var(--line-1); }
.dcard__head h3 { font-size: var(--fs-sm); font-weight: var(--fw-bold); flex: 1; min-width: 0; }
.dcard__body { padding: var(--sp-4); display: grid; gap: var(--sp-3); align-content: start; }
.dcard__body--flush { padding: 0; }
.dcard__handle { display: none; }
.dash.is-editing .dcard { border-style: dashed; border-color: var(--accent); }
.dash.is-editing .dcard__handle { display: flex; gap: var(--sp-1); }
.dash.is-editing .dcard__head { background: var(--accent-quiet); }
.dash__addslot { min-height: 8rem; display: grid; place-items: center; gap: var(--sp-2); border: var(--border-input) dashed var(--line-3); border-radius: var(--r-md); background: transparent; color: var(--ink-2); font-size: var(--fs-md); font-weight: var(--fw-semi); cursor: pointer; }
@media (min-width: 900px) {
  .dash { grid-template-columns: repeat(12, 1fr); gap: var(--sp-4); padding: var(--sp-6); }
  .dcard { grid-column: span 4; }
  .dcard[data-w="3"] { grid-column: span 3; }
  .dcard[data-w="4"] { grid-column: span 4; }
  .dcard[data-w="6"] { grid-column: span 6; }
  .dcard[data-w="8"] { grid-column: span 8; }
  .dcard[data-w="12"]{ grid-column: span 12; }
  .dash__addslot { grid-column: span 4; }
  .dash.is-editing .dcard { cursor: grab; }
}
/* Alert list inside a dashboard card */
.alerts { display: grid; }
.alerts > * + * { border-block-start: var(--border-hair) solid var(--line-1); }
.alert { display: grid; grid-template-columns: auto 1fr auto; gap: var(--sp-3); align-items: center; min-height: var(--tap-lg); padding: var(--sp-3) var(--sp-4); text-decoration: none; color: inherit; border-inline-start: var(--border-state) solid transparent; }
.alert[data-state="warn"] { border-inline-start-color: var(--st-warn); }
.alert[data-state="bad"]  { border-inline-start-color: var(--st-bad); }
.alert[data-state="hold"] { border-inline-start-color: var(--st-hold); }
.alert[data-state="idle"] { border-inline-start-color: var(--st-idle); border-inline-start-style: dotted; }
.alert[data-state="unknown"] { border-inline-start-color: var(--st-unknown); }
.alert__n { font-family: var(--font-mono); font-size: var(--fs-lg); font-weight: var(--fw-bold); min-inline-size: 2ch; text-align: end; unicode-bidi: isolate; direction: ltr; }
.alert__t { font-size: var(--fs-sm); }
.alert__t small { display: block; color: var(--ink-3); font-size: var(--fs-2xs); }
/* Shortcut grid */
.shortcuts { display: grid; grid-template-columns: repeat(auto-fit, minmax(5.5rem, 1fr)); gap: var(--sp-2); }
.shortcut { min-height: var(--tap-xl); display: grid; place-items: center; gap: var(--sp-1); padding: var(--sp-2); background: var(--surface-sunken); border: var(--border-hair) solid var(--line-2); border-radius: var(--r-sm); text-decoration: none; color: var(--ink-1); font-size: var(--fs-2xs); font-weight: var(--fw-semi); text-align: center; }
.shortcut__glyph { font-size: 1.5rem; line-height: 1; }
.shortcut:hover { background: var(--accent-quiet); border-color: var(--accent); }

/* ================================================ 15. STATES =========== */
.empty { display: grid; gap: var(--sp-3); place-items: center; text-align: center; padding: var(--sp-8) var(--sp-5); }
.empty__glyph { font-size: 2.5rem; line-height: 1; color: var(--ink-3); }
.empty h3 { font-size: var(--fs-lg); }
.empty p { font-size: var(--fs-sm); color: var(--ink-2); max-inline-size: 30ch; }
.empty--done .empty__glyph { color: var(--st-ok); }

.skel { background: var(--surface-sunken); border-radius: var(--r-xs); position: relative; overflow: clip; }
.skel::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--surface-card) 60%, transparent), transparent); animation: shim 1.3s infinite; }
@keyframes shim { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
[dir="rtl"] .skel::after { animation-direction: reverse; }
.skel--line { block-size: .8em; }
.skel-row { display: grid; grid-template-columns: 1fr auto; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); border-block-end: var(--border-hair) solid var(--line-1); align-items: center; }

.notice { display: grid; grid-template-columns: auto 1fr; gap: var(--sp-3); padding: var(--sp-4); border-radius: var(--r-md); border: var(--border-hair) solid var(--line-2); background: var(--surface-card); }
.notice__glyph { font-size: 1.25rem; line-height: 1.2; }
.notice h4 { font-size: var(--fs-md); font-weight: var(--fw-bold); margin-block-end: var(--sp-1); }
.notice p { font-size: var(--fs-sm); color: var(--ink-2); }
.notice__acts { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-block-start: var(--sp-3); }
.notice--bad  { background: var(--st-bad-quiet);  border-color: var(--st-bad);  color: var(--st-bad-ink); }
.notice--warn { background: var(--st-warn-quiet); border-color: var(--st-warn); color: var(--st-warn-ink); }
.notice--hold { background: var(--st-hold-quiet); border-color: var(--st-hold); color: var(--st-hold-ink); }
.notice--ok   { background: var(--st-ok-quiet);   border-color: var(--st-ok);   color: var(--st-ok-ink); }
.notice--bad p, .notice--warn p, .notice--hold p, .notice--ok p { color: inherit; opacity: .92; }

.queue { display: grid; }
.queue > * + * { border-block-start: var(--border-hair) solid var(--line-1); }
.qrow { display: grid; grid-template-columns: auto 1fr auto; gap: var(--sp-3); align-items: center; padding: var(--sp-3) var(--sp-4); min-height: var(--tap-lg); }
.qrow__time { font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--ink-3); unicode-bidi: isolate; direction: ltr; }

.toast[hidden], .conn[hidden], [hidden] { display: none !important; }
.toast { position: fixed; inset-block-end: calc(var(--tabbar-h) + var(--sp-4)); inset-inline: var(--sp-4); z-index: var(--z-toast); background: var(--surface-inverse); color: var(--surface-app); padding: var(--sp-3) var(--sp-4); border-radius: var(--r-sm); box-shadow: var(--elev-3); display: flex; gap: var(--sp-3); align-items: center; font-size: var(--fs-sm); font-weight: var(--fw-semi); }
.toast a, .toast button { color: inherit; margin-inline-start: auto; }

/* State precedence ladder, used in docs */
.ladder { display: grid; gap: var(--sp-2); counter-reset: l; }
.ladder li { display: grid; grid-template-columns: auto auto 1fr; gap: var(--sp-3); align-items: center; padding: var(--sp-2) var(--sp-3); background: var(--surface-card); border: var(--border-hair) solid var(--line-1); border-radius: var(--r-sm); font-size: var(--fs-sm); }
.ladder li::before { counter-increment: l; content: counter(l); font-family: var(--font-mono); color: var(--ink-3); }

/* ================================================ 16. DESKTOP SHELL ==== */
@media (min-width: 900px) {
  .app--desk { display: grid; grid-template-columns: 15rem 1fr; grid-template-rows: auto 1fr; }
  .app--desk .appbar { grid-column: 1 / -1; }
  .app--desk .sidenav { display: block; }
  .app--desk .main { padding-block-end: var(--sp-8); }
  .app--desk .tabbar { display: none; }
  .app--desk .actionbar { position: static; box-shadow: none; border: 0; background: transparent; padding-inline: 0; }
}
.sidenav { display: none; background: var(--surface-chrome-2); border-inline-end: var(--border-hair) solid var(--line-chrome); padding: var(--sp-3) var(--sp-2); }
.sidenav__grp { margin-block-end: var(--sp-4); }
.sidenav__lbl { font-size: var(--fs-micro); font-weight: var(--fw-bold); letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--ink-on-chrome-3); padding: var(--sp-2) var(--sp-3); }
.sidenav a { display: flex; align-items: center; gap: var(--sp-3); min-height: var(--tap); padding-inline: var(--sp-3); border-radius: var(--r-sm); color: var(--ink-on-chrome-2); text-decoration: none; font-size: var(--fs-sm); font-weight: var(--fw-semi); }
.sidenav a:hover { background: rgba(255,255,255,.08); color: var(--ink-on-chrome); }
.sidenav a[aria-current="page"] { background: var(--accent); color: var(--accent-ink); }
/* A destination nobody has built. It stays in the list — the list IS the map
   of the application, and a screen that is coming is worth knowing about — but
   it is not a link, because a link to '#' is a tap that does nothing. */
.sidenav__unbuilt { display: flex; align-items: center; gap: var(--sp-3); min-height: var(--tap); padding-inline: var(--sp-3); font-size: var(--fs-sm); font-weight: var(--fw-semi); color: var(--ink-on-chrome-3); opacity: .6; cursor: default; }
.sidenav__soon { margin-inline-start: auto; font-size: var(--fs-micro); font-weight: var(--fw-bold); letter-spacing: var(--tracking-caps); text-transform: uppercase; }
.sidenav__grp > * { min-inline-size: 0; }
/* The same list inside the phone's "everything else" sheet: light surface,
   always visible, and it never becomes the desktop rail. */
.sidenav--onlight { display: block; background: transparent; border: 0; padding: 0; }
.sidenav--onlight .sidenav__lbl { color: var(--ink-3); }
.sidenav--onlight a { color: var(--ink-1); min-height: var(--tap-lg); }
.sidenav--onlight a:hover { background: var(--surface-sunken); color: var(--ink-1); }
.sidenav--onlight .sidenav__unbuilt { color: var(--ink-3); }

/* ================================================================
   18. THE MINIMUM-SIZE RULE — new here, and load-bearing
   A grid/flex item's automatic minimum size is min-content, so one nowrap
   table cell or one <input> at its default ~20ch intrinsic width pushes the
   whole document past 360 px. Nothing on screen looks wrong; the page just
   scrolls sideways. The design pass hit this four separate times and patched
   four selectors. This states it once for every layout child.

   Chrome zeroes the automatic minimum for overflow:hidden/auto/scroll but NOT
   for overflow:clip — which is exactly how `.ctx` inflated a page to 562 px
   while looking perfectly contained. So `overflow:clip` is not a substitute
   for this rule.

   Opt back out where intrinsic width IS the point (a table that should scroll
   inside its own frame, a sticky first column).
   ================================================================ */
.u-stack > *, .u-row-flex > *,
.card__head > *, .card__pad > *, .card__foot > *,
.dash > *, .dcard > *, .dcard__head > *, .dcard__body > *,
.group > *, .alerts > *, .queue > *,
.sec > *, .grid2 > *, .grid3 > *,
.pid > *, .pid--row > *, .pid--result > *, .pid--picker > *, .pid--header > *,
.rec > *, .alert > *, .qrow > *, .notice > *, .colpick__row > *,
.appbar > *, .sheet__head > *, .sheet__body > *, .sheet__foot > *,
.actionbar > *, .actionbar__pair > *, .field__row > *, .searchbar > *,
.task__body > *, .fromto > *, .calc__line > *, .countbar__nums > * {
  min-inline-size: 0;
}
/* The opt-outs. Each of these wants its intrinsic width and owns a scroller. */
.dt__table-wrap, .chart__data, .chiprow, .docstrip, .viewer__pages,
.tbl [data-col="sticky"] { min-inline-size: auto; }

/* A BUTTON IS NOT A LAYOUT CHILD, and this was found by looking rather than by
   measuring. With the rule above applied to `.card__head > *`, a card header
   carrying a title and an action shrank the action to nothing: "Send them all"
   wrapped onto three lines inside a pill about forty pixels wide. It escaped
   nothing, so the harness was right not to report it, and it was unusable.

   A button's minimum size is its content — that is what makes it a target. So
   buttons opt back out, and a card header wraps onto a second line instead of
   crushing the thing you are meant to press. `btn--block` keeps `width: 100%`
   and is unaffected either way.

   AND THE SAME ARGUMENT, MISSED THE FIRST TIME, FOR EVERY OTHER TARGET.
   `.appbar > *` and `.sheet__head > *` are in the list above, and `.iconbtn`
   declares `min-width: var(--tap)` at exactly the same specificity — one class
   each — so the later rule won and the app bar's targets collapsed to their
   content. Measured on the real screens: back arrow 18x48 and 16x48, theme
   button 32x48, language toggle 57x40. Every one of them under the 48 px floor
   this design system states as a rule, in the one bar that is on every screen.

   A target's minimum size is not a layout question. So the app bar's targets
   opt back out here — and `.iconbtn` opts out to `var(--tap)` and NOT to
   `auto`, which is the trap. `auto` resolves to min-content, and the content of
   an icon button is one 20 px glyph: it beats `.appbar > *` and then hands back
   a 32 px target, which is the same defect with a different number. Measured
   after the first attempt at this fix: theme button 32x48. The floor has to be
   the floor, stated after the rule that removed it.

   `.btn` keeps `auto` — its content is a word, which is wider than 48 px — and
   the app bar's title, which is genuinely the flexible thing in that row, keeps
   `min-inline-size: 0` and does the shrinking, as §18a already intended. */
.btn, .who__btn, .langtoggle { min-inline-size: auto; }
.iconbtn { min-inline-size: var(--tap); min-block-size: var(--tap); }
.card__head, .dcard__head { flex-wrap: wrap; }
.card__head .btn, .dcard__head .btn { flex: none; }

/* 18a. THE APP BAR, WHICH THE HARNESS CAUGHT ---------------------------------
   The first run of the overflow harness reported the English `offline` page at
   366 px against a 360 px viewport, with the language toggle 6 px past the
   edge. `min-inline-size: 0` was already on `.appbar__title` and was not
   enough: a flex item can shrink to zero and still contribute the width of its
   longest UNBREAKABLE word, and an English subtitle full of ten-letter words
   contributes about a hundred pixels of it.

   The consequence is what makes this worth fixing rather than nudging. What
   gets pushed off the end is not decoration — it is the language toggle and
   the theme button. Somebody holding a phone in the wrong language, in a dark
   aisle, would have had both of the controls that fix that pushed off the
   screen by a sentence that was only ever context.

   So: the subtitle is a desktop affordance and it goes away when the bar is
   narrow, and the title itself is allowed to break anywhere so its min-content
   can never exceed the space available. Two rules, and the second one holds
   even for a title nobody has written yet. */
.appbar__title { overflow-wrap: anywhere; }
@media (max-width: 559px) {
  .appbar__title small { display: none; }
}

/* 18b. A BUTTON WITH A SECOND LINE ------------------------------------------
   `.btn` is a flex row, so a `.btn__sub` set to `display: block` still lands
   BESIDE the label rather than under it. On a 64 px action that reads as two
   competing labels: "Load today's work onto this phone   5 orders · 34 lines".
   A button carrying a sub-line is a column. */
.btn--stacked { flex-direction: column; gap: 2px; }
.btn--stacked .btn__sub { margin-block-start: 1px; }

/* 18c. THE QUEUE ROW AT 360 px ----------------------------------------------
   `.qrow` is `auto 1fr auto`, and `.badge` is `white-space: nowrap`, so a badge
   reading "This one needs a connection." claimed the whole first track and the
   title collapsed to one character per line. Unreadable, and invisible to an
   overflow check because nothing escaped anything — it was found by looking.

   Below 560 the badge and the timestamp take the first line and the title takes
   the second, which is the reading order anyway: what state it is in, when, and
   then what it is. */
@media (max-width: 559px) {
  .qrow {
    grid-template-columns: 1fr auto;
    grid-template-areas: "badge time" "body body";
    row-gap: var(--sp-2);
  }
  .qrow > :first-child { grid-area: badge; justify-self: start; }
  .qrow > .qrow__time { grid-area: time; }
  .qrow > :nth-child(2) { grid-area: body; }
}

/* 18d. The gallery's own page list wraps rather than scrolling once there is
   room for it to wrap. A chip cut in half at the edge of a 1440 px window
   reads as a rendering fault even though it is a live scroller. */
@media (min-width: 900px) {
  .chiprow--wrap { flex-wrap: wrap; overflow-x: visible; }
}

/* ================================================================
   19. SECTION SCAFFOLDING
   Every design screen carried these three rules inline in a <style> block.
   They are layout, not decoration, so they belong in the component layer
   where the overflow harness can hold them to the same standard.
   ================================================================ */
.sec { padding: var(--sp-6) var(--sp-4) 0; max-inline-size: var(--w-content); margin-inline: auto; }
.sec > h2 { font-size: var(--fs-lg); margin-block-end: var(--sp-1); }
.sec > .lead { font-size: var(--fs-sm); color: var(--ink-2); margin-block-end: var(--sp-4); max-inline-size: 60ch; }
/* A width-constrained block sits at the INLINE-START OF ITS PARENT, which in an
   Arabic page is the right — even when the block itself carries dir="auto" and
   reads left to right. The result at 1440 was a heading hard against the left
   edge and its own lead paragraph hard against the right, which reads as a
   layout fault. `margin-inline-end: auto` resolves against the element's OWN
   direction, so a left-to-right block inside a right-to-left page lines up
   under its heading. Only the gallery's English documentation needs this;
   product copy is always in the page's own language. */
.sec > .lead[dir] { margin-inline-end: auto; }
.sec--tight { padding-block-start: var(--sp-4); }
.grid2, .grid3 { display: grid; gap: var(--sp-3); }
.grid2--top, .grid3--top { align-items: start; }
@media (min-width: 900px) {
  .grid2 { grid-template-columns: 1fr 1fr; }
  .grid3 { grid-template-columns: repeat(3, 1fr); }
}

/* ================================================================
   20. AUTHENTICATION
   The owner said staff mostly use their own devices but CAN log out. So
   this is built for cheap switching rather than for a shared kiosk: the
   people who have signed in on this device stay on the sign-in screen as
   tappable faces, and picking one only asks for the passcode.

   Sizing note: every control here is --tap-xl (64 px), not --tap (48). This
   is the screen most often used with a glove on, one-handed, in a doorway.
   ================================================================ */
.auth { min-block-size: 100dvh; display: grid; grid-template-rows: auto 1fr auto; background: var(--surface-chrome); color: var(--ink-on-chrome); }
.auth__head { display: grid; gap: var(--sp-3); justify-items: center; padding: var(--sp-8) var(--sp-4) var(--sp-6); text-align: center; }
.auth__mark { inline-size: 4rem; }
.auth__name { font-size: var(--fs-xl); font-weight: var(--fw-black); letter-spacing: var(--tracking-tight); }
.auth__name small { display: block; font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--ink-on-chrome-3); letter-spacing: 0; margin-block-start: var(--sp-1); }
.auth__panel {
  background: var(--surface-card); color: var(--ink-1);
  border-start-start-radius: var(--r-lg); border-start-end-radius: var(--r-lg);
  padding: var(--sp-6) var(--sp-4) var(--sp-8);
  display: grid; gap: var(--sp-5); align-content: start;
}
.auth__form { display: grid; gap: var(--sp-4); max-inline-size: 26rem; inline-size: 100%; margin-inline: auto; }
.auth__form > * { min-inline-size: 0; }
.auth__foot { display: flex; align-items: center; justify-content: center; gap: var(--sp-3); flex-wrap: wrap; padding: var(--sp-4); background: var(--surface-card); border-block-start: var(--border-hair) solid var(--line-1); }
.auth .input { min-block-size: var(--tap-xl); font-size: var(--fs-lg); }
/* A passcode is digits. Keep it LTR and monospaced in both languages, same
   reasoning as the keypad: it is an instrument, not prose. */
.auth__code { font-family: var(--font-mono); direction: ltr; text-align: center; letter-spacing: .35em; font-size: var(--fs-xl); }
/* THE AUTHENTICATION SCREEN'S OWN ACTION BAR, and the one place in this file
   where the bar is STICKY rather than FIXED.

   See the note in the auth-frame component for the screen this exists because
   of — /passcode had its submit 112 px below the fold at 360x640, on the one
   screen every new account is forced through. A fixed bar would need the page
   to reserve exactly its own height at the bottom, and that height is not
   knowable from CSS: the first attempt reserved `--actionbar-h` and put the
   language toggle half under the bar, which is the same defect one row down.

   Sticky cannot get it wrong. The bar rides the bottom edge while there is
   content beneath it and comes to rest above the footer when there is not, and
   no number in this file has to match a rendered height. */
.auth--act .actionbar {
  position: sticky; inset-block-end: 0; z-index: var(--z-sticky);
  margin-inline: calc(var(--sp-4) * -1);
  margin-block: var(--sp-2) calc(var(--sp-8) * -1);
}
@media (min-width: 900px) {
  .auth { grid-template-rows: 1fr auto; place-items: center; padding: var(--sp-7); }
  .auth__panel { inline-size: min(32rem, 100%); border-radius: var(--r-lg); box-shadow: var(--elev-3); }
  .auth__head { padding-block-end: var(--sp-7); }
  /* A 64 px bar pinned across a 1440 px window is a phone pattern that has
     escaped — the same reason `.app--desk .actionbar` goes static. */
  .auth--act .actionbar {
    position: static; box-shadow: none; border: 0; background: transparent;
    padding-inline: 0; margin: 0;
  }
}

/* 20a. The user switch — the whole point is that it is one tap deep ------ */
.who { display: grid; gap: var(--sp-2); }
.who__btn {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: var(--sp-4);
  min-block-size: var(--tap-xl); padding: var(--sp-3) var(--sp-4);
  background: var(--surface-card); color: inherit; text-align: start; text-decoration: none;
  border: var(--border-input) solid var(--line-2); border-radius: var(--r-md); cursor: pointer;
}
.who__btn > * { min-inline-size: 0; }
.who__btn:hover { background: var(--surface-sunken); border-color: var(--accent); }
.who__btn[aria-current="true"] { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
/* Initials, not a photograph. Nobody is uploading a headshot to a stock app,
   and an empty avatar ring is worse than two letters. */
.who__face {
  inline-size: 2.75rem; block-size: 2.75rem; border-radius: var(--r-pill);
  display: grid; place-items: center; flex: none;
  background: var(--accent-quiet); color: var(--accent-quiet-ink);
  font-weight: var(--fw-black); font-size: var(--fs-md);
  unicode-bidi: isolate;
}
.who__face--idle { background: var(--st-idle-quiet); color: var(--st-idle-ink); }
.who__name { font-weight: var(--fw-bold); overflow-wrap: anywhere; }
.who__role { display: block; font-size: var(--fs-xs); color: var(--ink-3); font-weight: var(--fw-regular); }
.who__go { color: var(--ink-3); font-size: 1.25rem; flex: none; }
[dir="rtl"] .who__go.glyph-dir { transform: scaleX(-1); }

/* 20b. Forced password change ------------------------------------------- */
/* The rules are shown before the box, not as an error after it. Being told
   what was wrong on the fourth attempt is how people pick `Aa123456`. */
.rules { display: grid; gap: var(--sp-2); font-size: var(--fs-sm); }
.rules li { display: grid; grid-template-columns: 1.5rem 1fr; gap: var(--sp-2); align-items: baseline; color: var(--ink-2); }
.rules li > * { min-inline-size: 0; }
.rules li::before { content: "○"; color: var(--ink-3); font-size: .9em; }
.rules li[data-met="true"] { color: var(--st-ok-ink); }
.rules li[data-met="true"]::before { content: "✓"; color: var(--st-ok); font-weight: var(--fw-black); }

/* ================================================================
   21. LANGUAGE TOGGLE
   Two buttons, both always visible, both always in their OWN language —
   «عربي» is never written "Arabic". Somebody who cannot read the current
   language has to be able to find the way out, and a person looking for
   Arabic is looking for the Arabic word.
   ================================================================ */
.langtoggle { display: inline-flex; background: rgba(255,255,255,.10); border-radius: var(--r-sm); padding: 3px; gap: 3px; }
.langtoggle__btn {
  /* 48 px, not `--tap` minus 8. The pill's own minimum was written on the
     assumption that its padding made up the difference; measured on the real
     app bar it came out 57x40 and 48x40, under the floor this design system
     states as a rule, on the two controls somebody holding a phone in a
     language they cannot read has to be able to hit. */
  min-block-size: var(--tap); min-inline-size: var(--tap);
  padding-inline: var(--sp-3); border: 0; border-radius: calc(var(--r-sm) - 3px);
  background: transparent; color: var(--ink-on-chrome-2);
  font-size: var(--fs-sm); font-weight: var(--fw-bold); cursor: pointer;
  text-decoration: none; display: inline-grid; place-items: center;
}
.langtoggle__btn[aria-current="true"] { background: var(--surface-card); color: var(--ink-1); }
.langtoggle__btn:hover { color: var(--ink-on-chrome); }
.langtoggle__btn[aria-current="true"]:hover { color: var(--ink-1); }
/* Each label carries its own direction so «عربي» and "EN" both read correctly
   whichever way round the page is. */
.langtoggle__btn[lang="ar"] { font-family: var(--font-arabic); direction: rtl; unicode-bidi: isolate; }
.langtoggle__btn[lang="en"] { font-family: var(--font-latin); direction: ltr; unicode-bidi: isolate; }
/* On a light surface (the auth screen's panel, a settings sheet). */
.langtoggle--onlight { background: var(--surface-sunken); border: var(--border-hair) solid var(--line-2); }
.langtoggle--onlight .langtoggle__btn { color: var(--ink-2); }
.langtoggle--onlight .langtoggle__btn[aria-current="true"] { background: var(--surface-card); color: var(--ink-1); box-shadow: var(--elev-1); }

/* ================================================================
   22. THE GALLERY
   The design system rendered as a page in the running application, which is
   also what the overflow harness measures. It is a development surface, not
   a product screen — but it uses the product's own components, so it cannot
   drift from them.
   ================================================================ */
.gal { display: grid; gap: var(--sp-6); padding-block-end: var(--sp-9); }
.gal__note { font-size: var(--fs-xs); color: var(--ink-3); max-inline-size: 68ch; }
.gal__swatches { display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); gap: var(--sp-3); }
.gal__swatches > * { min-inline-size: 0; }
.sw { display: grid; gap: var(--sp-2); }
.sw__chip { block-size: 3.5rem; border-radius: var(--r-sm); border: var(--border-hair) solid var(--line-1); }
.sw__k { font-size: var(--fs-2xs); font-weight: var(--fw-bold); }
/* A hex code draws as `E0A23B#` inside an Arabic paragraph without this. It
   was a real defect in the design's own token page. */
.sw__v { font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--ink-3); unicode-bidi: isolate; direction: ltr; }
.gal__type { display: grid; gap: var(--sp-4); }
.gal__typerow { display: grid; gap: var(--sp-1); padding-block-end: var(--sp-3); border-block-end: var(--border-hair) solid var(--line-1); }
.gal__typerow > * { min-inline-size: 0; }
.gal__typek { font-size: var(--fs-2xs); color: var(--ink-3); font-family: var(--font-mono); unicode-bidi: isolate; direction: ltr; }
@media (min-width: 560px) {
  .gal__typerow { grid-template-columns: 9rem 1fr; align-items: baseline; gap: var(--sp-4); }
}

/* ================================================================
   23. THE VIEW — which one you are in, and where the answer came from

   THERE ARE THREE VIEWS AND NONE OF THEM IS IN THIS FILE. The phone is
   tokens.css §4 and this file's own rules referencing it; the other two are
   view-touch.css and view-desk.css, each linked by the server with the `media`
   attribute that switches it on. tokens.css §4b is the whole rule and the
   reason it is done that way round.

   WHAT IS LEFT HERE IS THE PART A PERSON READS. An automatic choice that is
   applied silently is indistinguishable from a bug — "why are the buttons small
   on this machine" has no answer a warehouse manager can check. So the /screen
   page says, in a sentence, which view is running and whether it was DETECTED
   or CHOSEN, and it says it WITHOUT JAVASCRIPT, because the detection lives in
   a media query and a media query is the only thing that knows.

   THE TWO CONDITIONS BELOW ARE COPIES, AND THAT IS DELIBERATE AND GUARDED.
   They also appear in App\Support\Ui\Screen, which is where the server gets the
   `media` attribute from, and Tests\Feature\Ui\ViewChoiceTest reads this file
   and fails if the two ever differ by a character. The alternative — deriving
   this page's wording from the link that happens to be loaded — cannot work,
   because when a person has FORCED a view the other file is not linked at all
   and the page still has to be able to say what the device looks like. Which is
   the whole point: you can only see that your choice disagrees with the
   detection if both are on the screen.

   EVERY REVEAL BELOW IS `inline`, and there is exactly one display value in
   this section on purpose: each of these elements is a fragment inside a
   sentence, so a block reveal would break the sentence it lives in.
   ================================================================ */
.viewsays { display: none; }

/* WHAT THE DEVICE LOOKS LIKE — the automatic answer, computed on every page
   whether or not it is the one in force. Phone is the base for the same reason
   it is the base everywhere else: it is what a browser too old to parse the
   conditions below lands on, and that is the safe direction. */
.viewsays--found-phone { display: inline; }
@media screen and (pointer: coarse) and (min-width: 1024px) {
  .viewsays--found-phone { display: none; }
  .viewsays--found-touch { display: inline; }
}
@media screen and (((pointer: fine) and (hover: hover)) or (pointer: none)) {
  .viewsays--found-phone { display: none; }
  .viewsays--found-desk  { display: inline; }
}

/* AND WHAT IS ACTUALLY IN FORCE. `data-view` is written on <html> by the server
   from this person's stored choice — `auto` when there is none — so this half
   needs no condition at all and cannot disagree with the stylesheet that was
   linked, because the same value decided both. */
[data-view="auto"]  .viewsays--chose-auto,
[data-view="phone"] .viewsays--chose-phone,
[data-view="touch"] .viewsays--chose-touch,
[data-view="desk"]  .viewsays--chose-desk { display: inline; }

/* "You chose this" as opposed to "the device did", without writing the same
   sentence out three times. */
[data-view]:not([data-view="auto"]) .viewsays--chosen { display: inline; }

/* AND THE SENTENCE THAT ONLY EXISTS WHEN THE TWO DISAGREE.
   This is the one a person needs and could not otherwise get: your choice is
   not what this device would have been given, and your choice is what runs.
   Without it, "the buttons are small on this machine and I do not know why" has
   no answer on the screen — which is the same defect as applying the detection
   silently, one level up.

   Read the three blocks as one rule with two exceptions. The base is "the
   device looks like a phone", so any forced view other than the phone clashes;
   each condition then withdraws its own view from the clash and adds the phone
   back to it. The two conditions cannot both match, so they cannot interfere,
   and `auto` is named by none of them — an automatic view never disagrees with
   itself. */
[data-view="touch"] .viewsays--clash,
[data-view="desk"]  .viewsays--clash { display: inline; }
@media screen and (pointer: coarse) and (min-width: 1024px) {
  [data-view="touch"] .viewsays--clash { display: none; }
  [data-view="phone"] .viewsays--clash,
  [data-view="desk"]  .viewsays--clash { display: inline; }
}
@media screen and (((pointer: fine) and (hover: hover)) or (pointer: none)) {
  [data-view="desk"]  .viewsays--clash { display: none; }
  [data-view="phone"] .viewsays--clash,
  [data-view="touch"] .viewsays--clash { display: inline; }
}

/* THE CHOOSER ITSELF.
   A radio per view, drawn as a row rather than as a segmented control, because
   a segmented control has room for a word and each of these needs a sentence:
   «موبايل» on its own does not tell a manager standing at a desk why he would
   want it. Same markup shape as `.seg__btn--radio` — the input is the whole hit
   area and the `<span>` beside it is what is drawn — so the target is the row
   at every density and never the 20 px circle. */
.viewpick { display: grid; gap: var(--sp-2); }
.viewpick__opt { display: block; position: relative; cursor: pointer; }
.viewpick__opt input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.viewpick__opt input + span {
  display: grid; grid-template-columns: 1.5rem 1fr; gap: var(--sp-1) var(--sp-3);
  align-items: center; min-height: var(--tap-lg);
  padding: var(--sp-3) var(--sp-4);
  border: var(--border-input) solid var(--line-3); border-radius: var(--r-md);
  background: var(--surface-card);
}
.viewpick__opt input:checked + span { border-color: var(--accent); background: var(--accent-quiet); box-shadow: var(--elev-1); }
.viewpick__opt input:focus-visible + span { outline: var(--focus-width) solid var(--focus-ring); outline-offset: var(--focus-offset); }
/* `visibility` and not `display`, so the row does not reflow when the selection
   moves — a list of four options that jumps as you arrow through it reads as a
   fault. */
.viewpick__mark { visibility: hidden; grid-row: 1 / span 2; font-size: var(--fs-lg); color: var(--accent); }
.viewpick__opt input:checked + span .viewpick__mark { visibility: visible; }
.viewpick__name { font-weight: var(--fw-bold); font-size: var(--fs-lg); min-inline-size: 0; }
.viewpick__why { grid-column: 2; font-size: var(--fs-sm); color: var(--ink-2); min-inline-size: 0; }

/* ================================================ 24. PRINT ============ */
/* Last on purpose: print overrides everything above it. Renumbered from the
   design's §17 because sections 18–23 were added after it. */
/* `.why` IS IN THE HIDDEN SET BELOW and «الشرح» is why. The explanation layer
   goes on paper, open or closed, unconditionally: a printed report of stock
   carrying tutorial text is the exact outcome §27 exists to remove, and the
   thing it leaves behind is something you PRESS — there is nothing to press on
   a sheet of paper. Contrast §26, which is deliberately NOT in the set, because
   the sample-data warning has to leave the building on the paper.
   docs/PROGRESSIVE-HELP.md §8.

   THE FIRST LINE OF THIS BLOCK KEEPS ITS SHAPE. `.appbar,` follows the brace
   directly, with nothing between them, because
   Tests\Feature\Demo\RemovableSampleDataTest reads this block to prove the
   sample-data strip is NOT in it — and a comment in the middle of the selector
   list is enough to make that check unreadable. It was, for one run. */
@media print {
  .appbar, .tabbar, .actionbar, .sheet, .conn, .sidenav, .why, .no-print { display: none !important; }
  .main { padding: 0 !important; }
  .card, .group, .dcard { box-shadow: none !important; border: .5pt solid #000 !important; break-inside: avoid; }
  body { font-size: 10pt; background: #FFF; }
  a { color: #000; text-decoration: none; }
  .chart__plot { border: .5pt solid #000; }
  .badge { border: .5pt solid #000; background: #FFF !important; background-image: none !important; }
  .badge--hold::after { content: " (HOLD)"; }
}

/* ================================================ 25. SHELF MAP ======== */
/* Server-rendered inline SVG, like §12, and paired with a real listing rather
   than a table — see resources/views/map/_listing.blade.php for why.

   THE ONE RULE TO KEEP IN MIND EDITING THIS SECTION: nothing here may scale a
   bin by what is in it. `.mapcell` has no width or height at all, because the
   geometry is computed in App\Support\Ui\ShelfMap and every bin is CELL_W by
   CELL_H, always. The schema does not know how big a bin is, how big a drum is,
   or how full anything is; a box that grew with its contents would be reporting
   a volume nobody has ever measured. docs/SHELF-MAP.md §1. */

.map { display: grid; gap: var(--sp-3); min-inline-size: 0; }
.map__h { font-size: var(--fs-sm); font-weight: var(--fw-bold); color: var(--ink-2); margin: var(--sp-4) 0 0; }
.map__note { font-size: var(--fs-xs); color: var(--ink-3); margin: 0; max-inline-size: 68ch; }
.map__cap { font-size: var(--fs-2xs); color: var(--ink-3); margin: 2px 0 var(--sp-2); }
.map__summary { font-size: var(--fs-xs); color: var(--ink-2); margin: 0; font-variant-numeric: tabular-nums; }
.map__out { display: flex; gap: var(--sp-3); flex-wrap: wrap; margin-block-start: var(--sp-4); }
.mapwh { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-block-start: var(--sp-3); }
.mapwh .chip.is-chosen { background: var(--accent-quiet); border-color: var(--accent); color: var(--accent-quiet-ink); }

/* THE MAP DOES NOT MIRROR IN RTL, and this is the only place this design system
   departs from the chart layer. §12 flips `.chart__plot`, which is right for a
   chart of TIME — reading order is the only thing that decides which end is the
   start. An aisle is a corridor in a building: rack 01 does not move to the
   other end when somebody switches to English, and two people standing at the
   same shelf must be able to point at the same picture. `.addr` is already
   `direction: ltr` unconditionally for the same family of reasons, so a shelf
   address is a left-to-right token inside Arabic too, and the map is that token
   drawn out. DO NOT ADD `[dir="rtl"] .map__plot { transform: scaleX(-1) }`. */
.map__plot { max-inline-size: 100%; block-size: auto; overflow: visible; }
.map__aisles { display: grid; gap: var(--sp-4) var(--sp-5); align-items: start;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), max-content)); }
.map__aisle { min-inline-size: 0; }
.map__ax { font-family: var(--font-mono); font-size: 13px; fill: var(--ink-2); text-anchor: middle; direction: ltr; unicode-bidi: isolate; }

/* ---- one bin. Fill says THERE IS STOCK HERE and nothing else — never "full". */
.mapcell__box { fill: var(--surface-card); stroke: var(--line-2); stroke-width: 1.25; }
.mapcell[data-empty] .mapcell__box { fill: var(--surface-sunken); stroke: var(--line-3); }
.mapcell__n { font-family: var(--font-mono); font-size: 17px; font-weight: 600; fill: var(--ink-1); text-anchor: middle; direction: ltr; unicode-bidi: isolate; }
/* How many addressed ROWS one drawn shelf stands for — `×23`. Quiet on purpose:
   the loud number in the middle of a cell is how much stock is there, and this
   is a fact about the racking that does not change from one day to the next.
   See App\Support\Ui\ShelfMap::foldShelves(). */
.mapcell__slots { font-family: var(--font-mono); font-size: 10px; fill: var(--ink-3); text-anchor: start; direction: ltr; unicode-bidi: isolate; }
.mapcell__glyph { fill: none; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.mapcell__bar { fill: var(--ink-3); }
.mapcell__hatch { stroke: none; opacity: .55; }
.maphatch { stroke-width: 2.5; stroke: var(--ink-3); }

.mapcell[data-state="warn"] .mapcell__box { fill: var(--st-warn-quiet); stroke: var(--st-warn); }
.mapcell[data-state="warn"] .mapcell__bar { fill: var(--st-warn); }
.mapcell[data-state="warn"] .mapcell__glyph { stroke: var(--st-warn-ink); }
.mapcell[data-state="hold"] .mapcell__box { fill: var(--st-hold-quiet); stroke: var(--st-hold); }
.mapcell[data-state="hold"] .mapcell__bar { fill: var(--st-hold); }
.mapcell[data-state="hold"] .mapcell__glyph { stroke: var(--st-hold-ink); }
.mapcell[data-state="hold"] .maphatch { stroke: var(--st-hold); }
.mapcell[data-state="bad"] .mapcell__box { fill: var(--st-bad-quiet); stroke: var(--st-bad); }
.mapcell[data-state="bad"] .mapcell__bar { fill: var(--st-bad); }
.mapcell[data-state="bad"] .mapcell__glyph { stroke: var(--st-bad-ink); }
.mapcell[data-state="bad"] .maphatch { stroke: var(--st-bad); }
/* FILLED, not tinted. `--st-unknown-quiet` is deliberately the same value as
   `--st-unknown` (see the token comment): never-counted and dormant are told
   apart by WEIGHT in the same grey family rather than by a second hue, and the
   badge already does this. It is the reason the danger states carry a hatch — a
   solid grey box is visually heavy, and without the hatch a blocked bin would
   read as quieter than an uncounted one, which inverts State::precedence(). */
.mapcell[data-state="unknown"] .mapcell__box { fill: var(--st-unknown-quiet); stroke: var(--st-unknown); }
.mapcell[data-state="unknown"] .mapcell__bar,
.mapcell[data-state="unknown"] .mapcell__glyph { stroke: var(--st-unknown-ink); fill: var(--st-unknown-ink); }
.mapcell[data-state="unknown"] .mapcell__glyph { fill: none; }
.mapcell[data-state="unknown"] .mapcell__n { fill: var(--st-unknown-ink); }
/* WHITE, not `--st-idle-quiet`: that token is the same sand as an empty bin, so
   a dormant bin read as an empty one. Dormant is the quietest rung on the ladder
   and looking almost-fine is correct — but there is stock in it. */
.mapcell[data-state="idle"] .mapcell__box { fill: var(--surface-card); stroke: var(--st-idle); }
.mapcell[data-state="idle"] .mapcell__bar { fill: var(--st-idle); }
.mapcell[data-state="idle"] .mapcell__glyph { stroke: var(--st-idle); }

/* found: a ring AND a pip. Colour and shape, like every other signal here. */
.mapcell__ring { fill: none; stroke: var(--accent); stroke-width: 3; }
.mapcell__pip { fill: var(--accent); }
.map[data-searching] .mapcell:not([data-hit]) { opacity: .3; }
.map[data-searching] .mapplace:not([data-hit]) { opacity: .45; }

/* ---- the places that are not on the racking. A LIST, not a strip: the system
   knows these exist and knows nothing about where they are, and a row of pills
   side by side is a little floor plan of a building nobody has surveyed. */
.mapplaces { display: grid; gap: var(--sp-2); min-inline-size: 0;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr)); }
.mapplace { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: var(--sp-3);
  min-height: var(--tap-lg); padding: var(--sp-2) var(--sp-3);
  background: var(--surface-card); border: var(--border-hair) solid var(--line-2); border-radius: var(--r-md);
  border-inline-start: var(--border-state) solid transparent; min-inline-size: 0; }
.mapplace[data-empty] { background: var(--surface-sunken); }
.mapplace[data-hit] { outline: 3px solid var(--accent); outline-offset: 2px; }
.mapplace__n { font-family: var(--font-mono); font-size: var(--fs-lg); font-weight: var(--fw-medium); direction: ltr; unicode-bidi: isolate; min-inline-size: 2.5ch; text-align: center; }
.mapplace__t { display: grid; gap: 1px; min-inline-size: 0; }
.mapplace__t b { font-family: var(--font-mono); font-size: var(--fs-sm); font-weight: var(--fw-semi); direction: ltr; unicode-bidi: isolate; }
.mapplace__c { font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--ink-3); }
.mapplace__end { display: flex; align-items: center; gap: var(--sp-2); }
.mapplace[data-state="warn"] { border-inline-start-color: var(--st-warn); }
.mapplace[data-state="hold"] { border-inline-start-color: var(--st-hold); }
.mapplace[data-state="bad"] { border-inline-start-color: var(--st-bad); }
.mapplace[data-state="unknown"] { border-inline-start-color: var(--st-unknown); }
.mapplace[data-state="idle"] { border-inline-start-color: var(--st-idle); }

/* ---- the answer, at the size this system reads addresses at */
.mapfound { background: var(--accent-quiet); border-radius: var(--r-md); padding: var(--sp-4); display: grid; gap: var(--sp-2); min-inline-size: 0; }
.mapfound__k { margin: 0; font-size: var(--fs-xs); color: var(--accent-quiet-ink); font-weight: var(--fw-semi); }
.mapfound__a { margin: 0; display: flex; gap: var(--sp-5); flex-wrap: wrap; }
.mapfound__part { display: grid; gap: 2px; }
.mapfound__part small { font-size: var(--fs-xs); color: var(--ink-2); }
.mapfound__part b { font-family: var(--font-mono); font-size: var(--fs-addr); line-height: 1; direction: ltr; unicode-bidi: isolate; }
.mapfound__code { margin: 0; font-size: var(--fs-xs); color: var(--ink-2); }
.mapfound__clear { margin: 0; font-size: var(--fs-xs); }

/* ---- the listing, grouped by bin */
.mapbin { margin-block-start: var(--sp-4); min-inline-size: 0; }
.mapbin__h { display: flex; align-items: baseline; gap: var(--sp-3); flex-wrap: wrap; margin: 0 0 var(--sp-2); }
.mapbin__h .addr { font-family: var(--font-mono); font-size: var(--fs-addr); font-weight: var(--fw-medium); }
.mapbin__h small { font-size: var(--fs-xs); color: var(--ink-3); font-weight: var(--fw-regular); }

.map__legend i.lg { inline-size: 1rem; block-size: 1rem; border-radius: 0; }
.map__legend i.lg svg { inline-size: 100%; block-size: 100%; fill: none; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.map__legend b[data-state="ok"] svg { stroke: var(--st-ok); }
.map__legend b[data-state="warn"] svg { stroke: var(--st-warn); }
.map__legend b[data-state="hold"] svg { stroke: var(--st-hold); }
.map__legend b[data-state="bad"] svg { stroke: var(--st-bad); }
.map__legend b[data-state="unknown"] svg { stroke: var(--st-unknown); }
.map__legend b[data-state="idle"] svg { stroke: var(--st-idle); }

/* ---- the printed sheet. ONE BUILDING PER PAGE, and it says when it was taken. */
.mapsheet__head h1 { font-size: var(--fs-lg); margin: 0; }
.mapsheet__at { margin: var(--sp-1) 0 0; font-size: var(--fs-sm); color: var(--ink-2); }
.mapsheet__note { margin: var(--sp-1) 0 0; font-size: var(--fs-xs); color: var(--ink-3); }
.mapsheets { display: grid; gap: var(--sp-6); padding: var(--sp-4); max-inline-size: var(--w-content); margin-inline: auto; }

@media print {
  /* The search surface is not part of the document. */
  .searchbar, .mapwh, .map__out, .mapfound__clear { display: none !important; }
  .mapsheet { break-after: page; }
  .mapsheet:last-child { break-after: auto; }
  .map__aisle, .mapbin, .mapplace { break-inside: avoid; }
  /* On paper the state is the GLYPH, the HATCH and the WORD. tokens.css §5
     flattens every hue to black, so the fill can no longer carry it — and the
     hatch has to get darker to survive a photocopy of a photocopy. */
  .mapcell__box { fill: #FFF !important; stroke: #000 !important; }
  .mapcell[data-empty] .mapcell__box { fill: #FFF !important; stroke: #999 !important; }
  .mapcell__glyph { stroke: #000 !important; }
  .mapcell__n { fill: #000 !important; }
  .mapcell__slots { fill: #555 !important; }
  .mapcell__bar { fill: #000 !important; }
  .mapcell__hatch { opacity: 1; }
  .maphatch { stroke: #000 !important; stroke-width: 1.6; }
  .mapcell__ring, .mapcell__pip { display: none; }
  .mapplace { border: .5pt solid #000 !important; }
}

/* ================================================ 26. SAMPLE DATA ====== */
/* THE STRIP THAT SAYS THE NUMBERS ON THIS PAGE WERE INVENTED.
   resources/views/components/ivg/demo-mark.blade.php explains what it is for.
   This section explains the four decisions that are in the stylesheet.

   1. IT IS NOT STICKY, and that is on purpose. `.conn` and `.appbar` are both
      `position: sticky; inset-block-start: 0`, so a third sticky strip at the
      same offset would stack on top of the app bar on every scroll and cover
      the screen title. Persistent here means "on every page, for every person,
      until it is untrue" — not "pinned under the thumb". It is the first thing
      in the reading order of every screen in the system, which is where a
      warning about the page belongs, and it costs one line of height.

   2. THE HATCH CARRIES THE MEANING, NOT THE COLOUR. tokens.css §5 flattens
      every semantic colour to black on white for print, so an amber strip
      prints as a white strip. A hatch, a glyph and the words survive that;
      `print-color-adjust: exact` keeps the hatch through a printer driver's
      "ignore background colours" default, which is the setting most people
      have and none of them know about.

   3. IT IS NOT IN THE `.no-print` SET IN §24, which hides the app bar, the tab
      bar, the action bar and the connection strip. A printed report of stock
      that was never delivered is the whole reason this exists.

   4. THE RUNNING COPY IS `position: fixed` IN PRINT, which the engines that
      support paged fixed positioning repeat on every sheet. It cannot overlap
      anything in practice in THIS application, and that is an argument about
      this codebase rather than a general one: §24 sets `break-inside: avoid`
      on every card, group and detail card, and `.mapsheet` sets
      `break-after: page`, so a page here ends on a block boundary with
      whitespace under it rather than mid-paragraph at the sheet edge. The
      padding on `.main` reserves the space on the last page, where content can
      genuinely finish anywhere. No `@page` rule is set: this stylesheet is
      also loaded by the 50×30 mm thermal label path, and a page margin chosen
      for A4 would eat half a label. */

.demomark {
  display: flex; align-items: baseline; flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-2) var(--sp-4);
  font-size: var(--fs-2xs); line-height: 1.35;
  color: var(--st-warn-ink);
  background-color: var(--st-warn-quiet);
  background-image: repeating-linear-gradient(
    135deg,
    color-mix(in srgb, var(--st-warn) 34%, transparent) 0 6px,
    transparent 6px 14px
  );
  border-block-end: var(--border-input) solid var(--st-warn);
  print-color-adjust: exact; -webkit-print-color-adjust: exact;
}
.demomark__glyph { flex: none; font-size: var(--fs-sm); line-height: 1; color: var(--st-warn-ink); }
.demomark__say { display: flex; align-items: baseline; gap: var(--sp-2); min-inline-size: 0; }
.demomark__say strong { font-weight: var(--fw-bold); letter-spacing: var(--tracking-tight); white-space: nowrap; }
.demomark__what { color: var(--st-warn-ink); opacity: .82; }
/* A rule between the two scripts, not a slash: a slash inside a bidi run picks
   a side and moves, and this line has one run of each direction on it. */
.demomark__div {
  flex: none; inline-size: 1px; align-self: stretch;
  background: color-mix(in srgb, var(--st-warn) 60%, transparent);
}

/* The second copy exists only on paper. */
.demomark--running { display: none; }

/* THE THREE AUTHENTICATION SCREENS. `.auth` names its rows explicitly, and it
   names a different set of them above 900 px, so a fourth child needs the extra
   row declared at both — otherwise the strip takes the `1fr` that belongs to
   the panel and the dark header stretches down the screen. `justify-self`
   because the desktop rule centres every child and a warning that is only as
   wide as its own words is a badge, not a strip.

   Declared here rather than in §20 because the extra row exists only while the
   sample data does, and `:has()` is what says so. */
.auth:has(.demomark) { grid-template-rows: auto auto 1fr auto; }
.auth > .demomark { justify-self: stretch; }
@media (min-width: 900px) {
  .auth:has(.demomark) { grid-template-rows: auto 1fr auto; }
}

/* On the narrowest screens the sentence goes and the two names stay. Losing
   "None of these products, quantities or deliveries are real" is a loss;
   wrapping «بيانات تجريبية / Sample data» onto four lines at the top of every
   screen is how the strip stops being read at all. */
@media (max-width: 419px) {
  .demomark__what { display: none; }
}

@media print {
  .demomark {
    display: flex !important;
    background-color: #FFF !important;
    background-image: repeating-linear-gradient(135deg, #000 0 1pt, #FFF 1pt 6pt) !important;
    color: #000 !important;
    border: 1pt solid #000 !important;
    padding: 2mm 3mm; margin-block-end: 3mm;
    font-size: 9pt;
    break-inside: avoid; break-after: avoid;
  }
  /* The words sit on paper, not on the hatch. */
  .demomark__say { background: #FFF; padding-inline: 2mm; }
  .demomark__what { opacity: 1; }
  .demomark__div { background: #000; }

  .demomark--running {
    display: flex !important;
    position: fixed; inset-block-end: 0; inset-inline: 0;
    margin: 0; padding: 1mm 3mm; font-size: 7pt;
    border: 0; border-block-start: 1pt solid #000 !important;
    background-image: none !important;
  }
  /* Only when the strip is actually there. Every other print in this system —
     a label, a receipt, a shelf map of real stock — keeps §24's `padding: 0`
     and gains no mystery gap at the foot of the page. */
  .app:has(.demomark) .main { padding-block-end: 14mm !important; }
}

/* ================================================ 27. THE EXPLANATION == */
/* «الشرح» — WHAT A SCREEN SAYS ABOUT ITSELF, AND WHEN IT STOPS SAYING IT.

   THE OWNER'S SENTENCE: "The UI/UX must be educative at the start, then after a
   while when users get used to it will be extremely professional." Both halves
   are requirements and they pull opposite ways. 143 paragraphs printed
   permanently for everybody satisfies the first and destroys the second; a
   tooltip satisfies the second and — on this hardware — destroys the first,
   because THERE IS NO HOVER ON A RUGGED ANDROID IN AN AISLE.

   So: a native `<details>`, OPEN until the screen is familiar and closed after.
   App\Support\Ui\Help decides which. docs/PROGRESSIVE-HELP.md is the argument
   and the numbers; this section is only the four decisions in the stylesheet.

   1. THE WHOLE ROW IS THE CONTROL, NOT THE LITTLE CIRCLE.
      `min-block-size: var(--tap)` on the summary, and the heading lives inside
      it. Identical rule to `.viewpick__opt` in §23 and for the identical
      reason: the thing you press is the row at every density and never a 20 px
      circle. `.why__mark` is `aria-hidden` — a sign, not a button — and it is
      NOT sized as a target, because it is not one.

      tools/overflow/reach.mjs measures `.why > summary` against its view's
      floor in all eight passes. It is deliberately not a CEILING witness: a
      long heading wraps to two lines and the height stops being the token,
      which is the same reason `.tabbar__item` is excluded there.

   2. `inline-flex`, SO THE MARK HUGS ITS OWN HEADING. The first render of this
      component stretched the summary across the content column, which parked
      the mark about a thousand pixels away from the heading it belonged to at
      1920 and read as a fault. `max-inline-size: 100%` plus `min-inline-size: 0`
      on the heading is what keeps a long Arabic title wrapping instead of
      pushing the page sideways — §18's rule, applied to a flex item that would
      otherwise take its min-content width.

   3. THE MARK CHANGES WITH THE STATE, AND THAT IS THE TEACHING.
      Open, it is filled in the accent and it is attached to a tinted well.
      Somebody on their first visit presses it, watches the paragraph fold into
      it, and now knows where the explanation went — so nothing has to be
      discovered on the day it disappears. It is the one interaction this
      feature is allowed to spend.

   4. IT DOES NOT PRINT. The rule is in §24 with the app bar and the tab bar,
      not here, because print overrides everything above it. */

.why > summary {
  display: inline-flex; align-items: center; gap: var(--sp-3);
  max-inline-size: 100%;
  min-block-size: var(--tap);
  padding-block: var(--sp-1);
  cursor: pointer;
  /* Both, because the two engines hide the default triangle differently. */
  list-style: none;
}
.why > summary::-webkit-details-marker { display: none; }
.why > summary > :first-child { min-inline-size: 0; }
.why > summary:focus-visible { outline: var(--focus-width) solid var(--focus-ring); outline-offset: var(--focus-offset); border-radius: var(--r-xs); }

.why__mark {
  flex: none;
  inline-size: 1.75rem; block-size: 1.75rem;
  display: inline-grid; place-items: center;
  border: var(--border-input) solid var(--line-3);
  border-radius: var(--r-pill);
  background: var(--surface-card);
  color: var(--ink-2);
  font-size: var(--fs-sm); font-weight: var(--fw-black); line-height: 1;
}
.why > summary:hover .why__mark { background: var(--surface-sunken); color: var(--ink-1); }
.why[open] .why__mark { background: var(--accent-quiet); border-color: var(--accent); color: var(--accent-quiet-ink); }

/* THE PROSE. A tinted well with a rule on the inline-start edge, so it reads as
   an ASIDE rather than as data. That distinction is the whole complaint: a
   paragraph set as body text beside a stock figure is a paragraph somebody has
   to decide is not a stock figure, every time. */
.why__say {
  margin-block-start: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  border-inline-start: 3px solid var(--accent);
  border-radius: var(--r-sm);
  background: var(--accent-quiet);
  color: var(--ink-1);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  max-inline-size: 62ch;
}

/* THE FIELD SHAPE. There is no heading to wrap here — a `<label for>` inside a
   `<summary>` would fight the input for the tap — so the summary carries the
   QUESTION, in the hint's own type, and the mark sits after it. */
.why--field > summary { gap: var(--sp-2); }
.why__lead { font-size: var(--fs-xs); font-weight: var(--fw-bold); color: var(--ink-2); }
.why--field[open] .why__lead { color: var(--accent-quiet-ink); }

/* THE GALLERY DRAWS BOTH STATES SIDE BY SIDE, so the overflow manifest carries
   the settled screen as well as the learning one. Nothing in the product uses
   this class. */
.whypair { display: grid; gap: var(--sp-5); }
@media (min-width: 900px) { .whypair { grid-template-columns: 1fr 1fr; } }
.whypair__one { min-inline-size: 0; padding: var(--sp-4); border: var(--border-hair) solid var(--line-1); border-radius: var(--r-md); background: var(--surface-card); }
.whypair__cap { font-size: var(--fs-micro); font-weight: var(--fw-bold); letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--ink-3); margin-block-end: var(--sp-2); }


/* ================================================================
   28. WAREHOUSE SETUP — making the buildings, and naming the shelves
   ================================================================ */

/* THE WORKED EXAMPLE. On the warehouse form it is the shelf label the code
   about to be typed will produce; on the generator it is the address the format
   about to be chosen will produce. Both are the one decision on their screen
   that cannot be taken back — `locations.label_code` is derived once and frozen
   by trigger, because it is printed on a sticker and stuck to a shelf — so it is
   drawn at display size rather than as a 13 px caption. The first mockup had it
   as a `factlist` row and it disappeared into the form. */
.setup__example {
  margin-block: var(--sp-2) 0;
  font-size: var(--fs-xl);
  font-weight: var(--fw-bold);
  color: var(--ink-1);
}

/* FROM AND TO. `.field__row` already means "one control, not two" and already
   carries the 30rem cap that keeps a pair of boxes together instead of at
   opposite ends of a 1920 px window; this only adds the third column for the
   arrow between them. The arrow is `.glyph-dir`, which has mirrored in RTL
   since §1 — the boxes reverse with the writing direction and a bare glyph
   does not, which is what made an earlier draft read backwards in Arabic. */
.field__row--range { grid-template-columns: 1fr auto 1fr; align-items: center; }
