/* ==========================================================================
   Aura Trade — Design System v2 (common/css/theme.css)
   "Indigo Refined" — D-043 direction A, adopted 2026-07-15, refined here in
   full. Single source of truth for tokens, base styles, components and the
   three-app shell (store / supplier / admin). Synced into each app via
   `npm run sync-assets` — edit here only, never edit the per-app copies under apps/.

   Lineage: originally adapted (copy-and-adapt, not shared) from Aura Retail's
   theme via docs/reference/retail-theme-intel.md (v1). Trade has owned this
   file outright since v1; v2 is Trade's own OKLCH-regenerated palette, first
   explored side by side with two alternatives in design-lab/directions.html
   and picked by the founder (D-043). It does not re-sync from Retail.

   What's new in v2 (D-043 refinement round):
   - Full "Indigo Refined" palette, regenerated in OKLCH (H258 neutrals,
     H280 accent) — see design-lab/directions.html #dir-a for the oklch(...)
     source values behind every hex below.
   - New 3rd surface layer, --surface-raised, for popovers / the sticky
     topbar (section 5 has the layering convention).
   - Ledger typography: --font-mono + .mono-chip (section 6).
   - Motion tokens (--motion-fast/base/slow, --ease-standard) driving every
     transition in this file, zeroed under prefers-reduced-motion (section 13).
   - Anti-liquid-glass topbar: blurred --surface-raised + hairline border,
     opaque fallback where backdrop-filter is unsupported (section 9).
   - h1/h2 page-title rules + a unified :focus-visible ring (sections 3, 12).
   - Dark mode: commented-out token skeleton only, not implemented (section 15).
   Component class names and contracts are unchanged from v1 — this is a
   token/value + additive-rule upgrade, nothing structural.

   House rules:
   - Font: Inter. Container radius 12px, control radius 6px.
   - Flat, thin borders; shadows reserved for hover states / popovers.
   - Light sidebar, tint-only badges (no border, no dot).
   - Touch baseline: controls >= 48px tall, tap targets >= 44px, inputs
     >= 16px font (avoids iOS focus-zoom). Base font 16px.
   - Responsive is built in, not bolted on (D-040): desktop (>=1200px) and
     tablet (768-1199px) are BOTH first-class targets; <768px is a usable
     degradation only for MVP. See the "Responsive" section at the bottom
     for the single system-level implementation of shell + table behavior.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Design tokens
   -------------------------------------------------------------------------- */
:root {
  /* Indigo Refined palette (D-043 direction A). Regenerated in OKLCH, a
     perceptually-even color space: H258 for the neutral/gray ramp, H280 for
     the indigo accent family — smoother lightness/chroma steps than the v1
     hand-picked hexes. Still Trade's one fixed theme; no runtime theming. */
  --primary:        #131D2C;
  --surface:        #FFFFFF; /* founder feedback 2026-07-15: cards back to clean white (v2.0.1) */
  --surface-raised: #FFFFFF; /* 3rd surface layer: popovers, sticky topbar (glass) — never .card, see section 5;
                                same value as --surface in light mode (distinction = glass blur + hairline),
                                kept as a separate token for the dark-mode pre-embed */
  --bg:             #F7F9FC; /* founder feedback 2026-07-15: lighter grey — v2's #EFF3FA read as "muddy" */
  --border:         #DCE2E9;
  --border-strong:  #C2CAD5;
  --muted:          #5D6A7D;
  --muted-soft:     #909CAE;
  --nav-text:       #424E60;
  --accent:         #574ED1;
  --accent-hover:   #4638B6;
  --accent-soft:    #ECEFFF;
  --accent-ring:    rgba(87, 78, 209, .16);

  --success: #00BB81;  --success-soft: #E2F8EC; --success-text: #005D2F;
  --warning: #FBA100;  --warning-soft: #FFEDD2; --warning-text: #813900;
  --info:    #00A4E9;  --info-soft:    #E6F4FE; --info-text:    #005491;
  --danger:  #E54C4A;  --danger-soft:  #FFEBE8; --danger-text:  #960011;
  --neutral: #5D6A7D;  --neutral-soft: #E7EBF2; --neutral-text: #424E60;
  --indigo:  #574ED1;  --indigo-soft:  #ECEFFF; --indigo-text:  #4638B6; /* = accent family */

  --radius:    6px;
  --radius-lg: 12px;
  --shadow-hover: 0 6px 22px rgba(15, 23, 42, .07);
  --shadow-pop:   0 16px 44px rgba(15, 23, 42, .14);

  --sidebar-w:      240px;  /* desktop: pinned sidebar */
  --sidebar-w-rail: 64px;   /* tablet: collapsed icon rail */

  --control-h:    48px;
  --control-h-sm: 40px;
  --tap-min:      44px;

  /* Ledger typography (trend 3, D-043 refinement round): mono stack for
     order-number / SKU chips (.mono-chip, section 6). Amount / quantity /
     order-number table columns are always tabular-nums (.t-mono, section 6),
     with or without this font. */
  --font-mono: 'SFMono-Regular', Menlo, Consolas, monospace;

  /* Motion tokens (trend 2/4, D-043 refinement round). Every transition
     below is built from these — never a bare duration — so the single
     prefers-reduced-motion query (section 13) can zero all of them at once. */
  --motion-fast:   80ms;
  --motion-base:   120ms;
  --motion-slow:   200ms;
  --ease-standard: cubic-bezier(.4, 0, .2, 1);
}

/* --------------------------------------------------------------------------
   2. Reset (trimmed)
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; }
body { margin: 0; }
h1, h2, h3, h4, h5, h6, p, figure { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
a { color: inherit; }
button { font: inherit; color: inherit; }
img, svg { display: block; max-width: 100%; }
/* The native `hidden` attribute must WIN (D-096 self-check, Retail bug
   [8.0.27]): the browser's own `[hidden] { display: none }` lives in the UA
   stylesheet, so ANY author rule that sets `display` beats it regardless of
   specificity — `.btn` (display: inline-flex, section 4) and `.row`/`.field`
   (flex, section 10/7) all do. A `<button class="btn" hidden>` therefore
   stayed visible. Declared once here, at the reset, rather than fixing it
   component by component. */
[hidden] { display: none !important; }

/* --------------------------------------------------------------------------
   3. Base typography
   -------------------------------------------------------------------------- */
body {
  font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  background: var(--bg);
  color: var(--primary);
  font-size: 16px;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-feature-settings: "cv02", "cv03", "cv04", "cv11";
}

/* Page titles (backlog item, D-043 refinement round). h1 = page title,
   h2 = page section title — distinct from .card-title (14.5px/700, section
   5), which is scoped to a single card's header, not a whole page. */
h1 { font-size: 24px; font-weight: 800; letter-spacing: -.02em; line-height: 1.3; }
h2 { font-size: 20px; font-weight: 800; letter-spacing: -.015em; line-height: 1.3; }

/* --------------------------------------------------------------------------
   4. Buttons
   -------------------------------------------------------------------------- */
.btn {
    display: inline-flex; align-items: center; gap: 7px; justify-content: center;
    font-family: inherit; font-weight: 600; font-size: 15px; letter-spacing: -.005em;
    min-height: var(--control-h); padding: 0 18px; border-radius: var(--radius); border: 1px solid transparent; cursor: pointer;
    transition: background var(--motion-base) var(--ease-standard),
                border-color var(--motion-base) var(--ease-standard),
                color var(--motion-base) var(--ease-standard);
    text-decoration: none; white-space: nowrap;
}
.btn:active { transform: translateY(.5px); }
.btn-primary   { background: var(--accent); color: #fff; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-secondary { background: var(--surface); color: var(--primary); border-color: var(--border-strong); }
.btn-secondary:hover { background: var(--bg); border-color: var(--muted-soft); }
.btn-ghost     { background: transparent; color: var(--muted); }
.btn-ghost:hover { background: var(--bg); color: var(--primary); }
.btn-danger    { background: var(--danger); color: #fff; }
.btn-success   { background: var(--success); color: #fff; }
/* Row-action weight rule (S4.1 W3 design review, restated and extended by the
   CM-3 design ledger, 2026-07-28). Two questions decide a row action's button,
   in this order:

   1. Does it DESTROY something irreversibly? A filled `.btn-danger` is
      reserved for that, plus the final confirm button inside a confirmation
      dialog (confirm-dialog.js; the supplier's "Cancel this order" confirm).
      A REVERSIBLE action never takes a filled red however serious it sounds:
      the store customer book's row "Archive" is undone by the Archived view's
      own Restore, so it is `.btn-ghost-danger` — a page of filled red buttons
      down a 50-row customer list reads as a wall of alarms about an action
      that loses nothing (founder review, reports/screenshots/cm3-01).
      Ghost-toned red keeps the destructive colour cue without that weight;
      the same reason the order editor's repeating item/extra "Remove" rows
      use it.
   2. Is it the action the CURRENT VIEW exists for? That one action carries
      `.btn-secondary`; every other action in the same row steps down to a
      ghost tone (`.btn-ghost`, or `.btn-ghost-danger` when destructive). So
      the customers list reads View = secondary + Archive = ghost-danger, and
      its Archived view reads Restore = secondary + View = ghost — the view's
      purpose is the strongest thing in the row, reached by stepping the other
      action DOWN rather than adding a second fill. The orders list's
      admin-only "Show hidden" column keeps Restore at `.btn-ghost` under this
      same rule, not as an exception: that list exists for working quotes, and
      Restore there is an escape hatch inside a dimmed (opacity .55) row.
      `.btn-primary` never repeats down a list at all — a per-row primary fill
      turns a table into a colour field and leaves the page's own primary
      action ("+ Add customer") with nothing to stand out against.

   One deliberate exception, named rather than silently tolerated: the
   supplier Settings price-tier table's per-row "Delete" stays a filled
   `.btn-danger` — a genuine irreversible destroy (rule 1) in a two-or-three
   row admin table, and disabled outright while any store sits on the tier.
   If that table ever grows into a real list, it moves to ghost-danger too. */
.btn-ghost-danger { background: transparent; color: var(--danger-text); }
.btn-ghost-danger:hover { background: var(--danger-soft); color: var(--danger-text); }
.btn-sm { min-height: var(--control-h-sm); padding: 0 14px; font-size: 14px; }
.btn:disabled { opacity: .5; cursor: not-allowed; }

/* Back button (D-064-1): every "Back to …" button in the app uses this same
   class combination (`btn btn-ghost btn-sm btn-back`) and renders as
   chevron-left icon + label (icon/label markup set once in app.js, same
   pattern as the rail action tiles) — page top-left in every view that has
   one, never grouped with a view's other header actions on the right. */
.btn-back svg { width: var(--icon-size); height: var(--icon-size); }

/* --------------------------------------------------------------------------
   5. Cards
   Surface layering convention (3-layer model, D-043 direction A): --bg (page
   canvas) < --surface (.card, below) < --surface-raised (popovers, sticky
   topbar — section 9). Depth is expressed as a background-shade step, not a
   shadow; --shadow-hover/--shadow-pop stay reserved for hover/popover lift.

   Title hierarchy, three tiers (D-069) — never blend these, never repeat one
   tier's text in another:
     1. PAGE level — `.eyebrow` in the topbar (section 9), one word naming the
        active page (Dashboard/Orders/Customers/Settings), set once per view
        by the app's own view-switch function (store: showView()'s
        TOPBAR_PAGE_TITLE map). A page's own cards do NOT repeat this word as
        a `.card-title` — see D-069 point 1 (e.g. the Orders/Customers list
        cards keep only their descriptive subtitle, no "Orders"/"Customers"
        heading of their own).
     2. FLOW-CARD level — `.sec-num` (section 22), a numbered sequence of the
        main-area cards that make up ONE linear workflow inside a full-screen
        workspace (e.g. the order editor's Client details=01 / items=02 /
        Extras=03). Numbers are positional per workspace, not global; the
        text after the number is either a plain field-group name ("Client
        details") or an Edition vocab token ("Curtains" — never the wholesale
        catalog's category name, D-062-2) — `.sec-num`'s own
        `text-transform: uppercase` renders it in caps, the underlying string
        is never upper-cased in source.
     3. DASHBOARD-CARD level — `.card-title` (below), a small dark bold label
        with NO number, used for (a) an ordinary page card's own heading
        ("Store identity", "Recent quotes") and (b) a workspace's side-rail
        gauge cards (Quote summary / Actions / Order info, section 22) — the
        rail is a set of independent instrument cards, not a linear flow, so
        it never gets `.sec-num` numbering.
   -------------------------------------------------------------------------- */
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 20px; }
.card-pad-0 { padding: 0; overflow: hidden; }
.card-title { font-size: 14.5px; font-weight: 700; margin: 0; letter-spacing: -.01em; }
.card-head { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 16px; }
.card-head p { margin: 3px 0 0; font-size: 12px; color: var(--muted); }
.divider { height: 1px; background: var(--border); border: 0; margin: 0; }

/* --------------------------------------------------------------------------
   6. Tables
   Responsive convention (defined once at system level, per D-040):
   - Wrap every .table in a .table-wrap so it scrolls horizontally instead
     of squeezing columns unreadable on tablet.
   - Mark less-critical columns with .col-optional on BOTH the <th> and the
     matching <td> in every row; those columns are hidden at <=1199px,
     leaving the key columns visible without scrolling.
   -------------------------------------------------------------------------- */
.table-wrap { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.table { width: 100%; min-width: 640px; border-collapse: collapse; font-size: 14px; }
.table thead th {
    text-align: left; font-size: 11px; font-weight: 600; color: var(--muted); text-transform: uppercase;
    letter-spacing: .05em; padding: 12px 16px; border-bottom: 1px solid var(--border); background: var(--bg);
}
.table tbody td { padding: 14px 16px; border-bottom: 1px solid var(--border); font-weight: 400; vertical-align: middle; }
.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr:hover { background: var(--bg); }
.t-strong { font-weight: 600; color: var(--primary); }

/* Ledger typography (trend 3, D-043 refinement round): amount / quantity /
   order-number columns are ALWAYS tabular-nums, so digits line up down a
   column instead of drifting with proportional-width glyphs. */
.t-mono { font-variant-numeric: tabular-nums; }

/* Small inline order#/SKU chip: tint background, no border, mono digits.
   Deliberately LESS rounded than .badge (var(--radius), not a pill) and
   always neutral-toned, so it reads as "an identifier", never a status. */
.mono-chip {
  display: inline-flex; align-items: center;
  font-family: var(--font-mono); font-size: 12.5px; font-weight: 600;
  font-variant-numeric: tabular-nums; letter-spacing: -.01em;
  padding: 3px 8px; border-radius: var(--radius);
  background: var(--neutral-soft); color: var(--neutral-text);
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   7. Form fields
   -------------------------------------------------------------------------- */
.field { display: flex; flex-direction: column; gap: 6px; }
.label { font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
.input {
    width: 100%; font-family: inherit; font-size: 16px; font-weight: 400; color: var(--primary);
    background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius); min-height: var(--control-h); padding: 10px 14px;
    outline: none;
    transition: border-color var(--motion-base) var(--ease-standard),
                box-shadow var(--motion-base) var(--ease-standard);
}
.input::placeholder { color: var(--muted-soft); }
.input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-ring); }

/* --------------------------------------------------------------------------
   8. Badges
   Tint background + colored text only — no border, no dot.
   Trade order-state machine (docs/ARCHITECTURE-V2.md #2), NOT Retail's
   status vocabulary. Semantic grouping:
     - pending / awaiting confirmation -> warning
     - paid / completed                -> success
     - mid-flow (confirmed..dispatched)-> info / indigo
     - failed                          -> danger
     - cancelled                       -> neutral
   -------------------------------------------------------------------------- */
.badge {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 12px; font-weight: 600; line-height: 1;
  padding: 6px 10px; border-radius: 999px; white-space: nowrap;
}
.b-pending-payment    { background: var(--warning-soft); color: var(--warning-text); }
.b-awaiting-transfer  { background: var(--warning-soft); color: var(--warning-text); }
.b-payment-processing { background: var(--warning-soft); color: var(--warning-text); }
.b-payment-failed     { background: var(--danger-soft);  color: var(--danger-text); }
.b-paid              { background: var(--success-soft); color: var(--success-text); }
.b-confirmed         { background: var(--info-soft);     color: var(--info-text); }
.b-po-sent           { background: var(--indigo-soft);   color: var(--indigo-text); }
.b-goods-received    { background: var(--indigo-soft);   color: var(--indigo-text); }
.b-dispatched        { background: var(--info-soft);     color: var(--info-text); }
.b-completed         { background: var(--success-soft); color: var(--success-text); }
.b-cancelled         { background: var(--neutral-soft); color: var(--neutral-text); }

/* Store's OWN stage machine (S4.2, docs/design/S4V2-store-workflow.md §1;
   seven-state machine landed D-071/S5-W3, 'dispatched' inserted at D-095) —
   storeOrders.stage: draft -> quoted -> confirmed -> pending ->
   in_production -> dispatched -> ready -> completed
   ('accepted' retired in S4.2 W2, 'installed' retired in D-071/S5-W3 — a
   document still carrying either fails loud instead of rendering,
   apps/store/js/app.js's assertValidStage()). A UI-level convention only
   (no Firestore rule enforces this transition); entirely separate from the
   wholesale order-state badges above — storeOrders and wholesale orders are
   different collections with different lifecycles.

   `.b-store-confirmed`/`.b-pending`/`.b-in-production`/`.b-ready` are NEW,
   OWN-named classes rather than reusing the wholesale `.b-confirmed`/
   `.b-pending-payment`/`.b-po-sent`/`.b-dispatched` above: those classes
   already render DIFFERENT, already-shipped meanings on a different
   collection's status field — repainting one here would silently recolour
   that live swatch too (a real naming collision, flagged rather than
   silently resolved, CLAUDE.md §5). `.b-completed` IS reused verbatim below
   for the store's 'completed' stage (task instruction) because both
   meanings ("fully done") genuinely agree — no collision, unlike the
   others. D-095's new store stage 'dispatched' reuses `.b-dispatched` above
   on that same exception (both mean "the goods have left the warehouse"),
   so no `.b-store-dispatched` twin exists — see app.js stageBadgeClass().
   `.b-accepted`/`.b-installed` (the two now-retired enum values)
   are removed outright here — their design-lab §8 swatches are retired in
   the same pass, so nothing references either class any more
   (grep-confirmed repo-wide). */
.b-draft           { background: var(--neutral-soft); color: var(--neutral-text); }
.b-quoted          { background: var(--info-soft);    color: var(--info-text); }
.b-store-confirmed { background: var(--indigo-soft);  color: var(--indigo-text); }
.b-pending         { background: var(--warning-soft); color: var(--warning-text); }
.b-in-production   { background: var(--indigo-soft);  color: var(--indigo-text); }
.b-ready           { background: var(--info-soft);    color: var(--info-text); }
/* 'completed' reuses .b-completed above (success tone) — no new rule needed. */

/* Soft-delete marker (D-085-2): a store order that has been "deleted" — i.e.
   hidden, never destroyed — shown ONLY in the store admin's revealed list,
   alongside (not instead of) the order's real stage badge. Its own class
   rather than reusing the neutral `.b-cancelled` above: that one already
   renders a WHOLESALE order's cancelled state on a different collection, and
   "cancelled" and "hidden" are genuinely different facts about an order —
   the naming-collision rule this section's own preamble states. */
.b-hidden { background: var(--neutral-soft); color: var(--neutral-text); }

/* Archived-customer marker (CM-3, docs/design/customer-module.md §A.4 point 4):
   a store customer whose record has been archived — the customer book has no
   hard delete, so "archived" is the terminal state a record can reach. Its own
   class for the same reason `.b-hidden` above is not `.b-cancelled`: the tone
   is shared, the meaning is not (a different collection, a different fact),
   and repainting either swatch must not silently repaint the other. */
.b-archived { background: var(--neutral-soft); color: var(--neutral-text); }

/* Extras three-list chips (S4.2 W2, docs/reference/retail-order-ui-intel.md
   §4 "彩色 chip 区分") — one tone per entry kind in the mixed services/
   payments/discounts list; a type label, not an order-state badge. */
.b-xtra-payment  { background: var(--success-soft); color: var(--success-text); }
.b-xtra-discount { background: var(--warning-soft); color: var(--warning-text); }
.b-xtra-service  { background: var(--neutral-soft); color: var(--neutral-text); }

/* Procurement "already ordered" badge (D-070, docs/design/S4V2-REV2.md §6)
   — an ITEM-level flag ("has this row's fabric component already gone out
   on a wholesale procurement order?"), derived from `procurements[]`
   (S4V2-REV1.md §2) — NOT a `storeOrders.stage` value. Kept as its own
   class rather than reusing `.b-completed`/`.b-paid`: order lifecycle stage
   and per-item procurement status are genuinely different concepts that can
   diverge later (a completed order could still have an un-procured item,
   e.g.), so the naming collision this file's own §8 preamble warns against
   is avoided up front. Colour ruling (S5-W3c design pass, 2026-07-21):
   INDIGO, not the success/green tone this class shipped with originally —
   "already ordered" is an IN-PROGRESS signal (material is on its way, not
   yet received/paid-out), and green is reserved for a genuinely `paid`/
   `completed` state (§8 above) — a green "Already ordered" chip read as a
   false completion signal next to items still mid-fulfilment. */
.b-procured { background: var(--indigo-soft); color: var(--indigo-text); }

/* "Recommended" tag (S5-W3, docs/design/S5-procurement-payment.md §5 item 3
   / D-038) — the wholesale payment-method cards' steer toward BSB/PayTo
   ($0 fees / instant confirmation, D-077-6), NOT an order-state badge (same
   "type label, not a state" precedent as `.b-xtra-*` above). Indigo — Trade's
   own accent/primary — rather than `.b-paid`'s green: green is reserved for
   an order that has actually been paid (§8 above); a "Recommended" tag on a
   still-unpaid choice must not borrow that same completion colour. */
.b-recommended { background: var(--indigo-soft); color: var(--indigo-text); }

/* --------------------------------------------------------------------------
   9. App shell
   -------------------------------------------------------------------------- */
.shell {
  display: grid;
  grid-template-columns: var(--sidebar-w) 1fr;
  min-height: 100vh;
}

.sidebar {
  position: sticky; top: 0; height: 100vh;
  display: flex; flex-direction: column;
  background: var(--surface);
  border-right: 1px solid var(--border);
  overflow-y: auto;
}

/* Shell branding rule (D-069 point 3): the brand slot at the top of the
   sidebar is TENANT identity (the signed-in account's own name), never the
   platform's — the platform's own name/attribution lives ONLY in
   `.sidebar-foot` at the bottom. Store (stage 1, shipped): `.brand-mark` /
   `.brand-name` show the signed-in STORE's initials/trading name
   (apps/store/js/app.js's renderShellBrand()), falling back to "Aura Trade"
   only while signed out; `.brand-sub` is unused there — the old "STORE
   PORTAL" subline is gone, not replaced. Supplier (D-087, shipped): the SAME
   rule — `.brand-mark` / `.brand-name` show the signed-in WHOLESALER's
   initials/company name (app.js's renderShellBrand(), from
   supplierSettings.companyInfo.tradingName -> suppliers.name), "SUPPLIER
   CONSOLE" subline gone; `.brand-sub` now used only by the admin shell, whose
   white-label pass is still separate, later work. */
.brand { display: flex; flex-direction: column; gap: 2px; padding: 20px 20px 16px; }
.brand-row { display: flex; align-items: center; gap: 10px; }
.brand-mark {
  flex: 0 0 auto;
  width: 36px; height: 36px; border-radius: var(--radius);
  background: var(--accent); color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 14px;
}
.brand-name { font-size: 15px; font-weight: 700; letter-spacing: -.01em; }
.brand-sub { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }

.nav-group {
  padding: 12px 20px 4px;
  font-size: 11px; font-weight: 600; color: var(--muted-soft);
  text-transform: uppercase; letter-spacing: .06em;
}

.nav-item {
  display: flex; align-items: center; gap: 12px;
  min-height: var(--control-h); padding: 0 16px; margin: 0 8px;
  border-radius: var(--radius);
  font-size: 14px; font-weight: 500; color: var(--nav-text);
  text-decoration: none;
  transition: background var(--motion-base) var(--ease-standard),
              color var(--motion-base) var(--ease-standard);
}
.nav-item:hover { background: var(--bg); color: var(--primary); }
.nav-item.active { background: var(--accent-soft); color: var(--accent-hover); font-weight: 600; }

.nav-ico {
  flex: 0 0 auto;
  width: 28px; height: 28px; border-radius: var(--radius);
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 700;
  background: var(--bg); color: var(--muted);
}
.nav-item.active .nav-ico { background: var(--accent); color: #fff; }

.nav-text { flex: 1 1 auto; }

/* Platform attribution home (D-069 point 3, see the `.brand` rule above for
   the full split): "Aura Trade" + shell role + version — two stacked lines
   in the store shell (index.html), plain child <div>s, no new class needed. */
.sidebar-foot {
  margin-top: auto; padding: 16px 20px;
  border-top: 1px solid var(--border);
  font-size: 12px; color: var(--muted);
}

.main { display: flex; flex-direction: column; min-width: 0; }

/* Anti-liquid-glass topbar (trend 7, D-043 refinement round): a translucent
   --surface-raised base + gaussian blur + a hairline bottom border — no
   refraction/distortion, no excess transparency, so text over a dense table
   scrolling underneath stays AA. Browsers without backdrop-filter support
   just get the opaque --surface-raised background (still AA-safe). */
.topbar {
  position: sticky; top: 0; z-index: 5;
  height: 60px; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; padding: 0 24px;
  background: var(--surface-raised);
  border-bottom: 1px solid var(--border);
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .topbar {
    background: color-mix(in srgb, var(--surface-raised) 66%, transparent);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
  }
}

.content { flex: 1 1 auto; padding: 24px; min-width: 0; }

.user-chip { display: flex; align-items: center; gap: 10px; }
.avatar {
  flex: 0 0 auto;
  width: 32px; height: 32px; border-radius: 999px;
  background: var(--accent-soft); color: var(--accent-hover);
  display: flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 700;
}

/* Page-level title, tier 1 of the title hierarchy (D-069, full rule in
   section 5's card comment) — names the active page (Dashboard/Orders/
   Customers/Settings in the store shell), set per view by the app's own
   view-switch function. Never repeated as a page's own `.card-title`. */
.eyebrow { font-size: 11px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; }

/* --------------------------------------------------------------------------
   10. Utilities
   -------------------------------------------------------------------------- */
.row { display: flex; align-items: center; gap: 10px; }
.between { display: flex; align-items: center; justify-content: space-between; }
.muted { color: var(--muted); }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.section-title { font-size: 13px; font-weight: 700; color: var(--primary); margin: 0 0 12px; }

/* --------------------------------------------------------------------------
   11. Loading overlay
   -------------------------------------------------------------------------- */
.load-overlay {
  position: fixed; inset: 0; z-index: 50;
  display: flex; align-items: center; justify-content: center;
  background: var(--bg);
}
.spinner {
  width: 32px; height: 32px;
  border: 3px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 999px;
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* --------------------------------------------------------------------------
   12. Focus ring (accessibility, D-043 backlog item)
   Keyboard-only ring (:focus-visible, not :focus) on .btn/.nav-item so a
   mouse click doesn't leave a lingering ring; .input is included for the
   same accent-ring recipe, on top of its own always-on :focus state
   (section 7), which intentionally still fires for pointer clicks too —
   the expected pattern for text fields.
   -------------------------------------------------------------------------- */
.btn:focus-visible,
.input:focus-visible,
.nav-item:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--accent-ring);
}

/* --------------------------------------------------------------------------
   13. Motion & reduced-motion (trend 2/4, D-043 refinement round)
   Every transition above is built from --motion-*/--ease-standard tokens
   (section 1) precisely so this single query can zero all of them for users
   who opt out of motion — nothing else in the file needs to change.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* --------------------------------------------------------------------------
   14. Responsive (D-040)
   Desktop (>=1200px): sidebar pinned at --sidebar-w, full nav labels.
   Tablet (768-1199px): sidebar collapses to a --sidebar-w-rail icon rail
   (labels hidden, icons centered); content takes the freed width.
   <768px (MVP usable degradation): inherits the tablet icon rail — still
   functional, not pixel-tuned.
   -------------------------------------------------------------------------- */
@media (max-width: 1199px) {
  .shell { grid-template-columns: var(--sidebar-w-rail) 1fr; }

  .brand { padding: 16px 0; align-items: center; }
  .brand-row { justify-content: center; }
  .brand-name, .brand-sub, .nav-group { display: none; }

  .nav-item { justify-content: center; padding: 0; }

  /* Visually hide nav labels (icon rail) without removing them from the
     accessibility tree — screen readers still announce the destination. */
  .nav-text {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; padding: 0; margin: -1px;
  }

  .sidebar-foot { padding: 12px 8px; text-align: center; }

  .topbar { padding: 0 16px; }
  .content { padding: 16px; }

  .grid-2 { grid-template-columns: 1fr; }

  /* Table responsive pattern, defined once here (see section 6 comment). */
  .table .col-optional { display: none; }
}

/* --------------------------------------------------------------------------
   15. Dark mode — preallocated skeleton, NOT implemented (D-043: predefined
   token structure, non-critical prep only). Every color already lives
   behind a var(--token), so turning this on later is "fill in these values
   and lift the comment markers" — no component rule needs to change.
   Left commented out on purpose: no values chosen yet, no dark-mode visual
   QA has been done, MVP ships light-only, no default dark theme.
   -------------------------------------------------------------------------- */
/*
@media (prefers-color-scheme: dark) {
  :root {
    --primary:        ;
    --surface:        ;
    --surface-raised: ;
    --bg:             ;
    --border:         ;
    --border-strong:  ;
    --muted:          ;
    --muted-soft:     ;
    --nav-text:       ;
    --accent:         ;
    --accent-hover:   ;
    --accent-soft:    ;
    --accent-ring:    ;

    --success: ; --success-soft: ; --success-text: ;
    --warning: ; --warning-soft: ; --warning-text: ;
    --info:    ; --info-soft:    ; --info-text:    ;
    --danger:  ; --danger-soft:  ; --danger-text:  ;
    --neutral: ; --neutral-soft: ; --neutral-text: ;
    --indigo:  ; --indigo-soft:  ; --indigo-text:  ;

    --shadow-hover: ;
    --shadow-pop:   ;
  }
}
*/

/* --------------------------------------------------------------------------
   16. Icon system (B-1, S2 design slice)
   Linear inline SVGs sourced from common/js/icons.js — icon(name) returns a
   self-contained <svg> string (24x24 viewBox, stroke="currentColor",
   stroke-width 1.75, fill: none). Sidebars inline that same markup as static
   HTML (no JS render dependency). These rules only size/align the SVG once
   it lands in markup; color always comes from the wrapping element's CSS
   `color` (currentColor) — never hardcoded here.
   -------------------------------------------------------------------------- */
:root {
  --icon-size:    20px; /* standard inline icon — nav rail, buttons, table toolbar */
  --icon-size-lg: 40px; /* state-block icon slot (section 18: empty/error/denied) */

  /* Skeleton shimmer sweep duration (section 17) — a deliberately slower,
     distinct scale from the UI-transition --motion-fast/base/slow tokens
     above (section 1), which are tuned for button/hover feedback, not a
     multi-second loading sweep. Still a motion token, so it is zeroed the
     same way under prefers-reduced-motion (section 13). */
  --motion-shimmer: 1600ms;
}

.nav-ico svg { width: var(--icon-size); height: var(--icon-size); display: block; }

/* --------------------------------------------------------------------------
   17. Loading — skeleton rows (B-2)
   Preferred over a spinner for predictable list/table loads (Codex #4,
   trend 2): the eventual row shape is already known, so a shimmering
   placeholder reads as "this is loading", not "something is stuck".
   -------------------------------------------------------------------------- */
.skeleton {
  position: relative;
  display: block;
  overflow: hidden;
  height: 14px;
  border-radius: 4px;
  background: var(--border);
}
.skeleton::after {
  content: '';
  position: absolute; inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .6), transparent);
  animation: skeleton-shimmer var(--motion-shimmer) var(--ease-standard) infinite;
}
@keyframes skeleton-shimmer { to { transform: translateX(100%); } }

/* Drop a <tr class="skeleton-row"> straight into a real <table class="table">
   tbody — it inherits actual cell padding/border from section 6, so loading
   rows sit at the exact height/rhythm of the loaded rows that replace them.
   Give each .skeleton bar an inline width (e.g. style="width:60%") per
   column to approximate that column's real content. */
.skeleton-row .skeleton { width: 100%; }

/* Spinner (section 11, .load-overlay/.spinner) stays reserved for
   button-level use — an inline, shrunk .spinner swapped in next to/instead
   of a .btn label while a single action is submitting, e.g.:
   <button class="btn btn-primary" disabled>
     <span class="spinner" style="width:16px;height:16px;border-width:2px;"></span> Saving…
   </button>
   Skeleton rows (above) are for list/table loads, where the row shape is
   already known; the button spinner is for a single in-flight action. */

/* --------------------------------------------------------------------------
   18. State blocks (B-2) — empty / error / permission-denied placeholders
   for list views. Composition: icon slot (.state-icon, filled with an
   icons.js SVG) + title + hint text + an optional CTA button slot
   (.state-cta, holding a normal .btn). Variant classes tone the icon slot
   only — title/hint/CTA markup is identical across variants.
   -------------------------------------------------------------------------- */
.state-block {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 6px; padding: 48px 24px; max-width: 360px; margin: 0 auto;
}
.state-icon {
  width: 64px; height: 64px; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  margin-bottom: 6px;
}
.state-icon svg { width: var(--icon-size-lg); height: var(--icon-size-lg); }
.state-title { font-size: 15px; font-weight: 700; color: var(--primary); }
.state-hint { font-size: 13px; color: var(--muted); }
.state-cta { margin-top: 14px; }

/* empty: neutral — nothing is wrong, there is just nothing here yet. */
.state-empty .state-icon { background: var(--neutral-soft); color: var(--muted); }
/* error: danger-toned — a real failure (network/server) the user can retry. */
.state-error .state-icon { background: var(--danger-soft); color: var(--danger-text); }
/* denied: lock icon + muted tone (NOT danger) — an access/permission fact,
   not a fault; nothing to retry. */
.state-denied .state-icon { background: var(--neutral-soft); color: var(--muted); }

/* --------------------------------------------------------------------------
   19. Store workspace (S4, docs/design/S4-store-workspace.md §5) — the V3
   order editor's two-column layout and its layered price panel. Additive
   only; no existing component contract above is touched.
   -------------------------------------------------------------------------- */

/* .workspace-split — generic two-pane grid for the order editor (list |
   editor). Desktop (>=1200px, matches the shell's own breakpoint, section
   14): fixed 40/60 split via `fr` units (accounts for `gap` automatically,
   unlike a raw 40%/60%). <=1199px: both children stack full-width, in
   source order. Works with any two direct children — no required child
   class names, so callers can wrap a list card and an editor card as-is. */
.workspace-split {
  display: grid;
  grid-template-columns: 2fr 3fr;
  gap: 20px;
  align-items: start;
}
.workspace-split > * { min-width: 0; }
@media (max-width: 1199px) {
  .workspace-split { grid-template-columns: 1fr; }
}

/* .totals-dock — order-total summary bar for the editor pane. Static (part
   of normal flow) at desktop width, where the editor column is short enough
   to stay in view; docks to the viewport bottom only at tablet/narrow width
   (<=1199px, once .workspace-split has stacked to a single column and the
   editor can scroll long), so the running total stays visible without
   scrolling back up. */
.totals-dock {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  background: var(--surface-raised); border-top: 1px solid var(--border);
  padding: 14px 20px;
}
@media (max-width: 1199px) {
  .totals-dock {
    position: sticky; bottom: 0; z-index: 4;
    box-shadow: var(--shadow-hover);
  }
}

/* .price-tier-panel — layered price panel (S4-store-workspace.md §5): the
   retail sell price is the only thing shown open by default (large, bold);
   the wholesale/discount/markup breakdown that produced it collapses into
   a native <details> ("Cost breakdown") so a retailer standing next to a
   walk-in customer never has the wholesale cost on screen unless they
   deliberately open it. Composition:
     <div class="price-tier-panel">
       <div class="price-tier-retail">
         <span class="label">Retail price</span>
         <span class="price-tier-amount t-mono">$113.05</span>
       </div>
       <details class="price-tier-details">
         <summary>Cost breakdown</summary>
         <div class="price-tier-rows"> ...rows built from .between + .muted/.t-mono... </div>
       </details>
     </div>
   Native <details>/<summary> chosen over a JS-driven collapse (no script
   dependency, keyboard/AT support for free) — see file header house rules. */
.price-tier-panel {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: 16px 18px;
}
.price-tier-retail { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.price-tier-amount { font-size: 28px; font-weight: 800; color: var(--primary); letter-spacing: -.02em; }
.price-tier-details { margin-top: 14px; border-top: 1px solid var(--border); padding-top: 12px; }
.price-tier-details summary {
  display: flex; align-items: center; gap: 6px; cursor: pointer;
  font-size: 12px; font-weight: 600; color: var(--muted);
  text-transform: uppercase; letter-spacing: .05em;
  list-style: none;
  transition: color var(--motion-base) var(--ease-standard);
}
.price-tier-details summary::-webkit-details-marker { display: none; }
.price-tier-details summary::before {
  content: '▸'; font-size: 10px; line-height: 1;
  transition: transform var(--motion-base) var(--ease-standard);
}
.price-tier-details[open] summary::before { transform: rotate(90deg); }
.price-tier-details summary:hover { color: var(--primary); }
.price-tier-rows { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; font-size: 13px; }

/* --------------------------------------------------------------------------
   20. Order editor — full-screen workspace shell, main grid, two-segment
   header (D-059/D-060 UI batch; docs/reference/retail-order-layout-spec.md
   §1/§2 supplied the numbers, ADS v2 tokens supply every value — none of
   Retail's DOM ids or component class strings are reused, per the spec's
   own "don't copy" list). Supersedes the S4.1-era `.order-rail-split`
   rules (kept the class NAME, rewrote the values/behaviour).

   `.workspace-fullscreen` (D-059-4): a body-level class toggled by
   `showView()` only while the order editor view is active — Trade's OWN
   full-screen mechanism (a class, not a shell swap), same intent as
   Retail's FULLSCREEN_VIEWS/#app-container path (hide the side menu so an
   accidental tap can't leave the editor mid-edit) — the header's own Back
   button (below) stays the one exit path. No other view is affected.

   `.ws-root` (spec §1 `.aura.ws-root`, numbers only — this app does not
   inherit the spec source's own compound-selector bug, its "don't copy"
   list item 1): centers the editor at a readable max width.

   `.order-rail-split` (spec §1 `.aura .ws`): 1fr + a rail column. The
   spec's own rail is 300px; this rail is widened to 320px — it carries a
   few longer label rows than Retail's own ("Subtotal (ex-GST)" etc). The
   breakpoint stays this app's EXISTING 1199px shell breakpoint (section 14)
   rather than adopting the spec's separate 1000px value, for one consistent
   ADS-wide breakpoint rather than a second one-off number. `.ws-work` (spec
   §1 `.ws-work`) is the left column's own flex stack (note card + items
   card); the rail's sticky/tile rules live in section 22, beside the rest
   of its own content.
   -------------------------------------------------------------------------- */
body.workspace-fullscreen .sidebar { display: none; }
body.workspace-fullscreen .shell { grid-template-columns: 1fr; }

/* Compact menu state — a middle ground between the full sidebar and
   `.workspace-fullscreen` above: instead of hiding the menu it COLLAPSES the
   sidebar to the SAME 64px icon rail the tablet breakpoint (§14) already ships,
   so navigation stays in place (every nav icon is a live exit) while the page
   gets most of the freed horizontal space.
   CURRENTLY UNUSED — no app toggles `menu-compact` today. It shipped for the
   supplier Products workspace (D-091 addendum) and that page became the D-092
   two-stage picker + full-hide editor one day later; D-092 point 4 explicitly
   keeps these rules in the library for a future navigate-and-browse deep page.
   These declarations mirror §14's tablet rail rules on purpose — keep the two in
   lockstep. The footer version line is dropped in compact (it still shows in the
   wide desktop menu and stays visible at the tablet breakpoint). At <=1199px the
   tablet rules already render this exact rail, so at tablet widths this block is
   a no-op layered on identical declarations — the two never conflict. */
body.menu-compact .shell { grid-template-columns: var(--sidebar-w-rail) 1fr; }
body.menu-compact .brand { padding: 16px 0; align-items: center; }
body.menu-compact .brand-row { justify-content: center; }
body.menu-compact .brand-name,
body.menu-compact .brand-sub,
body.menu-compact .nav-group { display: none; }
body.menu-compact .nav-item { justify-content: center; padding: 0; }
/* Visually hide nav labels (icon rail) without removing them from the a11y
   tree — the label still reads to screen readers, and each nav item's `title`
   (index.html) gives sighted users a hover/focus tooltip. */
body.menu-compact .nav-text {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; padding: 0; margin: -1px;
}
body.menu-compact .sidebar-foot { display: none; }

.ws-root { max-width: 1280px; margin: 20px auto 60px; }

.order-rail-split {
  display: grid;
  grid-template-columns: 1fr 320px;
  gap: 18px;
  align-items: start;
}
.order-rail-split > * { min-width: 0; }

.ws-work { display: flex; flex-direction: column; gap: 16px; min-width: 0; }

@media (max-width: 1199px) {
  .order-rail-split { grid-template-columns: 1fr; }
}

/* Header (spec §2): a two-segment card — `.ws-bar` (Back + title/doc# on
   the left) on top, the stage flowchain strip (section 21) below it. The
   divider between the two segments is `.ws-bar`'s own border-bottom, NOT a
   border-top re-added onto the flowchain strip — `.stage-flow` (section 21)
   keeps the no-top-border shape the founder asked for (D-055 round 4); the
   dividing line still exists, it just belongs to the segment above it, not
   the strip itself. This app has no header-right primary action (unlike
   Retail's "Save" — an item persists as soon as its own dialog is
   confirmed, there is no whole-order manual save button), so `.ws-bar`
   renders only its left group; `justify-content: space-between` still
   reads correctly with a single child, no empty placeholder needed. */
.ws-head {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  margin-bottom: 18px;
}
.ws-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; flex-wrap: wrap;
  padding: 14px 22px;
  border-bottom: 1px solid var(--border);
}
.ws-bar h1 { font-size: 18px; font-weight: 800; margin: 0; letter-spacing: -.01em; }
.ws-quote { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 4px; }

/* --------------------------------------------------------------------------
   21. Stage flowchain (D-055 three-round correction, 2026-07-19) — a
   compact chevron flowchain, rebuilt on Retail's own order-page flowchain
   shape (per the founder's direct callout) in place of the earlier
   full-width badge-strip draft (2026-07-18 addendum, kept only in history).
   Copy-and-adapt, not shared code — this is Trade's own markup/CSS.

   `.stage-flow` is the outer slim strip: a thin, edge-to-edge band (no
   card border/radius — just a hairline top rule) holding the chevron
   chain on the left and an optional one-line meta (`.stage-flow-meta`,
   e.g. "Updated ...") on the right, when the loaded order already carries
   `updatedAt` — no new data plumbing added for this.

   `.stage-flow-chain` is the <ol> chain: a fixed-width (520px, capped at
   100%) rounded strip; `overflow:hidden` on it is what turns each step's
   pointed corners into a smooth pill outline at the two ends. Each
   `.stage-flow-step` <li> is a six-point clip-path chevron (point out the
   right edge, notch in the left edge); a -11px margin pulls every step
   under the previous one's point, and a descending z-index (first step
   highest) keeps every point visually on top of the next step's notch —
   first/last steps drop the notch/point they don't need so the chain has
   flat outer edges.

   The eight-step color ramp is a flowchain-only hex ramp (light tint ->
   Trade's own indigo accent hue), hand-picked for this component alone —
   not general design-system tokens, same one-off approach Retail takes
   for its own flowchain. `.step-inactive` (future steps) overrides the
   ramp back to neutral; it carries the same selector "weight" (one class
   + one pseudo-class) as the ramp rules above it and simply comes later
   in the file, so it wins the cascade on source order alone — no
   `!important` needed. Done/current steps get no separate tint of their
   own: the last non-inactive step reads as "current" by construction
   (mirrors Retail — no invented visual language beyond the ramp).

   D-059/D-060 integration note: this strip is now the second segment of
   `.ws-head` (section 20) — mounted straight into `#oe-stage-flow` as
   before, no markup change here, just a new parent. It still carries no
   border of its own; the segment divider is `.ws-bar`'s border-bottom.

   Admin escape-hatch click affordance (D-068 point 1, founder S4.2 feedback):
   `.is-admin` on every `<li>` when the signed-in account is storeRole=='admin'
   (app.js renderStageFlow()) — cursor + hover lift, so the chain visibly
   reads as clickable for that role only. Clicking force-writes `stage`
   directly, bypassing every business gate (a second channel alongside the
   gated tiles below), mirroring Retail's own admin-only flowchain click
   (docs/reference/retail-order-ui-intel.md §5.2). Sales renders the
   identical markup with no `.is-admin` class — chain stays purely
   informational, same as before this patch.
   -------------------------------------------------------------------------- */
.stage-flow {
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;
  padding: 10px 22px;
  background: var(--bg);
  /* border-top removed per founder (D-055 round 4, 2026-07-19) */
}
.stage-flow-meta { font-size: 12px; color: var(--muted-soft); white-space: nowrap; }

.stage-flow-chain {
  display: flex;
  width: 520px; max-width: 100%;
  overflow: hidden;
  margin: 0; padding: 0; list-style: none;
  border-radius: var(--radius);
  box-shadow: 0 1px 2px rgba(15, 23, 42, .06);
}
.stage-flow-step {
  /* D-094-③ (founder test day, 2026-07-27): `flex: 1` alone did NOT give
     equal chevrons — a flex item cannot shrink below its own min-content
     width, and with `white-space: nowrap` that floor is the full label, so
     the longest step ("In production") stole width from the others and broke
     the chain's rhythm. `min-width: 0` removes that floor: flex-basis 0 +
     grow 1 + no floor = exactly equal steps, always, at any step count.
     The type is then sized so the longest label FITS that equal step rather
     than overflowing it (the clip-path would cut it off, silently).
     Padding tightened 16/4 -> 13/3 for the same reason (13px still clears
     the 11px notch, and the text's optical offset is unchanged at 5px).
     Caps are gone from this component for the same reason and stay gone
     (a 13-character label in caps needs 79.3px at 9px/.06em).

     D-095 re-measure (the store chain went from seven steps to eight): the
     chain is a fixed 520px, so each added step takes room from all of them —
     one step's content room fell from 67.7px (seven) to 58.6px (eight), and
     "In production" at 9px/.04em renders 63.0px, i.e. it overflowed by 4.4px
     and lost its last letter behind the chevron point. Measured in-browser
     with Inter loaded, 8px/.04em renders it at 56.0px = 2.6px of headroom, so
     the type drops one pixel rather than the chain widening: at 768 the band
     has 690px of content width and the 520px chain still shares one line with
     the "Updated ..." meta (144px) — a 600px chain would have wrapped it.
     One size for every chain everywhere, as before: this also closes a
     pre-existing 2.5px overflow on the supplier's own six-step wholesale
     track ("Pending payment", 82.4px at 9px -> 73.2px at 8px, §27.8). */
  flex: 1 1 0;
  min-width: 0;
  height: 26px; line-height: 26px;
  font-size: 8px; font-weight: 700; letter-spacing: .04em;
  text-align: center; white-space: nowrap;
  padding: 0 3px 0 13px;
  margin-left: -11px;
  clip-path: polygon(0% 0%, calc(100% - 11px) 0%, 100% 50%, calc(100% - 11px) 100%, 0% 100%, 11px 50%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18);
}
.stage-flow-step:first-child {
  margin-left: 0;
  clip-path: polygon(0% 0%, calc(100% - 11px) 0%, 100% 50%, calc(100% - 11px) 100%, 0% 100%);
}
.stage-flow-step:last-child {
  clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%, 11px 50%);
}

/* Flowchain-only ramp, light -> dark; z-index descends left-to-right so
   each step's point sits above the next step's notch. EIGHT steps for the
   store's eight-state stage machine (draft..completed with D-095's
   'dispatched', apps/store/js/orders/stage-gates.js STORE_ORDER_STAGES) —
   grown from five (S6 visual close-out) to seven (D-071's enum) to eight
   here, each time for the same reason: a step with no nth-child rule of its
   own renders UNCOLOURED with a broken z-order (its neighbour's point cuts
   into it), so the ramp must always be at least as long as the longest chain
   that uses this component. Shorter chains (the supplier's six-step
   wholesale track, §27.8) simply stop early — the ramp is indexed by
   position, not by any one chain's vocabulary.
   Text flips light->dark at step 4, the first background dark enough to keep
   white AA on the small label (same crossover point the five-step ramp used;
   the label stopped being caps at D-094-③ and dropped to 8px at D-095 —
   the crossover moved at neither). */
.stage-flow-step:nth-child(1) { background: #E3E1F8; color: var(--primary); z-index: 8; }
.stage-flow-step:nth-child(2) { background: #C6C0F1; color: var(--primary); z-index: 7; }
.stage-flow-step:nth-child(3) { background: #A9A2E8; color: var(--primary); z-index: 6; }
.stage-flow-step:nth-child(4) { background: #635AD2; color: #fff;           z-index: 5; }
.stage-flow-step:nth-child(5) { background: #4B41BF; color: #fff;           z-index: 4; }
.stage-flow-step:nth-child(6) { background: #332AA1; color: #fff;           z-index: 3; }
.stage-flow-step:nth-child(7) { background: #201974; color: #fff;           z-index: 2; }
.stage-flow-step:nth-child(8) { background: #150F52; color: #fff;           z-index: 1; }

.stage-flow-step.step-inactive { background: var(--bg); color: var(--muted-soft); box-shadow: inset 0 0 0 1px var(--border); }

/* Admin-only click affordance (D-068 point 1) — see section 21's own comment
   block above for the full rationale. Sales never gets this class. */
.stage-flow-step.is-admin { cursor: pointer; }
.stage-flow-step.is-admin:hover { filter: brightness(1.1); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18), 0 0 0 2px rgba(15, 23, 42, .18); }

/* The <=1199px type/padding override this section used to carry (a narrow-
   width adaptation Retail's own flowchain doesn't have) is retired at
   D-094-③. Its premise no longer holds: the chain is a FIXED 520px strip, so
   measured in-browser it renders at identical geometry on 1024 and 768 as it
   does on a desktop window — the tablet rail collapsing takes nothing away
   from it. The single sizing above already keeps all eight labels intact at
   both of those viewports (re-measured at D-095), and one size everywhere is
   one less rule to drift. (Its padding half is not lost either — 13/3 is now
   the base value.) */

/* --------------------------------------------------------------------------
   22. Order editor — item-section header, rail sticky, summary ledger,
   action tiles (D-059/D-060; spec §3/§4). Numbered main-column sequence
   (D-069 point 2, tier 2 of the title hierarchy — section 5's card comment):
   Client details = 01, items = 02, Extras = 03, each card's own first row
   `.sec-head`: `.sec-num` (left, plain text "0N · <label>" — for items the
   label is this Edition's neutral vocab token, e.g. "Curtains", never the
   wholesale catalog's category name, D-062-2) plus a `.row` of
   section-scoped buttons (right) — items' "+ Add item" and Client details'
   "Edit customer" both live in this same row, next to the content they act
   on (moved out of the rail; D-059 point 5's "若移区头则不重复").

   `.order-rail` is sticky at desktop width (spec §1's rail sticky
   behaviour) and un-stickies at the shared 1199px breakpoint (section 20),
   same width this whole grid already collapses at — it is now a plain
   layout wrapper (no border/background of its own), holding four
   INDEPENDENT `.card`s stacked with its own 16px gap (D-063, 2026-07-19
   founder note: "rail 拆三区,视觉独立" — the old single `.card.order-rail`
   bundle read as one undifferentiated block; money / actions / lifecycle
   info are three different kinds of information and now look like it;
   D-073 point 2 adds the fourth, Order, below — wholesale-ordering buttons
   migrated in from the old left-column "04" card, docs/DECISIONS.md D-073).

   `.order-rail-summary` (zone 1, unchanged content) holds the ledger:
   `.fin` rows (spec §4) — ONLY customer-safe figures (subtotal ex-GST / GST
   / total inc GST). The old "Cost subtotal (internal)" `<details>` is
   REMOVED (D-059 point 2's 2026-07-19 upgrade) — no wholesale/cost figure
   renders on this page any more; that data now lives only inside the Costs
   dialog (section 24).

   `.order-rail-actions` (zone 2) now carries its own `.card-title`
   "Actions" (D-069 point 2 — all three rail cards share one small dark bold
   heading style, tier 3 of the title hierarchy, section 5) above
   `.tiles`/`.tile` (spec §4, 2-column grid — the spec's own correction note,
   NOT a 3x3 grid). D-068 (founder
   S4.2 feedback) fixed the row pairing/order: row 1 Print quote + Print
   invoice; row 2 Photos (disabled placeholder, no feature built yet — see
   its `.rail-gate-notes` entry) + Comments (real feature since D-068 二轮
   增补 — the customer-visible remark printed on Quote/Invoice, locked only
   once the order reaches 'completed', same tier as payments); row 3 the
   current stage-advance tile (Mark as quoted/Confirm/Mark installed/an
   "Installed ✓" achieved placeholder once past that point) + Complete (now
   always visible, "恒 Complete" per D-068's PM judgement, not only once
   stage=='installed'); row 4 Replace fabric + Delete (new, danger-styled,
   draft/quoted only); row 5 Costs (admin-only, unchanged). Markup order in
   index.html IS the row order — hidden tiles (`display:none`, e.g. only one
   of the four stage-advance tiles at a time) simply don't occupy a grid
   cell, so the grid re-flows correctly without any extra layout code. The
   old Revert tile is gone entirely (D-068 point 1): reverting/forcing a
   stage is now an admin-only flowchain click (section 21), not a rail tile.
   A disabled tile still needs to say WHY — every currently-disabled tile's
   reason collects into `.rail-gate-notes` below the grid, one line each,
   rather than a hover-only `title` (useless on an iPad with no mouse).

   `.order-rail-procure` (zone 3, NEW — D-073 point 2) holds the wholesale-
   ordering category buttons that used to live in the main column's own "04"
   card (index.html, now a pure record list — see that card's own comment).
   Shares the same `.card-title` "Order" heading style as every other rail
   zone; its buttons stack vertically (a plain inline flex column, not
   `.tiles`' 2-column grid — a rail-width column has no room for two-across)
   and stay `disabled` until `isOrderLocked(currentOrder)` is true (i.e. the
   order has reached 'confirmed' or later — wholesale ordering UNLOCKS at the
   same boundary everything else in this app LOCKS at), with one
   `.rail-gate-notes` reason line explaining why while they are disabled —
   the same reason-line convention `.order-rail-actions` above uses for its
   own disabled tiles, just scoped to this card instead of the shared
   `#oe-tile-notes` slot (a different card's own gate, not one of the
   Actions grid's tiles). Never swapped out for a `.state-block` (unlike its
   pre-D-073 behaviour) — a 320px rail card has no room for that block's
   360px layout, and "not confirmed yet" is this app's COMMON pre-confirm
   state, not a one-off empty/error/denied case.

   `.order-rail-info` (zone 4, D-063 point 2) holds `.order-info`: a
   small Created / Updated / Quote sent / Invoice sent timestamp ledger, one
   `.order-info-row` per field. Typography follows the layout spec's own
   `.ws-meta` pattern (docs/reference/retail-order-layout-spec.md §2:
   font-size 11px, `var(--muted-soft)`, `tabular-nums`, `white-space:nowrap`)
   — the same small, quiet cadence Retail uses for its own header date line
   — rather than inventing a new type scale for a fourth kind of row. The
   stage-flow strip (section 21) keeps its own compact "Updated ..." meta
   text alongside the flowchain — a deliberate, judged duplication (D-063
   point 3): that copy is a quick glance right next to the flow state, while
   this zone is the complete, structured record; removing "Updated" from
   either one would make it read as incomplete on its own.
   -------------------------------------------------------------------------- */
.sec-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
.sec-num { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); margin: 0; }

.order-rail {
  position: sticky; top: 16px;
  display: flex; flex-direction: column; gap: 16px;
}
/* Tablet degradation (D-040/D-063): once `.order-rail-split` collapses to a
   single column (section 20's shared 1199px breakpoint) the rail is no
   longer a slim 320px sidebar — it has the full content width to work with,
   so its now-THREE cards lay out as a wrapping row instead of one tall
   vertical stack (a plain column here would read as an oddly long, empty
   tail below the items card). `flex: 1 1 260px` lets each card wrap onto
   its own row once three no longer fit at 768px portrait width. */
@media (max-width: 1199px) {
  .order-rail { position: static; flex-direction: row; flex-wrap: wrap; }
  .order-rail-summary, .order-rail-actions, .order-rail-procure, .order-rail-info { flex: 1 1 260px; min-width: 240px; }
}

.order-rail-summary { display: flex; flex-direction: column; gap: 2px; }

.fin { display: flex; justify-content: space-between; align-items: center; padding: 7px 0; font-size: 13.5px; }
.fin .lbl { color: var(--muted); text-transform: uppercase; font-size: 11px; font-weight: 600; letter-spacing: .04em; }
.fin.grand { border-top: 1px solid var(--border); margin-top: 4px; padding-top: 11px; }
.fin.grand .val { font-size: 15px; font-weight: 700; color: var(--primary); }

.tiles { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.tile {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
  min-height: 70px; padding: 8px; text-align: center;
  border: 1px solid var(--border); border-radius: 10px;
  background: var(--surface); color: var(--primary);
  font-family: inherit; font-weight: 600; font-size: 12.5px; cursor: pointer;
  transition: background var(--motion-base) var(--ease-standard),
              border-color var(--motion-base) var(--ease-standard);
}
.tile svg { width: 22px; height: 22px; }
.tile:hover { background: var(--bg); border-color: var(--border-strong); }
.tile:disabled { opacity: .5; cursor: not-allowed; }
.tile:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-ring); }

.rail-gate-notes { display: flex; flex-direction: column; gap: 4px; margin-top: 10px; }
.rail-gate-notes p { font-size: 11.5px; color: var(--warning-text); margin: 0; }

.order-info { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
.order-info-row { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.order-info-row .lbl { color: var(--muted); text-transform: uppercase; font-size: 11px; font-weight: 600; letter-spacing: .04em; }
.order-info-row .val { font-size: 11px; color: var(--muted-soft); font-variant-numeric: tabular-nums; white-space: nowrap; }

/* --------------------------------------------------------------------------
   23. Item row cards (`.irow`, spec §6) — the items card's per-line layout:
   index / main content / right-aligned price+actions, separated by
   border-bottom (replaces the earlier per-row `.card` + gap list). Price
   only ever shows `itemRetail` (17px/700) — no fabric/track cost figures
   render here any more (D-059 point 2's 2026-07-19 upgrade); that detail
   moved into the Costs dialog (section 24).
   -------------------------------------------------------------------------- */
.irow { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; padding: 14px 0; border-bottom: 1px solid var(--border); }
.irow:last-child { border-bottom: 0; }
.irow-idx { width: 26px; flex: none; font-size: 13px; font-weight: 700; color: var(--muted-soft); }
.irow-main { min-width: 0; flex: 1; }
.irow-right { flex: none; text-align: right; }

/* --------------------------------------------------------------------------
   24. Modal system — semantic sizes, 3-column grid, head/body/foot rhythm
   (D-059 points 1/3; spec §5). Every dialog's outer <dialog> stays
   borderless/transparent (inline style, unchanged) so `.modal-card` carries
   all the visual chrome; `.modal-sm/-md/-lg` replace one-off inline
   `width:` values — the spec's own "don't copy" list item 2 calls out
   Retail's nine dialogs each inlining their own max-width. `.mgrid` is the
   wide-dialog 3-column field grid (the item-edit dialog uses `.modal-lg`,
   D-059 point 3), collapsing to one column at <=700px, same breakpoint the
   spec itself uses.

   Backdrop click is deliberately NOT wired to close ANY dialog in this app
   (D-059 point 1 — too easy to mis-tap on an iPad); ESC still closes it
   (native <dialog> behaviour, left alone per the founder's ruling) and
   every dialog keeps an explicit ×/Cancel button. `dialog::backdrop` only
   sets the overlay's own tone (the spec's `.omodal` rgba value) — it is
   never a click target here.
   -------------------------------------------------------------------------- */
.modal-card {
  width: 100%; margin: 24px auto;
  display: flex; flex-direction: column;
  max-height: calc(100vh - 48px);
  background: var(--surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-pop);
  overflow: hidden;
}
/* Fixed widths (founder, 2026-07-20): native <dialog> defaults to
   fit-content, so max-width alone let content (long fabric names) grow or
   shrink the dialog between states. Lock each size to its full width so a
   dialog opens at its final size and never resizes — iPad-friendly. */
.modal-sm { width: min(480px, 92vw); max-width: 480px; }
.modal-md { width: min(640px, 92vw); max-width: 640px; }
.modal-lg { width: min(900px, 92vw); max-width: 900px; }

.modal-head { padding: 18px 22px; border-bottom: 1px solid var(--border); flex: none; }
.modal-head h3 { font-size: 17px; font-weight: 700; margin: 0; }
.modal-body { flex: 1 1 auto; padding: 22px; overflow-y: auto; }
.modal-foot { padding: 16px 22px; border-top: 1px solid var(--border); background: var(--bg); flex: none; display: flex; gap: 10px; justify-content: flex-end; }

.mgrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
/* Grid children default to min-width:auto, which lets a long <option> label
   push its column (and the whole dialog) wider — clamp so text truncates
   inside the fixed dialog width instead (same founder fix as above). */
.mgrid > * { min-width: 0; }
.mgrid select, .mgrid input { max-width: 100%; }
@media (max-width: 700px) {
  .mgrid { grid-template-columns: 1fr; }
}

dialog::backdrop { background: rgba(15, 23, 42, .6); }

/* --------------------------------------------------------------------------
   25. Lock banner (D-073 point 1, docs/DECISIONS.md D-073) — a thin,
   single-line "this list is locked" notice for a list that stays fully
   visible underneath it (the Items card, confirmed+). Replaces the earlier
   full `.state-block` (section 18) for that ONE case: once an order is
   confirmed, "items are locked" is this app's COMMON steady state for the
   rest of the order's life, not an empty/error/denied one-off — a 48px-
   padded icon block was eating half the screen above a list that can hold
   dozens of priced rows. Icon + one line of text only, no title/hint split,
   no CTA slot; every OTHER `#oe-items-gate-slot` branch (pricing engine
   unavailable / retail markup not configured) still uses the full
   `.state-block` — those are real blocking states with no item list to
   protect underneath (nothing is priced yet at that point).
   -------------------------------------------------------------------------- */
.lock-banner {
  display: flex; align-items: center; gap: 8px;
  background: var(--neutral-soft); border-radius: var(--radius);
  padding: 9px 12px; margin-bottom: 14px;
  font-size: 12.5px; font-weight: 600; color: var(--muted);
}
.lock-banner svg { width: 16px; height: 16px; flex: none; }

/* --------------------------------------------------------------------------
   26. Wholesale payment — method cards + BSB reference callout (S5-W3c
   design pass, docs/design/S5-procurement-payment.md §5 items 3/5,
   D-038/D-077-6) — both live inside `#oe-wholesale-pay-modal` (`.modal-md`).

   `.paycards`/`.paycard` (renderWholesalePayMethodCards(), apps/store/js/
   app.js): a plain `.card` stack read inconsistent once BSB/PayTo/Card
   pitch copy varied in length — `.paycard` fixes a common min-height and
   lays out as a column (title row / fee line / one-line pitch / CTA) with
   the CTA pinned to the bottom via `margin-top: auto`, so every card in the
   stack reads as one uniform row of choices regardless of copy length.
   `.paycard-recommended` is the founder's steer toward BSB/PayTo ($0 fees /
   instant confirmation) — an indigo (Trade's own accent) border + the
   `.b-recommended` tag (§8), NOT `.b-paid`'s green (that tone is reserved
   for an order actually paid). `.paycard.is-disabled` is PayTo's over-the-
   cap state (§8-1, PAYTO_MAX_DISPLAY) — the card dims as a whole (its CTA
   already greys via `.btn:disabled`) and drops its own `.b-recommended` tag
   (renderWholesalePayMethodCards() withholds it in that branch — a greyed-
   out card recommending itself would read as contradictory). Every CTA uses
   the plain, un-suffixed `.btn` (48px), not `.btn-sm` — this is the primary
   tap target of the whole screen, sized for an iPad finger, not a dense
   inline row action (contrast the 04-card row actions, deliberately
   `.btn-sm`, its own comment in this app's app.js).

   `.bsb-ref` (openWholesalePayBsbScreen()): the transfer-reference callout
   — large tabular-mono digits on an indigo-tinted panel, the single largest,
   highest-contrast element on the whole BSB screen (S5 §5 item 5: "大字
   「转账备注 = 订单号」" — must be the unmissable visual focus, not a plain
   inline sentence). `.status-note`/`.status-note-awaiting`: the screen's
   "what happens next" line, promoted from a plain muted sentence to a small
   tinted card so the (post-declare) `awaiting_transfer` state reads as an
   actual status, not incidental copy — `.status-note-awaiting` (warning
   tone) is toggled on only in 'view' mode (an order already awaiting
   transfer), matching `.b-awaiting-transfer`'s own tone (§8); 'declare' mode
   (before the founder has clicked "I have transferred") stays the neutral
   `--bg` tone since nothing is actually awaiting anything yet.
   -------------------------------------------------------------------------- */
.paycards { display: flex; flex-direction: column; gap: 12px; }
.paycard {
  display: flex; flex-direction: column; gap: 8px;
  min-height: 152px; padding: 16px 18px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
}
.paycard-recommended { border-color: var(--indigo); border-width: 1.5px; }
.paycard.is-disabled { opacity: .6; }
.paycard-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.paycard-fee { font-size: 13px; font-weight: 700; color: var(--indigo-text); margin: 0; }
.paycard-desc { font-size: 12.5px; margin: 0; }
.paycard-btn { margin-top: auto; align-self: flex-start; min-width: 200px; }

.bsb-ref {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  margin-top: 16px; padding: 18px 16px;
  background: var(--indigo-soft); border: 1px solid var(--indigo); border-radius: var(--radius-lg);
  text-align: center;
}
.bsb-ref-lbl { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--indigo-text); margin: 0; }
.bsb-ref-val { font-family: var(--font-mono); font-size: 32px; font-weight: 800; letter-spacing: .02em; color: var(--primary); margin: 0; }

.status-note { margin-top: 16px; padding: 12px 14px; border-radius: var(--radius); background: var(--bg); }
.status-note p { font-size: 12.5px; color: var(--muted); margin: 0; }
.status-note-awaiting { background: var(--warning-soft); }
.status-note-awaiting p { color: var(--warning-text); }

/* --------------------------------------------------------------------------
   27. Supplier console — S6 visual close-out (queue, order detail, allowance
   matrix, ad-hoc confirm modals). Additive only; all values from §1 tokens.
   The supplier console is the fuller wholesaler BACKEND (contrast the light
   store ordering FRONTEND) — these are the components its five S6 views need
   that the shared set above did not already cover. Every one reuses the
   established badge / card / table / field vocabulary; nothing here forks a
   parallel look.
   -------------------------------------------------------------------------- */

/* 27.1 Orders queue — tab strip + parent-grouped table.
   The tab strip keeps its existing structure: two `.row` groups (primary
   workflow lanes left, secondary filter lenses right) inside the `#orders-tabs`
   `.row`, and the tabs themselves stay `.btn .btn-sm` (active = `.btn-primary`,
   inactive = `.btn-secondary`) — the existing, tested pattern (CLAUDE.md §5),
   not a new control. `.qtab-count` is the only new piece: a small inset count
   pill the app fills on the ACTIVE tab from the live snapshot it already holds
   (no extra query — the other lanes' counts live on the Dashboard). Inherits
   the button's own text colour so it reads correctly inside both a filled
   primary tab and a secondary one. */
.qtab-count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; height: 20px; padding: 0 6px;
  border-radius: 999px; font-size: 11.5px; font-weight: 700;
  font-variant-numeric: tabular-nums; line-height: 1;
  background: color-mix(in srgb, currentColor 16%, transparent);
}

/* Parent-number group header row (queue "归组", S5.1 §9-1): every sub order of
   one store order renders under one head. A tinted band with a thin top rule
   so consecutive groups read as separate blocks; the parent number is a mono
   identifier (tabular), the sub-order count a quiet muted suffix. Replaces the
   old inline `background:var(--bg)` on the row's single <td>. */
.qc-group-head > td {
  background: var(--bg); border-top: 2px solid var(--border);
  padding: 9px 16px; font-size: 12px;
}
/* Each parent group is its own <tbody> (renderQueueTable) — suppress only the
   FIRST group's top rule, so the 2px band reads as a separator BETWEEN groups
   rather than a heavy double line right under the column header. */
.table tbody:first-of-type .qc-group-head > td { border-top: 0; }
.qc-group-parent { font-family: var(--font-mono); font-weight: 700; font-variant-numeric: tabular-nums; color: var(--primary); }
.qc-group-count { color: var(--muted); font-weight: 500; }
/* Queue rows are click-through to the detail; the pointer + hover tint (§6
   already tints tbody tr:hover) signal it, this only adds the cursor once at
   the system level instead of an inline style per row. */
.qc-row { cursor: pointer; }

/* 27.2 Order detail — action bar. Primary/secondary actions cluster on the
   left; a destructive action (Cancel order) is pushed to the far right with
   real distance so it never sits flush against the main CTA (mis-tap guard,
   same intent as the wholesale-pay foot's spaced Back/CTA). The danger button
   uses `.btn-ghost-danger` (§4) rather than a filled `.btn-danger`: set apart
   on its own, the ghost tone keeps the red cue without a second block of
   alarm colour competing with the primary blue CTA beside it. */
.detail-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 24px; }
.detail-actions .detail-actions-main { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.detail-actions .detail-actions-danger { margin-left: auto; }
@media (max-width: 1199px) {
  /* Once wrapped, a left-auto margin would strand the danger button on its own
     row hard against the right edge; keep it left-aligned with the cluster. */
  .detail-actions .detail-actions-danger { margin-left: 0; }
}

/* 27.3 Status-history timeline (order detail). A vertical rail with a node dot
   per entry, newest first: the dot is tinted by the entry's own status badge
   family so the colour cadence down the rail mirrors the badges beside it.
   `.timeline-item` carries the rail (a left border) and the dot (an absolutely
   positioned pseudo-element on the border); the last item's rail is trimmed so
   the line stops at the final dot rather than trailing past it. */
.timeline { list-style: none; margin: 0; padding: 0; }
.timeline-item {
  position: relative; padding: 0 0 18px 24px;
  border-left: 2px solid var(--border); margin-left: 5px;
}
.timeline-item:last-child { border-left-color: transparent; padding-bottom: 0; }
.timeline-item::before {
  content: ''; position: absolute; left: -7px; top: 2px;
  width: 12px; height: 12px; border-radius: 999px;
  background: var(--surface); box-shadow: 0 0 0 2px var(--border-strong);
}
.timeline-item.tl-warning::before  { box-shadow: 0 0 0 2px var(--warning); }
.timeline-item.tl-success::before  { box-shadow: 0 0 0 2px var(--success); }
.timeline-item.tl-info::before     { box-shadow: 0 0 0 2px var(--info); }
.timeline-item.tl-indigo::before   { box-shadow: 0 0 0 2px var(--indigo); }
.timeline-item.tl-danger::before   { box-shadow: 0 0 0 2px var(--danger); }
.timeline-item.tl-neutral::before  { box-shadow: 0 0 0 2px var(--neutral); }
.timeline-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.timeline-meta { font-size: 12px; color: var(--muted); margin-top: 4px; }
.timeline-meta .t-mono { font-variant-numeric: tabular-nums; }

/* 27.4 Callout — a bordered tinted notice block (order-detail cancelled
   banner). Uses the real danger tokens (§1) — replaces an inline block that
   referenced undefined `--danger-border`/`--danger-bg` vars via hardcoded
   fallbacks. `.callout-danger` is the only variant needed today; the base
   `.callout` is neutral so a future info/warning variant is a one-liner.
   That one-liner arrived with D-090: `.callout-warning` heads the BAS report's
   adjustments section (orders paid then cancelled, excluded from G1/1A). It is
   warning, not danger — nothing is broken, but the section must be impossible
   to miss, since a bookkeeper who overlooks it cannot explain why a paid order
   they remember is not in the figures. */
.callout {
  border: 1px solid var(--border); background: var(--bg);
  border-radius: var(--radius); padding: 12px 14px; margin-bottom: 24px;
}
.callout > p + p { margin-top: 4px; }
.callout-title { font-weight: 700; }
.callout-danger { border-color: var(--danger); background: var(--danger-soft); }
.callout-danger .callout-title { color: var(--danger-text); }
.callout-warning { border-color: var(--warning); background: var(--warning-soft); }
.callout-warning .callout-title { color: var(--warning-text); }

/* 27.5 Fabric cut allowance matrix (Settings). A 2×2 read-at-a-glance grid:
   a corner cell, two column headers (Manual / Motorised), then one labelled
   row per heading family (Wavefold / Other), each data cell an mm input. The
   axis labels make the four values legible as a matrix instead of four loose
   fields. Stays 2D at every width (see the tablet note below) — only the
   column min + gap tighten; it never collapses to one column, because a cell's
   meaning is the (heading × control) it sits under. */
.allow-matrix {
  display: grid;
  grid-template-columns: minmax(90px, 0.8fr) 1fr 1fr;
  gap: 12px; align-items: center;
  max-width: 520px; /* keep the four mm inputs compact on a full-width card */
}
.allow-matrix-col { font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; text-align: center; }
.allow-matrix-row { font-size: 13px; font-weight: 700; color: var(--primary); }
.allow-matrix .input { text-align: right; }
.allow-matrix-unit { position: relative; }
.allow-matrix-unit::after {
  content: 'mm'; position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
  font-size: 12px; color: var(--muted-soft); pointer-events: none;
}
.allow-matrix-unit .input { padding-right: 38px; }
/* Stays 2D on tablet (does NOT collapse to one column like the settings
   grid-2 blocks): a cut-allowance value is only legible AS a matrix cell —
   its meaning is the (heading × control) it sits under, so the axis headers
   must stay on screen. Only the column min + gap tighten. Each input also
   carries an aria-label with the full cell meaning, so the control is
   self-describing even when read out of the grid by a screen reader. */
@media (max-width: 1199px) {
  .allow-matrix { grid-template-columns: minmax(64px, 0.6fr) 1fr 1fr; gap: 8px; }
}

/* 27.5.1 Products page — collapsible settings fold (D-088, re-tiered D-089).
   Historical note: this section also documented a TOP category pill strip on the
   Products page; that tier has since moved twice (D-091 left rail → D-092 picker
   page, §27.5.3) and no pill strip remains, so only the fold below is live here.
   The SUB strip is deliberately NOT a row of `.btn` pills — it is a distinct,
   quieter control (`.subtab`, §27.5.2). `.settings-fold` is a native <details> styled as a
   compact card so each category leaf can LEAD with its (collapsed) settings and
   still put the high-frequency product List directly below; a `--flush` variant
   (§27.5.2) drops its border when it lives inside the shared panel card. Chevron/
   marker idiom mirrors the store price-tier <details> (§ price-tier-details) so
   the console has one collapse cue, not two. */
.settings-fold {
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--surface);
}
.settings-fold > summary {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; cursor: pointer;
  padding: 16px 20px; list-style: none;
  transition: color var(--motion-base) var(--ease-standard);
}
.settings-fold > summary::-webkit-details-marker { display: none; }
.settings-fold > summary::before {
  content: '▸'; font-size: 10px; line-height: 1; color: var(--muted);
  transition: transform var(--motion-base) var(--ease-standard);
}
.settings-fold[open] > summary::before { transform: rotate(90deg); }
.settings-fold > summary:hover .settings-fold-title { color: var(--primary); }
.settings-fold-title { font-weight: 700; }
/* The collapsed one-line summary (e.g. "Hanging allowance: 40/50/20/30 mm") —
   pushed to the trailing edge; drops to its own line before it would crush the
   title on a narrow tablet card (summary is flex-wrap). */
.settings-fold-value { margin-left: auto; color: var(--muted); font-size: 13px; font-variant-numeric: tabular-nums; }
.settings-fold-body { padding: 0 20px 20px; }

/* 27.5.2 Products page — SUB-category tier + single panel card (D-089 re-tier).
   Founder feedback on the D-088 build: the two tab strips rendered as identical
   pills (no hierarchy) and the settings + List read as two orphan cards. The
   re-tier gives the page a clear "page → top → sub → content" ownership chain:
     • TOP (§27.5.1) stays the primary pill tab at the page head.
     • SUB (`.subtabs`/`.subtab`) is a segmented track tucked INSIDE the panel
       card as its lead toolbar row — a raised chip on a tinted track, a
       different idiom from the left category rail's list rows (§27.5.3) so the
       two navigation tiers can never be mistaken for each other.
     • ONE `.card` holds the whole active sub-category: sub-tab row → settings
       zone (`.settings-fold--flush`, borderless) → in-card `.divider` → List.
   The sub strip is hidden entirely for leaf tops (Roller/Shutter) — no empty
   sub-tab row (app.js toggles display).
   Sizing (D-091 addendum, founder iPad test): each chip carries a >=44px touch
   target (Apple HIG minimum) at a 14px label, up from the original 12.5px pill —
   the workspace is tablet-first, and the sub-tabs were too small to press
   reliably. min-height + flex-centering (not top/bottom padding) sets the height
   so the label stays vertically centred as the track grows; the raised active
   chip fills the same 44px so it reads as one solid segmented control at both
   desktop and tablet widths. */
.subtabs {
  display: inline-flex; flex-wrap: wrap; gap: 4px;
  padding: 5px; border-radius: 999px; background: var(--bg);
}
.subtab {
  appearance: none; border: 0; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 44px; padding: 0 20px; border-radius: 999px;
  font-family: inherit; font-size: 14px; font-weight: 600; line-height: 1;
  color: var(--muted); background: transparent;
  transition: color var(--motion-base) var(--ease-standard),
              background var(--motion-base) var(--ease-standard);
}
.subtab:hover { color: var(--primary); }
/* Active = a raised surface chip lifted off the track (segmented-control idiom);
   distinct on purpose from the top tab's filled-primary pill. */
.subtab.is-active {
  color: var(--primary); background: var(--surface); cursor: default;
  box-shadow: 0 1px 2px rgba(15, 23, 42, .10), 0 0 0 1px rgba(15, 23, 42, .04);
}

/* Flush variant of the settings fold: when it lives INSIDE the shared panel
   card (not as its own bordered card) it drops border / surface / radius and
   aligns to the card's own 20px padding, so the settings zone and the List
   below read as two sections of ONE card (an in-card `.divider` between them),
   never two orphan cards. */
.settings-fold--flush { border: 0; background: transparent; border-radius: 0; }
.settings-fold--flush > summary { padding: 2px 0; }
.settings-fold--flush[open] > summary { padding-bottom: 14px; }
.settings-fold--flush > .settings-fold-body { padding: 0; }

/* 27.5.3 Products, two-stage (D-092) — Products follows the SAME "list → deep
   page" model as Orders → order workspace, so the console has exactly one way to
   enter a deep page:
     • STAGE 1, the category picker: a `.cat-tiles` grid of equal-sized tiles, one
       per top category, on an ordinary page with the menu in place.
     • STAGE 2, the category editor: entering toggles `body.workspace-fullscreen`
       (§20) — the same full-hide the store's order editor uses (D-091's
       compact-menu + left-rail draft is retired; its `menu-compact` rules stay in
       §20 per D-092 point 4, unused). `.ws-root` centers it, `.ws-head`/`.ws-bar`
       (§20) carry ← Back + the category name, and the 44px sub strip (§27.5.2)
       leads the panel card. This header has no second segment (the order
       workspace's stage-flow), so `--bare` drops `.ws-bar`'s divider border.

   `.cat-tiles` is deliberately NOT a new tile component: it reuses `.tile` (§22,
   the console's only tile idiom — border/radius/hover/focus ring/centred column)
   under a picker-scale grid + `.cat-tile` size step, so the two tile surfaces
   can't drift apart. `auto-fill` (not `auto-fit`) is the point of the grid: tiles
   keep ONE size and a dozen-plus categories wrap into further rows instead of
   three tiles stretching across the page — D-092's design constraint. 200px min
   holds 3 columns even at 768px portrait.
   Scale note: when the category count passes ~8, add a search field above the
   grid and swap the static PRODUCTS_IA source for `categories`-data (D-045 path);
   neither is built speculatively today. */
.ws-head--bare .ws-bar { border-bottom: 0; }

.cat-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 12px;
}
/* Picker-scale step on `.tile`: a big tablet touch target with a page-level
   (not rail-level) label size. Everything else is inherited. */
.cat-tile { min-height: 108px; padding: 16px; font-size: 15px; }

/* 27.6 Ad-hoc modal scrim — RETIRED, kept for reference only (no app uses it).
   The three S6 confirmations it was written for (send to factory / dispatch /
   cancel) now build a per-call native <dialog> instead (openModalDialog() in
   apps/supplier/js/app.js), so ESC and the never-clickable backdrop come from
   the platform rather than a hand-wired keydown listener. Every modal surface
   in this system is now the same element with the same rules (§24). Do NOT
   reach for this class in new work — use a <dialog> + `.modal-card`. */
.modal-scrim {
  position: fixed; inset: 0; z-index: 1000;
  display: flex; align-items: center; justify-content: center;
  padding: 24px; overflow-y: auto;
  background: rgba(15, 23, 42, .6);
}
.modal-scrim > .card { margin: 0; max-height: calc(100vh - 48px); overflow-y: auto; }

/* 27.7 Field hint — a small muted helper line under an input, ALWAYS present
   (not toggled) so a required/validation note reserves its own height and the
   dialog never reflows as state changes (§24 lock discipline, extended to the
   in-panel copy). Danger-toned variant for an actual inline error. */
.field-hint { font-size: 11.5px; color: var(--muted); margin: 6px 0 0; min-height: 15px; }
.field-hint-danger { color: var(--danger-text); }

/* 27.7.1 Reserved height — the house rule for EVERY message row that toggles
   mid-form (validation / error / success), ruled in the design ledger
   2026-07-28 after the S6 close-out pass reserved 24 of them one line at a
   time. Two parts:
     a) The row always stays in the layout — `visibility: hidden` while empty,
        never `display: none`, or nothing below it can hold still.
     b) It reserves the height of the copy the UI is DESIGNED to show, not of
        the worst case:
          20px = one line of a 16px `.muted` message row (the default; every
                 authored validation string in this app is one line);
          32px = two lines, ONLY where the authored copy itself runs two
                 (#cust-form-notice's dedupe verdict, #st-bank-hint's BSB
                 guidance);
          15px = `.field-hint`'s own one line of 11.5px type (above).
   A raw `err.message` (a Firestore/Stripe string) is NOT authored copy and has
   no bounded length, so it is explicitly OUT of this reservation: two lines
   would not cover it either (a long one wraps to three), while costing every
   dialog in the app a permanent empty band. Such a message may grow its
   dialog — it only ever appears after an action has already failed, never
   under a finger mid-tap. Reserve for your own copy; do not chase the
   backend's. */

/* 27.8 Order detail — three-row layout (D-094-2, founder test day 2026-07-27).
   The page used to be one column: lines, then store, then payment, then status
   history. A sub order can carry dozens of lines, so on a real order the two
   blocks the wholesaler actually needs were pushed clean off the bottom of the
   screen. The founder's own layout, fixed here: row 1 = order information
   (number, store, the wholesale status track and the action bar), row 2 =
   Payment | Status history side by side, row 3 = the line list, LAST.

   Each row is an ordinary `.card` (§5) — nothing new; `.od-two-col` is the
   only piece the shared set did not already cover, and it exists rather than
   reusing `.grid-2` (§10) because `.grid-2` collapses to one column at
   <=1199px. That threshold is right for a pair of form fields, but a tablet in
   landscape has ample room for these two blocks, and collapsing them there
   would put the status history back below a long payment block — the very
   stacking this layout was made to end. It drops to one column at <=768px
   instead — the founder's own cut-off: tablet portrait and narrower, where two
   columns genuinely stop fitting beside the icon rail. Payment is FIRST in
   source order, so it leads on the way down.

   `align-items: start` keeps each card at its own natural height: a long
   status history must not stretch a two-line payment block into a tall, mostly
   empty card beside it. */
.od-two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start; }
@media (max-width: 768px) {
  .od-two-col { grid-template-columns: 1fr; }
}

/* The wholesale status track (D-094-3) is the store's own chevron chain
   component (`.stage-flow-chain`, §21) reused verbatim inside row 1's card —
   this adds nothing but the gap under it, since §21's chain sits in an
   edge-to-edge `.stage-flow` band over there and here it is one block inside a
   normal card. Source order (§21 sets `margin: 0`) is what lets this win with
   no `!important`. */
.od-track { margin: 0 0 20px; }

/* --------------------------------------------------------------------------
   28. Suggestion menu (D-096) — the self-drawn dropdown for Google Places
   address autocomplete (apps/store/js/customers/address-autocomplete.js).

   Declared here as Trade's OWN component, in Trade's own tokens: the
   alternative Retail eventually removed was Google's `<gmp-place-autocomplete>`
   element, whose closed Shadow DOM cannot be themed at all (docs/reference/
   retail-address-autocomplete-intel.md §2). Drawing the list ourselves is what
   buys us design-system typography, 44px touch rows and the app's own hover
   tone.

   The module builds `.suggest-anchor` around the input at mount time, so no
   form markup needs to know about any of this. Visibility is CLASS-driven
   (`.is-open`), never the `hidden` attribute or an inline style — one state
   channel, and section 2's `[hidden]` guard stays a safety net rather than
   load-bearing machinery.
   -------------------------------------------------------------------------- */
.suggest-anchor { position: relative; display: block; }
.suggest-menu {
  display: none;
  position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 20;
  max-height: 264px; overflow-y: auto;
  background: var(--surface-raised); border: 1px solid var(--border-strong);
  border-radius: var(--radius); box-shadow: var(--shadow-pop);
}
.suggest-menu.is-open { display: block; }
/* 44px minimum row (house touch baseline): this list is tapped on an iPad in
   a customer's living room at least as often as it is clicked with a mouse. */
.suggest-item {
  display: flex; align-items: center; min-height: var(--tap-min);
  padding: 10px 14px; font-size: 15px; color: var(--primary); cursor: pointer;
}
.suggest-item + .suggest-item { border-top: 1px solid var(--border); }
/* Pointer hover and keyboard highlight are deliberately the SAME tone — the
   list has one notion of "the row you are about to accept", whichever input
   device asked for it. */
.suggest-item:hover, .suggest-item.is-active { background: var(--bg); }
