@import url('https://fonts.googleapis.com/css2?family=Oswald:wght@500;600;700&display=swap');

/* ======================================================================
   Revision 14 — the approved palette, as tokens.

   docs/UI-DESIGN-LOCK.md is the authority for every value below. The
   direction it locks is black + red + white: a white/very-light workspace,
   a near-black sidebar, and Legends red as the ONE accent, with green
   reserved for verified-healthy states and amber for warnings.

   What was here before was not that. The workspace was a warm grey
   (#f6f4f2) and the sidebar was WHITE with a red top rule — so the shell
   had no anchor, and red was doing double duty as both the brand and the
   only way to see which nav item you were on. §9 of the lock rejects the
   result by name.

   The legacy names (--brand, --ink, --line, --bg, --card, --ok, --err) are
   kept as ALIASES rather than replaced. Eighteen hundred lines of this
   stylesheet and every screen script already reach for them; re-pointing
   the alias re-colours all of it at once and keeps one definition per
   colour. New rules should use the lock's names.
   ====================================================================== */
:root {
  /* ---- brand ---- */
  --brand-red: #d93636;
  --brand-red-dark: #b92727;
  --brand-red-soft: #fdecec;

  /* ---- navigation: near-black, not "dark mode" ----
     The sidebar is the only dark surface in the product. The lock rejects
     a fully dark dashboard; a dark rail against a white workspace is what
     gives the shell its anchor and keeps red for the active item alone. */
  --sidebar-bg: #111214;
  --sidebar-hover: #1c1d20;
  --sidebar-active-bg: rgba(217, 54, 54, .14);
  --sidebar-line: #26272b;
  --sidebar-text: #c8c9cd;
  --sidebar-text-strong: #ffffff;
  --sidebar-muted: #83858c;

  /* ---- workspace ---- */
  --page-bg: #f7f7f5;
  --surface: #ffffff;
  --surface-muted: #fafaf9;

  /* ---- text ---- */
  --text-primary: #161616;
  --text-secondary: #686868;
  --text-muted: #8a8a8a;

  /* ---- lines ---- */
  --border: #e5e5e2;
  --border-strong: #d5d5d1;

  /* ---- status. Not decoration: each one is a claim about state. ----
     Green means verified healthy, amber means warning, red means
     attention/overdue/critical. Nothing else may use them. */
  --success: #2f9e5b;
  --success-soft: #eaf6ef;
  --warning: #c98218;
  --warning-soft: #fdf3e3;
  --success-ink: #14663f;
  --success-line: #c3e5d3;
  --warning-ink: #8a4f00;
  --warning-line: #f6ddb4;
  --danger: #d93636;
  --danger-soft: #fdecec;
  --danger-ink: #a4231b;
  --danger-line: #f6cdc9;
  --brand-red-line: #f2d4d4;

  /* ---- neutral ----
     The design lock's palette has no blue in it. "Info", "not configured"
     and "foundation" were all being drawn in navy, which both broke §9
     ("no navy") and overstated what they say: an absence of configuration
     is not a status claim in the way green, amber and red are. They are
     neutral now, which is both on-palette and more honest. */
  --neutral-soft: var(--surface-muted);
  --neutral-line: var(--border-strong);
  --neutral-ink: var(--text-secondary);

  /* ---- legacy aliases — one definition per colour, two names ---- */
  --brand: var(--brand-red);
  --brand-dark: var(--brand-red-dark);
  --brand-light: var(--brand-red-soft);
  --ink: var(--text-primary);
  --muted: var(--text-secondary);
  --line: var(--border);
  --bg: var(--page-bg);
  --card: var(--surface);
  --ok: var(--success);
  --err: var(--danger);

  /* Moderate corners, 8–12px (lock §4). --radius was 14px, which reads as
     a consumer app rather than an operations tool. */
  --radius: 12px;
  --heading-font: 'Oswald', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: 16px;

  /* ---- design tokens ----
     Named after what's already in use throughout this file (not new
     values — every existing rule that switches to one of these keeps its
     exact current pixel size, this just gives the number a name so it's
     shared instead of re-typed ad hoc per rule). New components should
     reach for these instead of a bare pixel value. */
  --space-1: 4px;  --space-2: 8px;  --space-3: 12px;
  --space-4: 16px; --space-5: 20px; --space-6: 24px;

  --radius-sm: 8px;    /* small chips/inputs inside a dense row */
  --radius-md: 10px;   /* inputs, buttons, list rows */
  --radius-lg: 12px;   /* cards, modals — same value as --radius (kept for back-compat) */
  --radius-pill: 999px; /* segmented control, status pill badges */

  --font-xs: 11px;   /* items-table column headers */
  --font-sm: 12px;   /* table.simple column headers, small meta text */
  --font-base: 13px; /* field labels, .muted body copy */
  --font-md: 14px;   /* default body/button text */
  --font-lg: 16px;   /* form inputs (16px avoids iOS auto-zoom on focus) */
  --font-xl: 20px;   /* modal/section headings */
  --font-2xl: 22px;  /* .page-title */

  /* Revision 17 — this used to read "buttons and text inputs both land
     here at their current padding+font-size". Inputs did: they say
     `height: var(--control-height)` out loud. Buttons did not, and could
     not: no button rule in this file declared a height at ANY width, so a
     button's box was its padding plus a line box computed from the
     browser's default `line-height: normal` over a font stack that
     resolves to a different font on every platform this portal runs on.
     The number was a hope about three operating systems' font metrics.
     It is now declared — see .btn — and the touch block near the foot of
     this file holds every other shared control to it. */
  --control-height: 44px;   /* the floor for anything a finger presses */
  --table-row-height: 38px; /* table.simple row, at its current padding+font-size */
  --radius-btn: 10px;       /* buttons round a touch more than a plain input/card */

  /* Subtle shadows only (lock §4): a thin neutral border does the work,
     and the shadow only lifts things that genuinely float. */
  --shadow-sm: 0 1px 2px rgba(22, 22, 22, .04);
  --shadow-pop: 0 8px 24px rgba(22, 22, 22, .10);

  --sidebar-w: 236px;
}
* { box-sizing: border-box; }

/* HIDDEN MEANS HIDDEN.
   ---------------------------------------------------------------------
   The `hidden` attribute is only styled by the user-agent stylesheet, as
   `[hidden] { display: none }`. Any AUTHOR rule that sets `display` on the
   same element wins on origin alone, no matter how weak its selector — so
   an element the script has carefully marked hidden goes on rendering, as
   an empty box with its own padding, background and border still applied.

   Two shell controls had exactly that shape, on every screen, for every
   user:

     * `#lowStockAlertTop` — `.low-stock-alert` sets `display: inline-block`.
       nav.js emits the anchor `hidden` and only fills it in when a product
       is at or below its reorder point. Everywhere else — which is most
       accounts, most days, and ALL accounts that cannot see Inventory at
       all, because wireLowStockAlert() returns before it is ever filled —
       what rendered was an empty amber pill in the topbar linking to Live
       Inventory. That is the "differently-coloured/live link with no
       meaningful visible state/content" the owner asked about by name in
       comments 5458064775 and 5458112899; it was never a navigation
       decision, it was this.

     * `#taskBadge` — `.bell-btn .badge` sets `display: flex`. The badge is
       emitted `hidden` with the literal text `0`, and refreshBadge() sets
       `.hidden = true` again whenever the unread count reaches zero. So
       the notification bell wore a red count badge permanently, reading
       `0` before the first poll answered and reading the last non-zero
       count forever after the notifications were dealt with.

   Fixed once, here, rather than twice at the two call sites: the next
   `display` rule added to a class that some script also toggles `hidden`
   would reintroduce it silently, and there is no author rule in this
   sheet that wants to override `hidden` (there was no `[hidden]` selector
   here at all before this line). Verified against every element in
   public/ that carries the attribute: none is shown again through
   `style.display`, so nothing legitimate loses. */
[hidden] { display: none !important; }
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  background: var(--bg);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
}
/* Oswald is the brand's display face and stays on headings and the
   wordmark. It came off .btn in Revision 14: a condensed uppercase display
   face on every button is what made the product read as a sports-club
   site rather than the operations tool the lock asks for. */
/* Revision 14 — the display face is the brand's, not the interface's.
   It is on the wordmark and on printed document kinds, and nowhere else;
   everything on screen is the body grotesque in sentence case. */
h1, h2 { letter-spacing: -.01em; }
.brand-word { font-family: var(--heading-font); }

/* Every icon in the shell is an inline SVG on a 24px grid at
   currentColor, so it takes the colour of whatever it sits in — the dark
   rail, the light topbar, an active red row — with no per-context rule. */
.nav-icon { display: block; flex: 0 0 auto; }

/* ---------------------------------------------------------------------
   Revision 14 — the near-black rail.

   docs/UI-DESIGN-LOCK.md §2: "Fixed left sidebar in near-black / charcoal.
   Legends ProShop logo at the top… Active item: restrained dark-red /
   red-tinted background, red accent/indicator, white label."

   What this replaces was a WHITE sidebar separated from a warm-grey page
   by one hairline, with a 3px red rule along its top edge. Three things
   were wrong with it and all three are in the lock:

     * the shell had no anchor. A white rail beside a near-white workspace
       is not a navigation region, it is a column of links; the eye had
       nothing to orient against and the page had no edge.
     * red was doing two jobs. It was the brand rule at the top AND the
       fill behind the active link, so the one red thing that should mean
       "you are here" competed with a decorative band that meant nothing.
     * a solid red fill behind the active row is not "restrained". Against
       the dark rail the active state is now a red-tinted ground, a red
       indicator on the leading edge, and a white label — three quiet
       signals rather than one loud one.

   Nothing here makes the PRODUCT dark. The rail is the only dark surface;
   §9 rejects a fully dark dashboard, and the workspace stays white.
   --------------------------------------------------------------------- */
.app-shell { display: flex; min-height: 100vh; }
.sidebar {
  width: var(--sidebar-w); flex-shrink: 0;
  background: var(--sidebar-bg); color: var(--sidebar-text);
  border-right: 1px solid var(--sidebar-bg);
  display: flex; flex-direction: column;
  position: sticky; top: 0; height: 100vh; z-index: 30;
  /* Revision 17 — the RAIL no longer scrolls; .side-nav inside it does.

     This used to be `overflow-y: auto` on the rail itself, and that one
     declaration had three consequences, none of them intended.

     The rail is `position: sticky`, so it is also the containing block for
     `.side-collapse-toggle`, which nav.js emits as its direct child. A
     scroll container CLIPS its absolutely positioned descendants when it is
     their containing block — so the toggle, at `right: -12px` on a 24px
     disc, had exactly half of itself cut off. The affordance it was drawn
     for, a handle half-hanging off the rail's edge, has never once
     rendered; what shipped was a half-disc flush against the border.

     Worse, an abspos child of a scroll container scrolls WITH the content,
     and `bottom: 64px` is measured from the bottom of the scrollable
     content — not the bottom of the visible rail. With eleven areas
     expanded the content is taller than 100vh, so the only control that
     un-collapses the sidebar sat below the fold and you had to scroll the
     nav to its end to find it.

     And because the whole column scrolled, `.side-scope` and `.side-user`
     scrolled away with it: on an account with a long nav, changing branch
     scope or pressing Log out meant scrolling past sixty links to reach a
     control that looks like it is pinned to the bottom of the screen.

     Moving the scroll inside .side-nav fixes all three at once. The rail
     becomes a fixed 100vh flex column: brand pinned at the top, scope and
     user pinned at the bottom, the long nested lists scrolling between
     them, and the toggle positioned against the rail's own box where it is
     both fully drawn and always on screen. Asserted in
     test/rev17-rail-scroll.test.js. */
  overflow: visible;
}

/* ---- brand ----
   The approved mark is black-on-white artwork, so it is carried on a white
   disc rather than recoloured to suit the rail. Recolouring somebody's
   logo is a change to their identity; the lock names this asset AS the
   identity. */
.side-brand {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-5) var(--space-4) var(--space-4);
  text-decoration: none; color: var(--sidebar-text-strong);
}
.side-brand-mark {
  width: 36px; height: 36px; border-radius: var(--radius-md); background: #fff;
  display: flex; align-items: center; justify-content: center; flex: 0 0 auto;
}
/* Revision 17 — `object-fit` on every on-screen instance of the mark.

   The approved asset is a 1599x1999 canvas — a 4:5 PORTRAIT — but the
   artwork inside it is 1169x1198, very nearly square (0.9758), floating in
   a transparent margin that is 395px at the top and 406px at the bottom
   against 211/219 at the sides. Roughly 40% of the canvas height is empty.

   Every rule below asked for a SQUARE box and left `object-fit` at its
   initial value, `fill`. `fill` scales the two axes independently, so the
   canvas was compressed 1599->N horizontally and 1999->N vertically: the
   mark came out 25.0% wider than tall, on every screen it appears on. It
   also came out SMALL, because the box was mostly that transparent margin
   — in the rail, 23.4 x 19.2 of a 32px box.

   `cover` is the fix rather than `contain`. Both stop the distortion, but
   `contain` fits the whole canvas — margin included — so the mark would
   shrink to 18.7 x 19.2 and the owner's complaint that the logo is hard to
   see would get worse. `cover` scales uniformly to fill the box and crops
   the overflow, and here the overflow is transparent margin: the mark
   becomes 23.4 x 24.0, correctly proportioned and taller than it was.

   Nothing is clipped, and that is measured rather than assumed. Under
   `cover` the opaque pixel furthest from the centre of the circular masks
   below sits at 77.2% of the radius — the same figure at 28px, 34px, 40px
   and 84px, because the transform is uniform. That 22.8% of headroom is
   asserted against the real asset in test/rev17-brand-mark.test.js, so
   replacing the artwork with something that would touch the mask fails a
   test instead of shipping a cropped logo.

   The printed A4 document already had this right — `#a4Area .doc-logo`
   says `width: auto` and `object-fit: contain`. The mark has been correct
   on paper and stretched on screen. */
.side-brand img.logo { width: 32px; height: 32px; object-fit: cover; }
.side-brand .brand-word {
  font-family: var(--heading-font); font-weight: 700; font-size: var(--font-md);
  letter-spacing: .06em; text-transform: uppercase; line-height: 1.15;
  color: var(--sidebar-text-strong); display: flex; flex-direction: column;
}
.side-brand .brand-word-sub { color: var(--brand-red); font-size: var(--font-sm); letter-spacing: .1em; }

.low-stock-alert {
  flex-basis: 100%; display: inline-block; width: fit-content;
  background: var(--warning-soft); color: var(--warning); border-radius: var(--radius-pill);
  padding: 4px 10px; font-size: var(--font-sm); font-weight: 600; text-decoration: none;
}
.low-stock-alert:hover { background: var(--warning-line); }
.topbar .low-stock-alert { flex-basis: auto; margin-left: auto; }

/* ---- the eleven areas ---- */
/* `min-height: 0` is load-bearing, not defensive: a flex item's automatic
   minimum size is its content, so without it this box refuses to shrink
   below the full height of sixty-two links and never scrolls — it just
   pushes .side-scope and .side-user off the bottom of the rail, which is
   the pre-Revision-17 behaviour wearing a different declaration. */
.side-nav { flex: 1; min-height: 0; overflow-y: auto; padding: 0 var(--space-2) var(--space-4); }
.side-nav-list { list-style: none; margin: 0; padding: 0; }
/* The administrative tools, "separated at the bottom" (lock §2). A rule
   and a gap — not a heading. Three headings over eleven rows would be a
   second level of hierarchy invented to describe a list short enough to
   read in one glance. */
.side-admin {
  margin-top: var(--space-4); padding-top: var(--space-4);
  border-top: 1px solid var(--sidebar-line);
}

/* An area row is a link and a disclosure button side by side, sharing one
   hover/active ground. Two controls, one row: pressing the name goes to
   the area, pressing the chevron shows what is inside it. */
.side-area-row {
  display: flex; align-items: stretch; border-radius: var(--radius-md);
  position: relative; margin-bottom: 1px;
}
.side-area-row:hover { background: var(--sidebar-hover); }
.side-area-link {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: center; gap: var(--space-3);
  padding: 9px var(--space-3); border-radius: var(--radius-md);
  color: var(--sidebar-text); text-decoration: none;
  font-size: var(--font-md); font-weight: 500; line-height: 1.3;
}
.side-area-icon { display: flex; color: var(--sidebar-muted); flex: 0 0 auto; }
.side-area-link .side-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* You are here. The approved reference fills the row with Legends red and
   sets the label in white, and that is what this does.
   The first pass used a red-tinted ground with a 3px red edge, reading
   "restrained" in the lock as "quiet". On a near-black rail it was too
   quiet: the tint is a 14%-opacity red over #111, which at a glance is
   just a slightly lighter dark row. Red is the accent this product has,
   and the selected item is the one place §2 spends it. */
.side-area-row.active,
.side-area-row.active:hover { background: var(--brand-red); }
.side-area-row.active .side-area-link { color: #fff; font-weight: 600; }
.side-area-row.active .side-area-icon { color: #fff; }
.side-area-row.active .side-section-toggle { color: rgba(255, 255, 255, .82); }
.side-area-row.active .side-section-toggle:hover { color: #fff; }

.side-section-toggle {
  flex: 0 0 auto; width: 34px; background: transparent; border: 0; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  color: var(--sidebar-muted); border-radius: var(--radius-md); padding: 0;
}
.side-section-toggle:hover { color: var(--sidebar-text-strong); }
.side-chevron { display: flex; transition: transform .15s ease; }
.side-section-toggle[aria-expanded="true"] .side-chevron { transform: rotate(90deg); }

/* Focus has to be visible on a dark ground, where a red ring is nearly
   invisible — so it is a white ring with a dark gap inside it. */
.sidebar :focus-visible {
  outline: 2px solid #fff; outline-offset: -2px; border-radius: var(--radius-md);
}

/* The detail under an area. Indented against a hairline so the list reads
   as belonging to the row above it rather than as more top-level links. */
.side-group-items {
  padding: var(--space-1) 0 var(--space-2);
  margin: 0 0 var(--space-1) 26px; border-left: 1px solid var(--sidebar-line);
}
.side-link {
  display: flex; align-items: center; gap: var(--space-2);
  padding: 6px var(--space-3); border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  color: var(--sidebar-muted); text-decoration: none;
  font-size: var(--font-base); font-weight: 500; line-height: 1.4;
  border-left: 2px solid transparent; margin-left: -1px;
}
.side-link:hover { background: var(--sidebar-hover); color: var(--sidebar-text-strong); }
.side-link.active {
  color: var(--sidebar-text-strong); font-weight: 600;
  background: var(--sidebar-active-bg); border-left-color: var(--brand-red);
}
.sidebar .side-foundation {
  margin: var(--space-1) var(--space-3) var(--space-2); padding: 0;
  font-size: var(--font-xs); line-height: 1.4; color: var(--sidebar-muted); font-style: normal;
}
.side-user {
  padding: var(--space-3) var(--space-4); border-top: 1px solid var(--sidebar-line);
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
  font-size: var(--font-base); color: var(--sidebar-text);
}
.side-user-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.side-user .iconbtn.side-logout {
  padding: 6px var(--space-2); font-size: var(--font-sm); gap: 6px;
  background: transparent; border-color: var(--sidebar-line); color: var(--sidebar-muted);
  display: inline-flex; align-items: center;
}
.side-user .iconbtn.side-logout:hover { border-color: var(--brand-red); color: var(--brand-red); background: transparent; }

/* Collapse toggle — desktop only (mobile already has its own open/close
   drawer via #menuToggle, this is a separate, independent control). Pinned
   bottom-right of the sidebar rather than inline with the logo, so it
   doesn't reflow when the brand word disappears on collapse. */
.side-collapse-toggle {
  position: absolute; bottom: 64px; right: -12px; width: 24px; height: 24px; border-radius: 50%;
  background: var(--surface); border: 1px solid var(--border); color: var(--text-secondary); cursor: pointer;
  display: flex; align-items: center; justify-content: center; z-index: 5; box-shadow: var(--shadow-sm);
}
.side-collapse-toggle:hover { border-color: var(--brand-red); color: var(--brand-red); }
@media (max-width: 860px) { .side-collapse-toggle { display: none; } }

/* Collapsed = icon-only rail. Brand text and group headers hidden, items
   always shown flat since a header with no room for its own label isn't
   worth keeping.

   Revision 13 — the LINK LABEL is the exception, and it was the bug. This
   rule used to list .side-label alongside the brand word, so a collapsed
   sidebar was eighty links whose only visible content was an emoji that
   nav.js marks aria-hidden. display:none removes an element from the
   accessibility tree as well as from the page, so every one of those links
   announced as its bare href, or as nothing at all: the rail was unusable
   by a screen reader and unlabelled for anyone whose emoji font differs
   from the designer's.

   The comment here claimed a title="" tooltip covered it. nav.js never
   emitted one — the tooltip was documented, not implemented, for three
   revisions. Both halves are fixed: the label is now visually hidden the
   way .sr-only hides a label (clipped, still announced) rather than
   removed, and nav.js emits the title the hover tooltip needs.

   Revision 13 — AND the rail is now gated to the widths where you can get
   out of it. `.collapsed` is persisted in localStorage, and the control that
   clears it is `display: none` under 860px (see .side-collapse-toggle
   above). So the rail survived into the drawer, where it has no toggle:
   collapse the sidebar on an iPad in landscape, rotate to portrait, and
   the ☰ opened a 72px strip of aria-hidden emoji with no way back —
   same browser, same localStorage, no second device needed. It also hid
   .side-scope, which under 480px is the only place branch scope can be
   changed at all, so the rotation could take the scope switcher away too.

   The rail is a desktop affordance and is now scoped like one: 861px is
   the exact complement of the toggle's own max-width, asserted against it
   in rev13-responsive-shell.test.js so the two cannot drift apart. */
@media (min-width: 861px) {
  .sidebar.collapsed { width: 68px; }
  .sidebar.collapsed .brand-word,
  .sidebar.collapsed .side-user-name,
  .sidebar.collapsed .side-logout-word,
  .sidebar.collapsed .side-group-heading,
  .sidebar.collapsed .side-section-toggle,
  .sidebar.collapsed .low-stock-alert { display: none; }
  /* Revision 14 — the detail lists collapse away with the labels rather
     than flattening into the rail. Eleven area icons is a rail; sixty-two
     unlabelled ones is the wall the design lock rejects, and the sub-items
     have no icon of their own to be recognised by. */
  .sidebar.collapsed .side-group-items { display: none; }
  .sidebar.collapsed .side-label {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  }
  .sidebar.collapsed .side-area-link { justify-content: center; padding: 10px; }
  .sidebar.collapsed .side-brand { justify-content: center; padding: 18px 8px 14px; }
  .sidebar.collapsed .side-user { justify-content: center; }
}

/* Persistent top bar — unlike the sidebar, visible at every screen width.
   The hamburger + mobile brand label are mobile-only (sidebar already shows
   the brand on desktop); topbar-right (Tasks link + notification bell)
   stays visible everywhere, which is the whole point of putting Tasks up
   here instead of as another sidebar link. */
/* Revision 14 — white, one hairline, nothing else. The lock asks for a
   compact aligned header rather than an oversized block, so the topbar
   carries only the four things it owes: what you are looking at (search),
   where you are looking (scope), what needs you (tasks/bell) and who you
   are. */
.topbar {
  display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-5);
  background: var(--surface); border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: 20; min-height: 56px;
}
.topbar .mobile-brand-word { display: none; font-size: var(--font-base); }
.topbar img.logo.mobile-brand-word { width: 28px; height: 28px; object-fit: cover; border-radius: 50%; box-shadow: 0 0 0 2px var(--brand-light); }
.topbar #menuToggle { display: none; }
.topbar-right { display: flex; align-items: center; gap: 8px; margin-left: auto; }
.topbar-whoami { color: var(--muted); font-size: var(--font-base); white-space: nowrap; }
.topbar-select {
  font-size: var(--font-base); padding: 4px 8px; border-radius: var(--radius-sm); border: 1px solid var(--line);
  background: var(--card); color: var(--ink); max-width: 120px;
}
.topbar-ctx { display: flex; align-items: center; gap: 4px; }
.topbar-ctx-icon { display: flex; color: var(--text-muted); }

@media (max-width: 480px) {
  /* Name + branch crowds Tasks/bell on very narrow phones — sidebar
     already shows the same info (.side-user) once the menu's opened. */
  .topbar-whoami, .topbar-select, .topbar-ctx { display: none; }
}

.main { flex: 1; min-width: 0; }

@media (max-width: 860px) {
  .sidebar {
    position: fixed; left: 0; top: 0; width: 280px; max-width: 86vw;
    transform: translateX(-100%); transition: transform .18s ease;
    box-shadow: 0 0 40px rgba(0,0,0,.28);
  }
  .sidebar.open { transform: translateX(0); }
  .topbar .mobile-brand-word, .topbar #menuToggle { display: block; }
  .topbar .low-stock-alert { margin-left: 0; }
}

.bell-wrap { position: relative; }
.bell-btn { position: relative; padding: var(--space-2); }
.bell-btn .badge {
  position: absolute; top: -6px; right: -6px; background: var(--err); color: #fff;
  border-radius: 999px; font-size: 10px; font-weight: 700; min-width: 16px; height: 16px;
  display: flex; align-items: center; justify-content: center; padding: 0 4px; line-height: 1;
  box-shadow: 0 0 0 2px var(--card);
}
.task-dropdown {
  /* Anchored to .bell-wrap, which sits at the topbar's right padding edge,
     and grows LEFTWARDS from there — so a width it cannot give up is a
     width that runs off the left of the viewport, where nothing can scroll
     it back into view. Capped against the viewport less the topbar's own
     padding on both sides, in the same token the topbar pads with, so the
     two cannot drift apart. Above ~344px of room the 320px wins and the
     dropdown is unchanged. */
  position: absolute; right: 0; top: calc(100% + 8px);
  width: 320px; max-width: calc(100vw - 2 * var(--space-3));
  max-height: 70vh; overflow-y: auto;
  background: var(--card); border: 1px solid var(--line); border-radius: 12px;
  box-shadow: 0 12px 32px rgba(0,0,0,.14); z-index: 40;
}
.task-dropdown-head {
  display: flex; align-items: center; justify-content: space-between; padding: 10px 12px;
  font-weight: 600; font-size: var(--font-base); border-bottom: 1px solid var(--line);
}
.task-dropdown-head a { font-weight: 500; font-size: var(--font-sm); }
.task-dropdown-item { padding: 10px 12px; border-bottom: 1px solid var(--line); }
.task-dropdown-item:last-child { border-bottom: none; }
.task-dd-main { display: flex; align-items: center; gap: 8px; }
.task-dd-title { font-size: var(--font-base); font-weight: 500; }
.task-dd-meta { font-size: var(--font-xs); color: var(--muted); margin: 3px 0 6px 16px; }
.task-dd-done { margin-left: 16px; font-size: var(--font-sm); }
.task-priority-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; background: var(--muted); }
.task-priority-dot.priority-low { background: var(--border-strong); }
.task-priority-dot.priority-normal { background: var(--neutral-ink); }
.task-priority-dot.priority-high { background: var(--warning-ink); }
.task-priority-dot.priority-urgent { background: var(--err); }

.app-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 12px 18px; background: var(--card); border-bottom: 1px solid var(--line);
  border-top: 3px solid var(--brand);
  position: sticky; top: 0; z-index: 10;
}
.app-header .brand { display: flex; align-items: center; gap: 10px; }
.app-header .brand img.logo { width: 34px; height: 34px; object-fit: cover; border-radius: 50%; display: block; box-shadow: 0 0 0 2px var(--brand-light); }
.app-header .brand .brand-word { font-weight: 700; font-size: var(--font-lg); letter-spacing: .02em; text-transform: uppercase; }
.app-header nav { display: flex; gap: 8px; align-items: center; }
/* An icon button now holds an SVG and (usually) a word. inline-flex with
   a gap rather than a text node and a space, so the two line up on their
   centres at every font size instead of on the emoji's baseline. */
.iconbtn {
  display: inline-flex; align-items: center; gap: 6px;
  border: 1px solid var(--border); background: var(--surface); color: var(--text-primary);
  padding: var(--space-2) var(--space-3); border-radius: var(--radius-md);
  font-size: var(--font-md); font-family: inherit; cursor: pointer; text-decoration: none;
  transition: border-color .12s ease, color .12s ease, background .12s ease;
}
.iconbtn:hover { border-color: var(--brand-red); color: var(--brand-red); background: var(--brand-red-soft); }

/* 720px was fine for a lone entry form; Reports/Admin have since grown into
   wide tables and 15+ tabs that were clipping at that width. Entry forms
   themselves stay narrow (.container.narrow) since a 2-4 field form
   stretched across a wide screen just reads as empty space either side. */
.container { max-width: 1180px; margin: 0 auto; padding: 20px 16px 80px; }
.container.narrow { max-width: 720px; }

.page-title {
  font-weight: 700; font-size: var(--font-2xl); letter-spacing: -.01em;
  margin: 22px 0 2px;
}

/* An eyebrow over a block of fields. Small caps is right for a label —
   it is a signpost, not a title — but it is set in the body face. */
.section-heading {
  font-size: var(--font-sm); font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-secondary); margin: 28px 0 4px;
}
.section-heading:first-of-type { margin-top: 18px; }

.picker-grid {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-top: 18px;
}
@media (min-width: 560px) { .picker-grid { grid-template-columns: repeat(3, 1fr); } }
.picker-tile {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 22px 12px 18px; text-align: center; cursor: pointer; text-decoration: none; color: var(--ink);
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  transition: transform .1s ease, box-shadow .1s ease, border-color .1s ease;
  position: relative; overflow: hidden;
}
.picker-tile::before {
  content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px;
  background: var(--brand); transform: scaleX(0); transform-origin: left;
  transition: transform .15s ease;
}
.picker-tile:hover::before { transform: scaleX(1); }
.picker-tile:active { transform: scale(0.97); }
.picker-tile:hover { box-shadow: 0 6px 18px rgba(217, 54, 54, .10); border-color: var(--brand-red-line); }
/* 403 — a real page a denied route lands on (see showAccessDenied in
   shared/nav.js), not a silent bounce to the dashboard. */
.access-denied {
  max-width: 420px; margin: 60px auto; text-align: center; padding: 12px;
}
.access-denied-icon { font-size: 40px; margin-bottom: 6px; }
.access-denied h2 { margin: 0 0 10px; font-size: var(--font-xl); font-weight: 600; }
.access-denied p { margin: 0 0 8px; color: var(--ink); }
.access-denied .muted { margin-bottom: 20px; }

.picker-tile .emoji-badge {
  width: 52px; height: 52px; border-radius: 50%; background: var(--brand-light);
  display: flex; align-items: center; justify-content: center; font-size: 24px;
}
.picker-tile .tile-label { font-weight: 600; font-size: var(--font-base); letter-spacing: .01em; text-transform: uppercase; }

.card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: var(--space-5); margin-bottom: var(--space-4); box-shadow: var(--shadow-sm);
}
/* Sentence case. A card title is a name, not a banner — the reference sets
   "Sales Overview", "Needs Attention", "Recent Activity" as plain words. */
.card h2 { margin: 0 0 14px; font-size: 15px; font-weight: 600; letter-spacing: 0; }
.card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; }
.card-head h2 { margin: 0; }
.report-actions { display: flex; gap: var(--space-2); margin-bottom: 14px; }
.report-actions .btn { width: auto; font-size: var(--font-sm); padding: var(--space-2) var(--space-3); }
.card h3 { margin: 0 0 10px; font-size: var(--font-base); text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }

/* Tinted explainer box — how-this-works copy (e.g. Import Products' intro)
   reads as a plain gray paragraph on a white card otherwise, easy to mistake
   for a selection highlight. A soft brand tint + icon makes it a deliberate
   callout instead. */
.callout {
  display: flex; gap: 10px; align-items: flex-start;
  background: var(--brand-light); border: 1px solid var(--line); border-radius: var(--radius-md);
  padding: 12px 14px; margin: 0 0 16px; font-size: var(--font-base); line-height: 1.5; color: var(--ink);
}
.callout .callout-icon { font-size: 16px; line-height: 1.5; flex-shrink: 0; }

/* Purchase Order's "raise from Vendor / Warehouse" toggle — see entry.js */
.po-source-toggle {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 14px 18px; margin-bottom: 14px; display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
}
.po-source-toggle label { font-size: var(--font-base); font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
.segmented { display: flex; border: 1px solid var(--line); border-radius: var(--radius-pill); padding: 3px; gap: 3px; }
.seg-btn {
  border: none; background: transparent; padding: 7px 18px; border-radius: var(--radius-pill); font-size: var(--font-md);
  font-weight: 600; cursor: pointer; color: var(--ink);
}
.seg-btn.active { background: var(--brand); color: #fff; }
.seg-btn:not(.active):hover { background: var(--brand-light); }

/* Open Orders — one card per running tab, click through to add
   items/close/cancel. Not literal drag-and-drop columns (labels are
   free-typed, not a fixed set) — a status-grouped card board instead. */
.oo-board { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }
.oo-card {
  background: #fff; border: 1px solid var(--line); border-radius: var(--radius); padding: 16px;
  cursor: pointer; transition: box-shadow .15s, transform .15s;
}
.oo-card:hover { box-shadow: 0 4px 14px rgba(0,0,0,.08); transform: translateY(-1px); }
.oo-card .oo-court { font-size: 15px; font-weight: 600; }
.oo-card .oo-meta { color: var(--muted); font-size: 12.5px; margin: 6px 0 10px; }
.oo-card .oo-total { font-size: var(--font-xl); font-weight: 700; font-variant-numeric: tabular-nums; }
.oo-empty { color: var(--muted); font-size: var(--font-md); padding: 30px 0; text-align: center; }

/* EmptyState / ErrorState — see emptyStateHtml/errorStateHtml in
   shared/panel.js. */
.empty-state { text-align: center; padding: 48px 20px; color: var(--muted); }
.empty-state-icon { font-size: 34px; margin-bottom: 10px; }
.empty-state-title { font-size: var(--font-lg); font-weight: 600; color: var(--text-primary); margin-bottom: 4px; }
.empty-state-message { font-size: var(--font-base); max-width: 380px; margin: 0 auto; }
.error-state .empty-state-icon { color: var(--err); }

/* LoadingSkeleton — see skeletonRows/skeletonBlock in shared/panel.js. */
.skeleton-bar {
  height: 14px; border-radius: var(--radius-sm); width: 100%;
  background: linear-gradient(90deg, var(--line) 25%, var(--border) 37%, var(--line) 63%);
  background-size: 400% 100%; animation: skeleton-shimmer 1.4s ease infinite;
}
.skeleton-row td { padding: 10px 8px; }
@keyframes skeleton-shimmer { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }

/* FormDrawer — see openDrawer/closeDrawer in shared/panel.js. */
.drawer-backdrop {
  position: fixed; inset: 0; background: rgba(23,19,15,0); pointer-events: none;
  z-index: 210; transition: background .2s ease;
}
.drawer-backdrop.open { background: rgba(23,19,15,.45); pointer-events: auto; }
.drawer {
  position: absolute; top: 0; right: 0; height: 100%; width: 100%; max-width: 480px;
  background: #fff; box-shadow: -8px 0 24px rgba(0,0,0,.12);
  transform: translateX(100%); transition: transform .22s ease;
  overflow-y: auto; padding: var(--space-6); border-left: 4px solid var(--brand);
}
.drawer-backdrop.open .drawer { transform: translateX(0); }
.drawer h3 { margin: 0 0 6px; font-size: var(--font-xl); font-weight: 600; letter-spacing: -.01em; }
@media (max-width: 480px) { .drawer { max-width: 100%; } }

.field { margin-bottom: 14px; }
.field label { display: block; font-size: var(--font-base); font-weight: 600; margin-bottom: 6px; color: var(--ink); }
.field label .req { color: var(--brand); }
.field input, .field select, .field textarea {
  width: 100%; padding: 11px var(--space-3); border: 1px solid var(--line); border-radius: var(--radius-md);
  font-size: var(--font-lg); background: #fff; color: var(--ink);
}
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px rgba(217, 54, 54, .10);
}
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }
@media (max-width: 480px) { .field-row { grid-template-columns: 1fr; } }

/* Item lines as a real spreadsheet-style table — one column per field,
   one row per item — instead of a stacked card per item. Column headers
   carry the labels, so the per-field <label> (still rendered by the shared
   field controllers, unchanged) is hidden here rather than duplicated. */
.items-table { border-collapse: collapse; width: 100%; min-width: 560px; }
.items-table th {
  text-align: left; font-size: var(--font-xs); text-transform: uppercase; letter-spacing: .03em;
  color: var(--muted); padding: 4px 8px 8px; border-bottom: 1px solid var(--line); white-space: nowrap;
}
.items-table th .req { color: var(--brand); }
.items-table td { padding: 6px 8px; vertical-align: top; border-bottom: 1px solid var(--line); }
.items-table tr:last-child td { border-bottom: none; }
.items-table .field { margin: 0; min-width: 130px; }
.items-table .field label { display: none; }
.items-table input, .items-table select { padding: 9px 10px; font-size: var(--font-md); }
.items-table .autocomplete input { min-width: 160px; }
.items-table .remove-line {
  border: none; background: none; color: var(--muted); font-size: 17px; cursor: pointer; padding: 8px 4px;
}
.items-table .remove-line:hover { color: var(--brand); }
.items-table .photo-list { max-width: 180px; }

.autocomplete { position: relative; }
/* left/top/width are set inline in JS (position: fixed, viewport-relative)
   so the dropdown isn't clipped by a scrolling ancestor — e.g. the
   horizontal-scroll wrapper around the item-entry table. */
.ac-list {
  position: fixed; background: #fff;
  border: 1px solid var(--line); border-radius: var(--radius-md); box-shadow: 0 6px 20px rgba(0,0,0,.08);
  max-height: 220px; overflow-y: auto; z-index: 210; display: none;
}
.ac-list.open { display: block; }
.ac-item { padding: 10px 12px; cursor: pointer; font-size: var(--font-md); display: flex; justify-content: space-between; gap: 10px; align-items: baseline; }
.ac-item:hover, .ac-item.active { background: var(--brand-light); }
.ac-item.quickadd { color: var(--brand); font-weight: 600; border-top: 1px solid var(--line); }
.ac-item.quickadd-more { color: var(--muted); font-size: 12.5px; padding-top: 0; }
.ac-quickadd-form { padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; border-top: 1px solid var(--line); }
.ac-quickadd-form input { padding: 8px 10px; border: 1px solid var(--line); border-radius: var(--radius-sm); font-size: var(--font-base); }
.ac-quickadd-form .btn { width: auto; margin-top: 2px; }
.ac-contact-block {
  margin-top: 6px; padding: 8px 10px; display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.ac-contact-block input { flex: 1 1 100px; padding: 6px 8px; border: 1px solid var(--line); border-radius: 6px; font-size: 12.5px; }
.ac-contact-block .btn { width: auto; padding: 6px 12px; font-size: 12.5px; }
.ac-detail { color: var(--muted); font-size: 12.5px; white-space: nowrap; }

.items-toolbar { display: flex; gap: 8px; flex-wrap: wrap; }
.items-toolbar .btn { width: auto; flex: 1 1 auto; font-size: var(--font-sm); padding: 11px 14px; }

/* Details + Items now live in one card (one form, not two floating boxes)
   — a divider marks the section break instead of a gap between cards. */
#itemsCard { margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--line); }

.modal-backdrop {
  position: fixed; inset: 0; background: rgba(23,19,15,.45); z-index: 200;
  display: flex; align-items: center; justify-content: center; padding: 20px;
}
/* max-height + overflow-y so a modal with more content than fits the
   viewport (e.g. a variant picker listing every size/color combo) scrolls
   inside the box instead of silently overflowing the fixed backdrop with
   no scrollbar — confirmed live, that read as "the popup isn't moving at
   all" rather than "there's more below the fold". */
.modal { background: #fff; border-radius: var(--radius); padding: 20px; width: 100%; max-width: 560px; border-top: 4px solid var(--brand); max-height: min(85vh, 720px); overflow-y: auto; }
.modal-wide { max-width: 720px; }
.modal h3 { margin: 0 0 6px; font-size: 15px; font-weight: 600; }
.modal textarea {
  width: 100%; margin-top: 10px; padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--radius-md);
  font-size: var(--font-base); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; resize: vertical;
}
.modal-actions { display: flex; gap: 10px; margin-top: 14px; }
.modal-actions .btn { width: auto; flex: 1; }

/* Revision 13 — the page behind an open detail modal must not scroll. It
   did: a scroll wheel over the backdrop, or a dragging finger on a phone,
   moved the list underneath the dialog. Same lock the navigation drawer
   uses and for the same reason — overflow:hidden alone is ignored by iOS
   Safari — with panel.js setting the matching negative offset so the page
   holds still where it is instead of jumping to the top, and restoring the
   reading position on close. */
body.modal-open {
  position: fixed; left: 0; right: 0; width: 100%; overflow: hidden;
}

.payment-row { margin-bottom: 10px; }
.payment-row-main { display: flex; gap: 10px; align-items: center; }
.payment-row-main select { flex: 1.2; } .payment-row-main input { flex: 1; }
.payment-row .rm { border: none; background: none; color: var(--muted); font-size: 18px; cursor: pointer; }
.payment-recon { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 6px; padding-left: 4px; }
.payment-recon input[type=text] { flex: 1; min-width: 140px; }
.recon-photo-btn {
  display: inline-flex; align-items: center; gap: 4px; font-size: 12.5px; color: var(--brand);
  border: 1px dashed var(--line); border-radius: 8px; padding: 8px 10px; cursor: pointer; white-space: nowrap;
}
.recon-photo-btn:hover { background: var(--brand-light); }
.total-line { font-size: var(--font-base); color: var(--muted); margin-top: 4px; }
.total-line.ok { color: var(--ok); }
.total-line.err { color: var(--err); }

/* Due / entered / remaining, side by side. Three equal cells so the eye
   lands on the same place every time rather than hunting a sentence. */
.pay-balance {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-2);
  margin-top: var(--space-3); padding: var(--space-2) var(--space-3);
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-md);
}
.pay-balance-cell { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.pay-balance-cell em {
  font-style: normal; font-size: var(--font-xs); text-transform: uppercase;
  letter-spacing: .04em; color: var(--muted); font-weight: 600;
}
.pay-balance-cell b { font-size: var(--font-md); font-variant-numeric: tabular-nums; }
.pay-balance-cell.is-due b { color: var(--err); }
.pay-balance-cell.is-over b { color: var(--warning-ink); }
.pay-balance-cell.is-ok b { color: var(--ok); }
.link-btn { border: none; background: none; padding: 0; color: var(--brand); text-decoration: underline; font-size: inherit; cursor: pointer; }

/* Revision 14 — sentence case, solid borders.
   The buttons were UPPERCASE with letter-spacing and, for the secondary
   variant, a DASHED border. Uppercase display type on every button is what
   made the product read as a sports-club site rather than the operations
   tool the lock asks for, and a dashed border is the universal signal for
   "drop a file here" — it made every secondary action look provisional.
   The approved reference sets buttons in sentence case with solid edges.

   width:100% stays: this button is used inside forms far more often than
   in toolbars, and the toolbar call sites already override it. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
  border: 1px solid transparent; border-radius: var(--radius-btn);
  padding: 11px 16px; font-size: var(--font-md); font-weight: 600;
  font-family: inherit; cursor: pointer; width: 100%;
  /* Declared rather than inferred (Revision 17). At 14px with 11px padding
     this lands within a few pixels of 44 on most font stacks anyway, which
     is exactly why nobody noticed it was never stated — and why a button
     sat a little short of the 44px input beside it in every toolbar. */
  min-height: var(--control-height);
  transition: background .12s ease, border-color .12s ease, color .12s ease;
}
.btn-primary { background: var(--brand); color: #fff; border-color: var(--brand); }
.btn-primary:hover { background: var(--brand-dark); border-color: var(--brand-dark); }
.btn-secondary { background: var(--surface); color: var(--text-primary); border: 1px solid var(--border-strong); }
.btn-secondary:hover { background: var(--surface-muted); border-color: var(--text-muted); }
/* The other dense row action. Admin and payment-reconciliation put a
   ghost Save/Delete in a <td>, so like .btn-small it opts out of the base
   floor on a fine pointer and is raised back to it in the touch block —
   a 44px button in every row of those tables is the density the design
   lock §4 asks us not to lose. */
.btn-ghost {
  background: none; color: var(--brand); padding: 8px; width: auto;
  text-transform: none; letter-spacing: normal; min-height: 32px;
}
.btn-danger { background: #fff; color: var(--err); border: 1px solid var(--err); }
.btn-danger:hover { background: var(--danger-soft); }
/* A row-level action inside a dense table — same button, sized to sit in a
   38px table row without stretching it. Several screens were already doing
   this with a one-off inline style per call site (see .day-status .btn and
   .ac-contact-block .btn above); this is that same size, named. */
.btn-small {
  width: auto; padding: 6px 12px; font-size: var(--font-sm);
  background: var(--surface); color: var(--text-primary); border: 1px solid var(--border-strong);
  /* Overrides the base 44px on purpose: this variant exists to fit a
     --table-row-height row without stretching it. Stated as a number
     rather than left to fall out of 12px + 6px padding, so a font change
     cannot silently shrink the densest control in the app. The touch
     block raises it back to the floor where a finger is doing the
     pressing — by which width an opted-in .ds-stack table has already
     become a stack of cards, so the taller row costs nothing. */
  min-height: 32px;
}
.btn-small:hover { background: var(--surface-muted); }

.toast {
  position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
  background: var(--ink); color: #fff; padding: 12px 18px; border-radius: var(--radius-md); font-size: var(--font-md);
  box-shadow: 0 6px 20px rgba(0,0,0,.2); z-index: 100; max-width: calc(100% - 32px); text-align: center;
}
.toast.err { background: var(--err); }
.toast.ok { background: var(--ok); }

.error-text { color: var(--err); font-size: var(--font-base); margin-top: 6px; }
.muted { color: var(--muted); font-size: var(--font-base); }

table.simple { width: 100%; border-collapse: collapse; font-size: var(--font-md); }
table.simple th, table.simple td { text-align: left; padding: 9px var(--space-2); border-bottom: 1px solid var(--line); }
/* A tinted header strip, so the column names read as a header rather than
   as a first row set in grey. */
table.simple th {
  color: var(--text-secondary); font-weight: 600; font-size: var(--font-sm);
  text-transform: uppercase; letter-spacing: .04em; white-space: nowrap;
  background: var(--surface-muted);
}
table.simple th.sortable-th { cursor: pointer; user-select: none; }
table.simple th.sortable-th:hover { color: var(--ink); }
table.simple th .sort-ind { color: var(--brand); }
/* FilterBar — a row of status/category/date filters above a table (see
   inventory.js, rentals.js, order-management.js for real examples: a
   status <select> + a free-text search combined into one predicate for
   makeSortableTable's setFilter, or a plain query-string reload like
   rentals.js's Due Periods filters). */
.table-filters { display: flex; gap: var(--space-3); flex-wrap: wrap; margin-bottom: 12px; }
.table-filters input[type="text"], .table-filters select { max-width: 220px; }

/* SearchInput — a FilterBar's free-text field, with a leading icon instead
   of a bare <input>. Optional: .table-filters input[type=text] on its own
   already works fine for a simple case (most existing pages use that) —
   reach for .search-input when the icon affordance earns its keep, e.g. a
   search box standing alone rather than beside other filter controls. */
.search-input { position: relative; flex: 1; min-width: 180px; }
.search-input::before {
  content: '🔍'; position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
  font-size: 13px; opacity: .55; pointer-events: none;
}
.search-input input { padding-left: 32px !important; }
.table-scroll { overflow-x: auto; }
.access-grid th, .access-grid td { white-space: nowrap; }
/* A grid this wide (17 report tabs × every staff member) scrolls
   horizontally — without this, scrolling right to reach the later columns
   scrolls the staff-name column out of view too, so there's no way to tell
   which row you're checking anymore. Pins just that first column. */
.access-grid th:first-child, .access-grid td:first-child {
  position: sticky; left: 0; z-index: 1; background: var(--card);
  box-shadow: 1px 0 0 var(--line);
}
.scope-badge {
  display: inline-block; padding: 2px 9px; border-radius: var(--radius-pill); font-size: var(--font-xs); font-weight: 600;
  text-transform: uppercase; letter-spacing: .03em;
}
.scope-badge.header { background: var(--brand-light); color: var(--brand-dark); }
.scope-badge.item { background: var(--neutral-soft); color: var(--muted); }
.scope-badge.status-pending, .scope-badge.status-open { background: var(--warning-soft); color: var(--warning-ink); }
.scope-badge.status-approved, .scope-badge.status-received, .scope-badge.status-closed { background: var(--success-soft); color: var(--ok); }
.scope-badge.status-rejected, .scope-badge.status-cancelled { background: var(--danger-soft); color: var(--err); }
.scope-badge.status-partial { background: var(--neutral-soft); color: var(--neutral-ink); }

.day-status {
  padding: 12px 16px; border-radius: var(--radius-btn); font-size: var(--font-md); margin-bottom: 16px;
  display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
}
.day-status.open { background: var(--success-soft); color: var(--ok); }
.day-status.closed { background: var(--danger-soft); color: var(--err); }
.day-status .btn { width: auto; padding: 8px 16px; font-size: 12.5px; }
.clickable-row { cursor: pointer; }
.clickable-row:hover { background: var(--brand-light); }

.photo-list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.photo-chip {
  display: inline-flex; align-items: center; gap: 6px; background: var(--brand-light); color: var(--brand-dark);
  padding: 5px 8px; border-radius: 8px; font-size: 12px;
}
.photo-chip button { border: none; background: none; color: var(--brand-dark); cursor: pointer; font-size: 14px; line-height: 1; padding: 0; }

/* padding-bottom gives the scrollbar its own strip below the pills instead
   of overlapping their text; the thin custom scrollbar keeps that strip
   small instead of the OS's full-size (often opaque) default. */
.tabs {
  display: flex; gap: var(--space-2); overflow-x: auto; margin-bottom: var(--space-5);
  padding-bottom: 0; border-bottom: 1px solid var(--border); scrollbar-width: thin;
}
.tabs::-webkit-scrollbar { height: 6px; }
.tabs::-webkit-scrollbar-track { background: transparent; }
.tabs::-webkit-scrollbar-thumb { background: var(--line); border-radius: 999px; }
.tabs::-webkit-scrollbar-thumb:hover { background: var(--muted); }
/* Revision 14 — an underline bar, not a row of filled pills.
   The active tab was a solid near-black pill, which is the heaviest object
   on a page whose primary action is a red button; two competing anchors in
   one view is what the lock means by "one clear primary action". The
   reference underlines the current tab and leaves the rest as plain text,
   which is also what makes a long channel list (All Orders, POS, Website,
   Social, Open, Facility, Quotes) readable rather than a wall of chips. */
.tabs button, .tabs a {
  border: none; border-bottom: 2px solid transparent; background: none;
  padding: var(--space-2) var(--space-3); margin-bottom: -1px;
  font-size: var(--font-md); font-family: inherit; white-space: nowrap;
  cursor: pointer; font-weight: 500; color: var(--text-secondary);
}
.tabs button:hover, .tabs a:hover { color: var(--text-primary); }
.tabs button.active, .tabs a.active { color: var(--text-primary); font-weight: 600; border-bottom-color: var(--brand-red); }
.tabs a { text-decoration: none; display: inline-flex; align-items: center; }
/* The area strip sits directly under the page title, so it carries the
   gap the title would otherwise have to leave for it. */
.area-tabs { margin-top: var(--space-3); }
/* Revision 10 — the second-level report row lived here. Reports is two
   labelled dropdowns in the one sticky filter bar now (renderReportFilters
   in reports/reports.js), so there is no nested pill row left to style.
   `.tabs` itself stays: Tasks, Subscriptions and a handful of other screens
   still use a short strip, which at four or five entries is a segmented
   control rather than a sprawl. `.tabs-wrap` went with Admin's strip — it
   existed only to wrap that nineteen-button row. */

/* Admin's 19 tabs used to be grouped clusters of pill buttons here. They
   are five <optgroup>s in one grouped <select> now (ADMIN_TABS in
   admin.js) — the same five groups and the same nineteen destinations,
   taking one control's worth of the screen instead of the whole first
   screen. The rules that styled the strip are gone with it. */

.login-wrap {
  min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px;
  background: radial-gradient(circle at 50% 0%, #fff 0%, var(--bg) 65%);
}
.login-card {
  width: 100%; max-width: 380px; background: #fff; border-radius: 18px; padding: 34px 26px 30px;
  box-shadow: 0 16px 50px rgba(23,19,15,.10); border-top: 4px solid var(--brand); text-align: center;
}
/* The login card is the largest the mark ever renders, so it is where the
   25% stretch was most visible — 61.4 x 50.3 in an 84px disc. */
.login-card img.logo { width: 84px; height: 84px; object-fit: cover; border-radius: 50%; margin: 0 auto 16px; display: block; box-shadow: 0 0 0 4px var(--brand-light); }
/* The login card is the one screen that is ALL brand, so the wordmark
   face stays here — it is the product introducing itself, not a heading
   inside the product. */
.login-card h1 {
  font-family: var(--heading-font); font-weight: 700; font-size: var(--font-2xl);
  letter-spacing: .04em; margin: 0 0 4px; text-transform: uppercase;
}
.login-card p.sub { text-align: center; color: var(--muted); margin: 0 0 24px; font-size: var(--font-base); letter-spacing: .02em; }
.login-card .field { text-align: left; }

/* ---------- overview dashboard: stat tiles + charts ---------- */
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 14px; }
.stat-tile { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 16px 18px; }
.stat-tile .stat-label { font-size: var(--font-sm); color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: .04em; margin-bottom: 6px; }
.stat-tile .stat-value { font-size: 26px; font-weight: 600; letter-spacing: -.01em; font-variant-numeric: tabular-nums; }

.dash-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
@media (max-width: 760px) { .dash-grid { grid-template-columns: 1fr; } }
.dash-grid .card { margin-bottom: 0; }

.mini-list { list-style: none; margin: 0; padding: 0; }
.mini-list li { display: flex; align-items: center; justify-content: space-between; padding: 9px 0; border-bottom: 1px solid var(--line); font-size: var(--font-md); }
.mini-list li:last-child { border-bottom: none; }
.mini-list .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--brand); display: inline-block; margin-right: 8px; flex-shrink: 0; }
.mini-list .row-label { display: flex; align-items: center; color: var(--ink); }
.mini-list .row-value { font-weight: 600; color: var(--ink); font-variant-numeric: tabular-nums; }

.bar-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.bar-row .bar-label { width: 96px; flex-shrink: 0; font-size: var(--font-base); color: var(--muted); }
.bar-row .bar-track { flex: 1; background: var(--brand-light); border-radius: var(--radius-pill); height: 10px; overflow: hidden; }
.bar-row .bar-fill { background: var(--brand); height: 100%; border-radius: var(--radius-pill); }
.bar-row .bar-value { width: 64px; text-align: right; font-size: var(--font-base); font-weight: 600; font-variant-numeric: tabular-nums; }

/* ============================================================
   REVISION 6 — shared design-system layer
   ============================================================
   One vocabulary for the screens this revision builds (POS/printing,
   Accounting, Warehouse Operations, Social Orders, Notifications) and for
   every screen touched alongside them. Built ON the tokens declared at the
   top of this file rather than introducing a second palette, so an
   existing page that adopts a class here keeps the app's look exactly.

   Nothing below restyles an existing selector — these are all new class
   names, so adopting them is opt-in per screen and no current page changes
   appearance until it does. */

/* ---- page scaffolding ---- */
.ds-page-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--space-4); flex-wrap: wrap; margin-bottom: var(--space-5);
}
.ds-page-head h1 { margin: 0 0 2px; font-size: var(--font-2xl); }
.ds-page-head .ds-page-sub { color: var(--muted); font-size: var(--font-base); margin: 0; }
.ds-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center; }
.ds-actions .btn { width: auto; }

.ds-toolbar {
  display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius-lg);
  padding: var(--space-3); margin-bottom: var(--space-4);
}
.ds-toolbar input, .ds-toolbar select {
  height: var(--control-height); border: 1px solid var(--line); border-radius: var(--radius-md);
  padding: 0 var(--space-3); font-size: var(--font-md); background: #fff; color: var(--ink); min-width: 0;
}
.ds-toolbar .ds-grow { flex: 1 1 200px; }

/* ---- segmented control / tab strip ---- */
.ds-segmented {
  display: inline-flex; background: var(--bg); border: 1px solid var(--line);
  border-radius: var(--radius-pill); padding: 3px; gap: 2px; flex-wrap: wrap;
}
.ds-segmented button {
  border: none; background: none; cursor: pointer; border-radius: var(--radius-pill);
  padding: 7px var(--space-4); font-size: var(--font-base); font-weight: 600; color: var(--muted);
}
.ds-segmented button.active { background: var(--card); color: var(--ink); box-shadow: 0 1px 3px rgba(0,0,0,.08); }
.ds-segmented button .ds-count {
  display: inline-block; margin-left: 6px; background: var(--brand-light); color: var(--brand-dark);
  border-radius: var(--radius-pill); padding: 0 6px; font-size: var(--font-xs); font-weight: 700;
}

/* ---- cards & stat tiles ---- */
.ds-card {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius-lg);
  padding: var(--space-4); margin-bottom: var(--space-4);
}
.ds-card > h2 { margin: 0 0 var(--space-3); font-size: var(--font-xl); }
.ds-card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin-bottom: var(--space-3); }
.ds-card-head h2 { margin: 0; font-size: var(--font-xl); }

.ds-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--space-3); margin-bottom: var(--space-4); }
.ds-stat {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius-lg);
  padding: var(--space-4); display: flex; flex-direction: column; gap: 2px;
}
.ds-stat .ds-stat-label { font-size: var(--font-sm); color: var(--muted); text-transform: uppercase; letter-spacing: .04em; font-weight: 600; }
.ds-stat .ds-stat-value { font-size: 26px; font-weight: 600; letter-spacing: -.01em; font-variant-numeric: tabular-nums; line-height: 1.1; }
.ds-stat .ds-stat-note { font-size: var(--font-sm); color: var(--muted); }
.ds-stat.ds-stat-alert { border-color: var(--danger-line); background: var(--danger-soft); }
.ds-stat.ds-stat-alert .ds-stat-value { color: var(--err); }
/* A figure the app does not actually know is shown as unknown, never as
   zero. Used for an opening balance nobody has supplied yet. */
.ds-stat.ds-unknown .ds-stat-value { color: var(--muted); font-size: var(--font-lg); font-family: inherit; font-style: italic; }

/* ---- status badges ---- */
.ds-badge {
  display: inline-flex; align-items: center; gap: 5px; border-radius: var(--radius-pill);
  padding: 3px 10px; font-size: var(--font-sm); font-weight: 700; letter-spacing: .02em;
  background: var(--bg); color: var(--muted); border: 1px solid var(--line); white-space: nowrap;
}
.ds-badge.is-neutral  { background: var(--bg);      color: var(--muted);   border-color: var(--line); }
.ds-badge.is-info     { background: var(--neutral-soft);        color: var(--neutral-ink);        border-color: var(--neutral-line); }
.ds-badge.is-progress { background: var(--warning-soft);        color: var(--warning-ink);        border-color: var(--warning-line); }
.ds-badge.is-success  { background: var(--success-soft);        color: var(--success-ink);        border-color: var(--success-line); }
.ds-badge.is-danger   { background: var(--danger-soft);        color: var(--danger-ink);        border-color: var(--danger-line); }
.ds-badge.is-brand    { background: var(--brand-light); color: var(--brand-dark); border-color: var(--brand-red-line); }

/* ---- data table ---- */
.ds-table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--card); }
table.ds-table { width: 100%; border-collapse: collapse; font-size: var(--font-md); }
table.ds-table th {
  text-align: left; font-size: var(--font-sm); text-transform: uppercase; letter-spacing: .04em;
  color: var(--muted); font-weight: 700; padding: var(--space-3); border-bottom: 1px solid var(--line);
  background: var(--bg); position: sticky; top: 0; z-index: 1; white-space: nowrap;
}
table.ds-table td { padding: var(--space-3); border-bottom: 1px solid var(--line); vertical-align: top; }
table.ds-table tr:last-child td { border-bottom: none; }
table.ds-table tbody tr:hover { background: var(--brand-light); }
table.ds-table .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
table.ds-table .ds-sub { font-size: var(--font-sm); color: var(--muted); }
table.ds-table tr.ds-row-clickable { cursor: pointer; }

/* ---- loading / empty / error states ----
   Three distinct states, deliberately not one shared "nothing here" blob:
   "still loading", "genuinely nothing", and "we could not find out" mean
   different things and an operator needs to tell them apart. */
.ds-state { text-align: center; padding: var(--space-6) var(--space-4); color: var(--muted); }
/* An SVG, not a glyph — sized in the markup and coloured by the state it
   belongs to rather than by whichever emoji font the device happens to
   have. */
.ds-state .ds-state-icon { display: flex; justify-content: center; margin-bottom: var(--space-2); color: var(--text-muted); }
.ds-error .ds-state-icon { color: var(--danger); }
.ds-state .ds-state-title { font-weight: 700; color: var(--ink); margin-bottom: 4px; font-size: var(--font-md); }
.ds-state .ds-state-body { font-size: var(--font-base); }
.ds-state.ds-error .ds-state-title { color: var(--err); }
.ds-skeleton {
  background: linear-gradient(90deg, var(--bg) 25%, var(--border) 37%, var(--bg) 63%);
  background-size: 400% 100%; animation: ds-shimmer 1.3s ease infinite;
  border-radius: var(--radius-sm); height: 14px; margin: 8px 0;
}
@keyframes ds-shimmer { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
@media (prefers-reduced-motion: reduce) { .ds-skeleton { animation: none; } }

/* ---- drawer (side panel for a record's detail) ---- */
.ds-drawer-backdrop {
  position: fixed; inset: 0; background: rgba(23,19,15,.42); z-index: 60;
  display: flex; justify-content: flex-end;
}
.ds-drawer {
  background: var(--card); width: min(560px, 100%); height: 100%; overflow-y: auto;
  border-left: 3px solid var(--brand); padding: var(--space-5);
  box-shadow: -4px 0 24px rgba(0,0,0,.16);
}
.ds-drawer-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3); margin-bottom: var(--space-4); }
.ds-drawer-head h2 { margin: 0; font-size: var(--font-xl); }
.ds-drawer-close { border: none; background: none; font-size: 22px; line-height: 1; cursor: pointer; color: var(--muted); padding: 0 4px; }
.ds-drawer-close:hover { color: var(--ink); }

/* ---- definition list for record detail ---- */
.ds-dl { display: grid; grid-template-columns: minmax(110px, 34%) 1fr; gap: 6px var(--space-3); margin: 0 0 var(--space-4); font-size: var(--font-md); }
.ds-dl dt { color: var(--muted); font-size: var(--font-base); }
.ds-dl dd { margin: 0; font-weight: 600; overflow-wrap: anywhere; }

/* ---- timeline (workflow/actor history) ---- */
.ds-timeline { list-style: none; margin: 0; padding: 0 0 0 var(--space-4); border-left: 2px solid var(--line); }
.ds-timeline li { position: relative; padding: 0 0 var(--space-4) var(--space-4); }
.ds-timeline li::before {
  content: ''; position: absolute; left: calc(-1 * var(--space-4) - 6px); top: 4px;
  width: 10px; height: 10px; border-radius: 50%; background: var(--brand); border: 2px solid var(--card);
}
.ds-timeline .ds-timeline-when { font-size: var(--font-sm); color: var(--muted); }
.ds-timeline .ds-timeline-what { font-weight: 600; }

/* ---- inline notice ---- */
.ds-notice {
  border: 1px solid var(--line); border-left: 4px solid var(--muted); background: var(--card);
  border-radius: var(--radius-md); padding: var(--space-3) var(--space-4);
  font-size: var(--font-base); margin-bottom: var(--space-4);
}
.ds-notice.is-warn { border-left-color: var(--warning); background: var(--warning-soft); }
.ds-notice.is-danger { border-left-color: var(--err); background: var(--danger-soft); }
.ds-notice.is-info { border-left-color: var(--neutral-ink); background: var(--neutral-soft); }
.ds-notice strong { display: block; margin-bottom: 2px; }

/* ---- exact-bank picker (shared/bank-picker.js) ---- */
.bank-picker { display: flex; flex-direction: column; gap: 4px; margin-bottom: var(--space-2); flex-basis: 100%; }
.bank-picker-label { font-size: var(--font-base); color: var(--muted); font-weight: 600; }
.bank-picker-select {
  height: 40px; border: 1px solid var(--line); border-radius: var(--radius-md);
  padding: 0 var(--space-2); font-size: var(--font-md); background: #fff; color: var(--ink); width: 100%;
}
.bank-picker-hint { font-size: var(--font-sm); }

/* ---- responsive: tablet & laptop ---- */
@media (max-width: 1024px) {
  .ds-stats { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
  .ds-drawer { width: min(480px, 100%); }
}
@media (max-width: 760px) {
  .ds-page-head { flex-direction: column; align-items: stretch; }
  .ds-actions .btn { flex: 1 1 auto; }
  .ds-toolbar { flex-direction: column; align-items: stretch; }
  .ds-dl { grid-template-columns: 1fr; }
  .ds-dl dt { margin-top: var(--space-2); }
  .ds-drawer { width: 100%; border-left: none; border-top: 3px solid var(--brand); padding: var(--space-4); }
  /* A wide table stays a table and scrolls, rather than being reflowed
     into cards — an operator comparing requested/approved/dispatched
     quantities needs the columns lined up. */
  .ds-table-wrap { -webkit-overflow-scrolling: touch; }
  table.ds-table th { position: static; }
}

/* ---------- print ---------- */
@media print {
  .ds-page-head, .ds-toolbar, .ds-segmented, .ds-actions, .ds-drawer-backdrop { display: none !important; }
  .sidebar, .topbar, .tabs, .report-actions, .no-print, .btn { display: none !important; }
  .main { margin: 0 !important; }
  body { background: #fff; }
  .card { border: none; box-shadow: none; padding: 0; margin-bottom: 24px; page-break-inside: avoid; }
  .container { max-width: none; padding: 0; }
  table.simple th, table.simple td { padding: 6px 4px; }
  /* A report opened inside a detail modal (e.g. the Customer Ledger
     statement) should print as a plain page, not a floating overlay. */
  .modal-backdrop { position: static; background: none; padding: 0; display: block; }
  .modal { max-width: none; border-top: none; padding: 0; }
}

/* ---------- thermal receipt — 80mm paper, BC-85AC class ----------
   Revision 6. The single most important number here is the CONTENT width:
   80mm paper on this printer class has roughly 72mm of actually printable
   area once the carriage margins are taken out, and anything laid out
   wider than that is silently clipped at the right edge — which is how a
   receipt loses its Total column. 72mm is therefore the hard content
   width, with the @page margin making up the difference.

   Everything is pure black on white with no greys in the item/total rows:
   thermal heads have no greyscale, so a #555 "muted" tone prints as a
   dither pattern or as nothing at all. Muted text is rendered as smaller
   black instead of lighter black.

   The matching data contract is src/services/documents.js's THERMAL
   constant — the two are meant to state the same number, in the two
   places that each need it. */
.receipt-modal .modal { max-width: 340px; }
#receiptArea {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px; color: #000; background: #fff;
  width: 72mm; max-width: 72mm; margin: 0 auto;
  line-height: 1.42;
  /* A long product name must wrap inside the column rather than push the
     numeric columns off the paper. */
  overflow-wrap: anywhere; word-break: break-word;
}
#receiptArea .r-center { text-align: center; }
#receiptArea .r-logo { width: 40px; height: 40px; object-fit: cover; border-radius: 50%; margin: 0 auto 4px; display: block; }
#receiptArea .r-title { font-weight: 700; font-size: 14px; letter-spacing: .02em; }
#receiptArea .r-sub { font-size: 11px; }
#receiptArea .r-line { border-top: 1px dashed #000; margin: 6px 0; }
#receiptArea .r-line-solid { border-top: 1px solid #000; margin: 6px 0; }
#receiptArea .r-row { display: flex; justify-content: space-between; gap: 6px; }
#receiptArea .r-row > span:last-child { text-align: right; }
#receiptArea .r-total { font-weight: 700; font-size: 14px; }
/* No grey: thermal heads are 1-bit. "Muted" is smaller, not lighter. */
#receiptArea .r-muted { color: #000; font-size: 10px; }
#receiptArea .r-badge {
  display: inline-block; border: 1px solid #000; padding: 1px 6px;
  font-weight: 700; font-size: 11px; letter-spacing: .06em;
}
/* Items table: the name column takes the slack, the three numeric columns
   stay narrow, fixed and right-aligned so the Total column can never be
   pushed past the 72mm edge. */
#receiptArea .r-item-row {
  display: grid; grid-template-columns: minmax(0,1fr) 12mm 8mm 14mm; gap: 3px; font-size: 11px;
}
#receiptArea .r-item-row span:not(:first-child) { text-align: right; font-variant-numeric: tabular-nums; }
#receiptArea .r-item-row.r-head { font-weight: 700; }
#receiptArea .r-item-sub { font-size: 10px; padding-left: 4px; }

@media screen {
  #receiptPrintOnly { display: none; }
  /* On-screen preview inside a modal — same 72mm geometry, so what staff
     approve on screen is what the paper gets. */
  .receipt-preview #receiptArea { border: 1px solid var(--line); padding: 8px; }
}
@media print {
  body.printing-receipt > *:not(#receiptPrintOnly) { display: none !important; }
  body.printing-receipt #receiptPrintOnly { display: block !important; }
  body.printing-receipt { background: #fff; margin: 0; }
  body.printing-receipt #receiptArea {
    /* Continuous roll: never force a page break mid-receipt, and never let
       a row split across a tear. */
    page-break-inside: auto;
  }
  body.printing-receipt #receiptArea .r-item-row,
  body.printing-receipt #receiptArea .r-row { page-break-inside: avoid; }
  /* auto height = the roll cuts where the content ends, rather than
     feeding a fixed page of blank paper after every receipt. */
  @page { size: 80mm auto; margin: 4mm 4mm 6mm; }
}

/* ---------- A4 / Letter business documents ----------
   Invoice, Quotation and Sales Order. Deliberately NOT the thermal
   layout: full page width, real table rules, a bank block and signature
   areas. Rendered by public/shared/a4-document.js from the contract in
   src/services/documents.js. */
#a4Area {
  background: #fff; color: #000;
  width: 210mm; max-width: 100%; margin: 0 auto; padding: 14mm 14mm 12mm;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: 11pt; line-height: 1.45;
}
#a4Area .doc-head { display: flex; justify-content: space-between; gap: 16mm; align-items: flex-start; }
#a4Area .doc-issuer { max-width: 95mm; }
#a4Area .doc-issuer .doc-logo { height: 18mm; width: auto; max-width: 46mm; object-fit: contain; margin-bottom: 3mm; }
#a4Area .doc-issuer h1 { font-size: 15pt; margin: 0 0 1mm; letter-spacing: .01em; }
#a4Area .doc-issuer .doc-lines { font-size: 9.5pt; }
#a4Area .doc-meta { text-align: right; min-width: 62mm; }
#a4Area .doc-meta .doc-kind { font-family: var(--heading-font); font-size: 20pt; letter-spacing: .06em; text-transform: uppercase; margin: 0 0 2mm; }
#a4Area .doc-meta table { margin-left: auto; border-collapse: collapse; font-size: 9.5pt; }
#a4Area .doc-meta td { padding: 0.6mm 0 0.6mm 6mm; }
#a4Area .doc-meta td:first-child { padding-left: 0; color: #333; }
#a4Area .doc-parties { display: flex; gap: 8mm; margin: 8mm 0 6mm; }
#a4Area .doc-party { flex: 1; border: 0.3mm solid #000; padding: 3mm 4mm; }
#a4Area .doc-party h2 { font-size: 8.5pt; letter-spacing: .1em; text-transform: uppercase; margin: 0 0 2mm; }
#a4Area .doc-party .doc-party-name { font-weight: 700; }
#a4Area table.doc-items { width: 100%; border-collapse: collapse; font-size: 10pt; }
#a4Area table.doc-items th {
  text-align: left; border-bottom: 0.5mm solid #000; padding: 2mm 2mm;
  font-size: 8.5pt; letter-spacing: .06em; text-transform: uppercase;
}
#a4Area table.doc-items td { padding: 2mm; border-bottom: 0.2mm solid #999; vertical-align: top; }
#a4Area table.doc-items .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
#a4Area table.doc-items .doc-item-sub { font-size: 8.5pt; color: #444; }
#a4Area .doc-summary { display: flex; justify-content: space-between; gap: 8mm; margin-top: 6mm; }
#a4Area .doc-notes { flex: 1; font-size: 9.5pt; }
#a4Area .doc-notes h3 { font-size: 8.5pt; letter-spacing: .1em; text-transform: uppercase; margin: 0 0 1.5mm; }
#a4Area .doc-totals { min-width: 72mm; }
#a4Area .doc-totals table { width: 100%; border-collapse: collapse; font-size: 10pt; }
#a4Area .doc-totals td { padding: 1.4mm 0; }
#a4Area .doc-totals td:last-child { text-align: right; font-variant-numeric: tabular-nums; }
#a4Area .doc-totals tr.grand td { border-top: 0.5mm solid #000; font-weight: 700; font-size: 12pt; padding-top: 2.5mm; }
#a4Area .doc-bank { border: 0.3mm solid #000; padding: 3mm 4mm; margin-top: 6mm; font-size: 9.5pt; }
#a4Area .doc-bank h3 { font-size: 8.5pt; letter-spacing: .1em; text-transform: uppercase; margin: 0 0 2mm; }
#a4Area .doc-bank dl { display: grid; grid-template-columns: 34mm 1fr; gap: 1mm 3mm; margin: 0; }
#a4Area .doc-bank dt { color: #333; }
#a4Area .doc-bank dd { margin: 0; font-weight: 600; }
#a4Area .doc-signatures { display: flex; gap: 10mm; margin-top: 16mm; }
#a4Area .doc-sign { flex: 1; border-top: 0.3mm solid #000; padding-top: 2mm; font-size: 9pt; }
#a4Area .doc-footer { margin-top: 8mm; border-top: 0.2mm solid #999; padding-top: 2.5mm; font-size: 8.5pt; color: #333; text-align: center; }
#a4Area .doc-stamp {
  display: inline-block; border: 0.5mm solid #000; padding: 1mm 3mm;
  font-weight: 700; letter-spacing: .1em; text-transform: uppercase; font-size: 9pt;
}
#a4Area .doc-missing-asset {
  border: 0.3mm dashed #000; padding: 2mm 3mm; font-size: 8.5pt; margin-bottom: 3mm;
}

@media screen {
  #a4PrintOnly { display: none; }
  .a4-preview #a4Area { box-shadow: 0 2px 18px rgba(0,0,0,.14); border: 1px solid var(--line); }
}
@media print {
  body.printing-a4 > *:not(#a4PrintOnly) { display: none !important; }
  body.printing-a4 #a4PrintOnly { display: block !important; }
  body.printing-a4 { background: #fff; margin: 0; }
  body.printing-a4 #a4Area { width: auto; padding: 0; box-shadow: none; border: none; }
  /* A multi-page document repeats its column headers and never splits a
     line row or the totals block across the fold. */
  body.printing-a4 table.doc-items thead { display: table-header-group; }
  body.printing-a4 table.doc-items tr { page-break-inside: avoid; }
  body.printing-a4 .doc-summary,
  body.printing-a4 .doc-bank,
  body.printing-a4 .doc-signatures { page-break-inside: avoid; }
  @page { size: A4 portrait; margin: 12mm; }
}

/* ---------- Revision 7: Super Admin Control Center ----------
   One responsive shell for mobile, iPad and desktop. Built on the same
   --space/--font/--radius tokens and .ds-* components as every other
   Revision 6 screen, so this is a new arrangement of the existing design
   system rather than a second look.

   The layout is a two-pane rail + panel on desktop, and the rail turns
   into a horizontally scrolling chip row on narrow screens rather than a
   hamburger inside a hamburger — the page already sits inside the app's
   own sidebar drawer on mobile, and nesting a second collapsible
   navigation inside it is how a settings screen becomes unnavigable on a
   phone. */
.cc-layout { display: grid; grid-template-columns: 232px 1fr; gap: var(--space-4); align-items: start; }
.cc-rail {
  /* Revision 9 (part 4): this stuck at 16px from the top of the VIEWPORT,
     but the topbar is sticky at 0 and sits over it — so the first two or
     three domains in the rail scrolled underneath it and could not be
     clicked. Its max-height had the same off-by-a-topbar: the space
     available is the viewport minus the bar, not the whole viewport, so a
     long domain list also ran off the bottom of the screen. --topbar-h is
     the measured height nav.js publishes. */
  position: sticky; top: calc(var(--topbar-h, 0px) + var(--space-4));
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius-lg); padding: var(--space-2);
  max-height: calc(100vh - var(--topbar-h, 0px) - 32px); overflow-y: auto;
}
.cc-rail-group { font-size: var(--font-xs); text-transform: uppercase; letter-spacing: .05em; color: var(--muted); font-weight: 700; padding: var(--space-3) var(--space-3) var(--space-1); }
.cc-rail-item {
  display: flex; align-items: center; gap: var(--space-2); width: 100%; text-align: left;
  background: none; border: 1px solid transparent; border-radius: var(--radius-md); cursor: pointer;
  padding: 9px var(--space-3); font-size: var(--font-md); color: var(--ink); font-family: inherit;
}
.cc-rail-item:hover { background: var(--bg); }
.cc-rail-item.active { background: var(--brand-light); border-color: var(--brand-red-line); color: var(--brand-dark); font-weight: 600; }
.cc-rail-item:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.cc-rail-icon { display: flex; flex: 0 0 auto; color: var(--text-muted); }
.cc-rail-item.active .cc-rail-icon { color: var(--brand-red); }
.cc-rail-item .cc-rail-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cc-rail-item .cc-rail-count { font-size: var(--font-xs); color: var(--muted); font-variant-numeric: tabular-nums; }
.cc-rail-item .cc-rail-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--warning); flex-shrink: 0; }

.cc-main { min-width: 0; }
.cc-crumbs { display: flex; align-items: center; gap: 6px; font-size: var(--font-base); color: var(--muted); margin-bottom: var(--space-2); flex-wrap: wrap; }
.cc-crumbs button { background: none; border: none; padding: 0; color: var(--muted); cursor: pointer; font: inherit; text-decoration: underline; }
.cc-crumbs button:hover { color: var(--ink); }
.cc-crumbs .cc-crumb-current { color: var(--ink); font-weight: 600; }

/* Revision 10 — the search moved into the shared .ds-filterbar, so the
   bespoke wrapper and its floating `/` hint are gone; the hint is in the
   placeholder now, where it does not need a positioned overlay to exist. */
.cc-results { list-style: none; margin: 0 0 var(--space-4); padding: 0; border: 1px solid var(--line); border-radius: var(--radius-lg); overflow: hidden; background: var(--card); }
.cc-results li { border-bottom: 1px solid var(--line); }
.cc-results li:last-child { border-bottom: none; }
.cc-results button {
  display: block; width: 100%; text-align: left; background: none; border: none; cursor: pointer;
  padding: var(--space-3); font: inherit; color: var(--ink);
}
.cc-results button:hover, .cc-results button:focus-visible, .cc-results li.cc-hit button { background: var(--brand-light); outline: none; }
.cc-results .cc-result-domain { font-size: var(--font-sm); color: var(--muted); }

/* A setting: label + explanation on the left, the control on the right,
   stacking on narrow screens so nothing is ever squeezed to unreadable. */
.cc-setting { border-bottom: 1px solid var(--line); padding: var(--space-4) 0; }
.cc-setting:last-child { border-bottom: none; }
.cc-setting-row { display: flex; gap: var(--space-4); align-items: flex-start; }
.cc-setting-text { flex: 1; min-width: 0; }
.cc-setting-title { font-weight: 600; font-size: var(--font-md); display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.cc-setting-desc { color: var(--muted); font-size: var(--font-base); margin-top: 3px; }
.cc-setting-note { font-size: var(--font-base); margin-top: 6px; color: var(--warning-ink); background: var(--warning-soft); border-left: 3px solid var(--warning); padding: 6px var(--space-3); border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
.cc-setting-control { flex: 0 0 210px; display: flex; flex-direction: column; gap: 4px; align-items: stretch; }
.cc-setting-control select, .cc-setting-control input[type="text"], .cc-setting-control input[type="number"] {
  width: 100%; height: var(--control-height); border: 1px solid var(--line); border-radius: var(--radius-md);
  padding: 0 var(--space-3); font-size: var(--font-lg); background: var(--card); color: var(--ink);
}
.cc-setting-control select:disabled, .cc-setting-control input:disabled { background: var(--bg); color: var(--muted); cursor: not-allowed; }
.cc-setting-source { font-size: var(--font-sm); color: var(--muted); }
.cc-setting.cc-dirty { background: var(--warning-soft); box-shadow: inset 3px 0 0 var(--warning); padding-left: var(--space-3); }

/* Boolean control — a real checkbox styled as a switch, so it keeps
   keyboard focus, space-to-toggle and screen-reader semantics for free. */
.cc-switch { display: inline-flex; align-items: center; gap: var(--space-2); cursor: pointer; font-size: var(--font-md); }
.cc-switch input { width: 20px; height: 20px; accent-color: var(--brand); cursor: pointer; }
.cc-switch input:disabled { cursor: not-allowed; }

.cc-tag {
  display: inline-flex; align-items: center; gap: 4px; font-size: var(--font-xs); font-weight: 700;
  text-transform: uppercase; letter-spacing: .04em; border-radius: var(--radius-pill);
  padding: 2px 8px; border: 1px solid var(--line); background: var(--bg); color: var(--muted);
}
.cc-tag.is-protected { background: var(--warning-soft); color: var(--warning-ink); border-color: var(--warning-line); }
.cc-tag.is-dangerous { background: var(--danger-soft); color: var(--danger-ink); border-color: var(--danger-line); }
.cc-tag.is-enforced { background: var(--success-soft); color: var(--success-ink); border-color: var(--success-line); }
.cc-tag.is-foundation { background: var(--neutral-soft); color: var(--neutral-ink); border-color: var(--neutral-line); }

/* Sticky apply bar — appears only when something is actually changed. */
.cc-savebar {
  position: sticky; bottom: 0; z-index: 20; margin-top: var(--space-4);
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  background: var(--card); border: 1px solid var(--line); border-top: 3px solid var(--brand);
  border-radius: var(--radius-lg); padding: var(--space-3) var(--space-4);
  box-shadow: 0 -4px 16px rgba(0,0,0,.08);
}
.cc-savebar .cc-savebar-text { flex: 1; font-size: var(--font-md); }
.cc-savebar .btn { width: auto; }

/* Health rows: the status word is the loudest thing in the row, because
   UNKNOWN and NOT CHECKED have to read as differently as OK does. */
.cc-health { display: flex; flex-direction: column; gap: var(--space-2); }
.cc-health-row { display: flex; gap: var(--space-3); align-items: flex-start; padding: var(--space-3); border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--card); }
.cc-health-status {
  flex: 0 0 118px; font-size: var(--font-xs); font-weight: 700; letter-spacing: .04em; text-align: center;
  border-radius: var(--radius-pill); padding: 4px 8px; border: 1px solid var(--line); white-space: nowrap;
}
.cc-health-status.is-OK { background: var(--success-soft); color: var(--success-ink); border-color: var(--success-line); }
.cc-health-status.is-WARN { background: var(--warning-soft); color: var(--warning-ink); border-color: var(--warning-line); }
.cc-health-status.is-FAIL { background: var(--danger-soft); color: var(--danger-ink); border-color: var(--danger-line); }
.cc-health-status.is-UNKNOWN, .cc-health-status.is-NOT_CHECKED, .cc-health-status.is-NOT_CONFIGURED {
  background: var(--bg); color: var(--muted); border-color: var(--line); font-style: italic;
}
.cc-health-body { flex: 1; min-width: 0; }
.cc-health-label { font-weight: 600; }
.cc-health-detail { color: var(--muted); font-size: var(--font-base); margin-top: 2px; }

.cc-kv { display: grid; grid-template-columns: minmax(120px, 30%) 1fr; gap: 4px var(--space-3); font-size: var(--font-base); margin: var(--space-2) 0 0; }
.cc-kv dt { color: var(--muted); }
.cc-kv dd { margin: 0; font-weight: 600; overflow-wrap: anywhere; }

.cc-grantgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: var(--space-2); }
.cc-grant {
  display: flex; align-items: center; gap: var(--space-2); padding: 8px var(--space-3);
  border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--card); font-size: var(--font-base);
}
.cc-grant input { width: 18px; height: 18px; accent-color: var(--brand); }
.cc-grant.cc-grant-blocked { background: var(--bg); color: var(--muted); }
.cc-grant .cc-grant-why { font-size: var(--font-xs); color: var(--muted); }

@media (max-width: 900px) {
  .cc-layout { grid-template-columns: 1fr; }
  .cc-rail {
    position: static; max-height: none; display: flex; gap: var(--space-2);
    overflow-x: auto; overflow-y: hidden; padding: var(--space-2); -webkit-overflow-scrolling: touch;
  }
  .cc-rail-group { display: none; }
  .cc-rail-item { width: auto; flex: 0 0 auto; white-space: nowrap; }
  .cc-setting-row { flex-direction: column; }
  .cc-setting-control { flex: 1 1 auto; width: 100%; }
  .cc-health-row { flex-direction: column; gap: var(--space-2); }
  .cc-health-status { flex: 0 0 auto; align-self: flex-start; }
}

/* ============================================================
   REVISION 9 — team-ready UI/UX layer
   ============================================================
   Everything below is either a NEW class or a deliberate hardening of an
   existing one (focus rings, touch targets, small-screen behaviour).
   Nothing here introduces a second palette or a second set of spacing
   values — it is all built on the tokens declared at the top of this file.

   The three things this layer exists to fix, stated up front so a future
   revision knows what it may not undo:

     1. The app shell has to work at 390px, 820px and 1440px. Before this,
        the location switcher simply disappeared below 480px, which on a
        multi-branch account hid the single most consequential piece of
        state in the application.
     2. Keyboard and touch. Focus was invisible on most controls; several
        primary actions were under the 44px target a thumb needs.
     3. Wide ERP tables were horizontally scrollable and nothing else. A
        table can now opt into reflowing to labelled cards on a phone,
        for the screens where reading one record beats comparing columns.
   ------------------------------------------------------------ */

/* ---- accessibility primitives ---- */
/* A real label that is visually hidden — NOT display:none, which removes
   it from the accessibility tree along with everything else. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* One visible focus treatment for the whole application. :focus-visible
   (not :focus) so a mouse click does not leave a ring behind, but every
   keyboard and switch user can always see where they are. */
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
/* Skip-to-content, for a keyboard user who should not have to tab through
   a twelve-group navigation to reach the page. */
.skip-link {
  position: absolute; left: var(--space-3); top: -60px; z-index: 200;
  background: var(--brand); color: #fff; padding: 10px var(--space-4);
  border-radius: var(--radius-md); font-weight: 600; text-decoration: none;
  transition: top .12s ease;
}
.skip-link:focus { top: var(--space-3); }

@media (prefers-reduced-motion: reduce) {
  .sidebar, .skip-link, .picker-tile, .oo-card { transition: none !important; }
}

/* ---- shell: groups, scope, drawer ---- */
.side-group-heading { margin: 0; font-size: inherit; font-weight: inherit; }
/* Revision 14 — .side-group-title / .side-group-icon are gone with the
   twelve-group rail they belonged to. A group is now a word above a set of
   areas ("OPERATIONS"), and the icon belongs to the area, not the heading. */
/* Revision 10 — the foundation note was a filled amber block repeated once
   per foundation group, which on an admin account is several saturated
   panels competing with the navigation they sit inside. Same words, said
   quietly: it is a caveat, not an alert. */
/* home.js prints this same note on a white card. The rail's own copy is
   overridden with the rail, above — this is the light-surface default, and
   it has to stay readable on white. */
.side-foundation {
  margin: 0 0 var(--space-2); padding: 0; font-size: var(--font-xs); line-height: 1.4;
  color: var(--text-secondary); font-style: italic;
}
.side-scope {
  padding: var(--space-3) var(--space-4); border-top: 1px solid var(--sidebar-line);
  display: flex; flex-direction: column; gap: var(--space-2);
}
.side-scope-title {
  font-size: var(--font-xs); font-weight: 600; text-transform: uppercase;
  letter-spacing: .1em; color: var(--sidebar-muted);
}
.side-scope-field { display: flex; flex-direction: column; gap: 3px; font-size: var(--font-sm); color: var(--sidebar-muted); }
/* On the dark rail the select cannot inherit the light --surface ground it
   gets everywhere else — white boxes on a near-black panel are the single
   loudest thing on the screen, and this is a context control, not an
   action. */
.side-scope-field select {
  max-width: none; width: 100%; height: 38px;
  background: var(--sidebar-hover); border-color: var(--sidebar-line); color: var(--sidebar-text-strong);
}
/* Gated with the rest of the rail: under 861px this is the only branch
   scope switcher on screen, and the drawer has no toggle to un-collapse. */
@media (min-width: 861px) {
  .sidebar.collapsed .side-scope { display: none; }
}

/* Dimmed backdrop behind the mobile drawer — without one the drawer floats
   over a page that still looks interactive. */
.side-scrim {
  position: fixed; inset: 0; background: rgba(23,19,15,.45); z-index: 25;
  animation: scrim-in .15s ease;
}
@keyframes scrim-in { from { opacity: 0; } to { opacity: 1; } }
@media (min-width: 861px) { .side-scrim { display: none; } }
/* Revision 13 — overflow:hidden on <body> does not stop iOS Safari
   scrolling, and iPhone and iPad are the two devices this drawer exists
   for. position:fixed does; nav.js sets the matching negative top offset
   so the page holds still where it is instead of jumping to the top, and
   restores the scroll position on close. */
body.nav-drawer-open {
  position: fixed; left: 0; right: 0; width: 100%; overflow: hidden;
}

.scope-chip {
  display: inline-flex; align-items: center; gap: 5px; padding: 5px 10px;
  border-radius: var(--radius-pill); background: var(--surface-muted); color: var(--text-secondary);
  border: 1px solid var(--border);
  font-size: var(--font-sm); font-weight: 600; white-space: nowrap; max-width: 190px;
  overflow: hidden; text-overflow: ellipsis;
}

/* Touch targets. A 44px minimum is the smallest a thumb reliably hits, and
   the sidebar is used one-handed on a shop floor. */
@media (max-width: 860px) {
  .side-area-link { min-height: 46px; }
  .side-link { min-height: 44px; }
  .side-section-toggle { min-height: 44px; width: 46px; }
  /* .iconbtn used to say 40px here — four pixels under the number its
     three siblings in this very block chose, for no stated reason. It is
     held to the floor with the rest of the shared controls below. */
}

@media (max-width: 600px) {
  /* Word labels go, icons stay — the buttons keep their full tap area. */
  .topbar .menu-word, .topbar .topbar-btn-word { display: none; }
}
@media (max-width: 480px) {
  /* The full selects live in the drawer at this width (see .side-scope);
     the read-only chip keeps the current scope on screen regardless. */
  .topbar-whoami { display: none; }
  .scope-chip { max-width: 140px; }
}

/* ---- responsive ERP tables ----
   Opt-in per screen with class="ds-table ds-stack". A table that stays a
   table (comparing requested/approved/dispatched across rows) simply does
   not add the class and keeps scrolling horizontally, which is still the
   right answer for those. Where it IS added, each row becomes a card on a
   phone with its column header printed beside every value, so a record
   stays readable without a horizontal scroll nobody discovers. */
@media (max-width: 760px) {
  table.ds-stack thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
  table.ds-stack, table.ds-stack tbody, table.ds-stack tr, table.ds-stack td { display: block; width: 100%; }
  table.ds-stack tr {
    border: 1px solid var(--line); border-radius: var(--radius-md);
    margin-bottom: var(--space-3); padding: var(--space-2); background: var(--card);
  }
  table.ds-stack tbody tr:hover { background: var(--card); }
  table.ds-stack td {
    display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-3);
    padding: 7px var(--space-2); border-bottom: 1px solid var(--line); text-align: right;
  }
  table.ds-stack td:last-child { border-bottom: none; }
  table.ds-stack td::before {
    content: attr(data-label); text-align: left; flex: 0 0 42%;
    font-size: var(--font-sm); text-transform: uppercase; letter-spacing: .04em;
    color: var(--muted); font-weight: 700;
  }
  /* A cell with no data-label (a spacer, an actions cell) must not print
     an empty label column and lose half its width to nothing. */
  table.ds-stack td:not([data-label])::before { content: none; }
  table.ds-stack td.ds-stack-full { display: block; text-align: left; }
  table.ds-stack td.ds-stack-full::before { display: block; margin-bottom: 3px; }
  .ds-table-wrap.ds-stack-wrap, .table-scroll.ds-stack-wrap { border: none; background: none; overflow-x: visible; }
}

/* ---- destructive confirmation ----
   A window.confirm() cannot say what is about to be destroyed, cannot be
   styled to look dangerous, and on iOS reads as a browser warning rather
   than an application one. */
.ds-confirm-backdrop {
  position: fixed; inset: 0; background: rgba(23,19,15,.5); z-index: 120;
  display: flex; align-items: center; justify-content: center; padding: var(--space-4);
}
.ds-confirm {
  background: var(--card); border-radius: var(--radius-lg); padding: var(--space-5);
  width: 100%; max-width: 460px; border-top: 4px solid var(--err);
  box-shadow: 0 20px 60px rgba(23,19,15,.25);
}
.ds-confirm.is-safe { border-top-color: var(--brand); }
.ds-confirm h2 { margin: 0 0 var(--space-2); font-size: var(--font-xl); }
.ds-confirm p { margin: 0 0 var(--space-3); font-size: var(--font-md); line-height: 1.5; }
.ds-confirm-consequences {
  margin: 0 0 var(--space-4); padding: var(--space-3); background: var(--danger-soft); border-radius: var(--radius-md);
  font-size: var(--font-base); color: var(--danger-ink); list-style: none;
}
.ds-confirm-consequences li { margin-bottom: 4px; }
.ds-confirm-consequences li:last-child { margin-bottom: 0; }
.ds-confirm-actions { display: flex; gap: var(--space-2); justify-content: flex-end; flex-wrap: wrap; }
.ds-confirm-actions .btn { width: auto; min-width: 120px; }
@media (max-width: 480px) {
  .ds-confirm-actions { flex-direction: column-reverse; }
  .ds-confirm-actions .btn { width: 100%; }
}

/* ---- truth markers ----
   Three distinct honest states, so "we have not built this", "nobody has
   configured it" and "there is genuinely nothing" never look alike. */
.ds-truth {
  display: flex; gap: var(--space-3); align-items: flex-start;
  padding: var(--space-3) var(--space-4); border-radius: var(--radius-md);
  border: 1px solid var(--line); background: var(--bg); margin-bottom: var(--space-3);
}
.ds-truth-icon { display: flex; flex-shrink: 0; margin-top: 1px; color: currentColor; }
.ds-truth-body { min-width: 0; }
.ds-truth-title { font-weight: 700; font-size: var(--font-md); margin-bottom: 2px; }
.ds-truth-text { font-size: var(--font-base); color: var(--muted); line-height: 1.5; }
.ds-truth-list { margin: var(--space-2) 0 0; padding-left: 18px; font-size: var(--font-base); color: var(--muted); }
.ds-truth.is-foundation { background: var(--warning-soft); border-color: var(--warning-line); }
.ds-truth.is-foundation .ds-truth-title { color: var(--warning-ink); }
.ds-truth.is-notconfigured { background: var(--neutral-soft); border-color: var(--neutral-line); }
.ds-truth.is-notconfigured .ds-truth-title { color: var(--neutral-ink); }
.ds-truth.is-nodata { background: var(--bg); }

/* ---- role-aware home ----
   Revision 10: what used to be a band of four 76px-tall bordered tiles is
   now a row of ordinary buttons in the page head, because a quick action
   is an action — it does not need to be the largest object on the screen.
   The role chip lost its red fill for the same reason the sidebar group
   headings did: on the dashboard, red should mean "ring up a sale", and it
   cannot mean that while four other things are also red. */
.home-quick { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.home-quick .btn { width: auto; }
.home-role-chip {
  display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: var(--radius-pill);
  background: var(--bg); color: var(--muted); font-size: var(--font-xs); font-weight: 700;
  text-transform: uppercase; letter-spacing: .05em; border: 1px solid var(--line);
  vertical-align: middle;
}
/* The module directory. Dense text links inside a collapsed section, not a
   wall of 48px bordered cards — it is a map somebody consults, not a
   surface they work on. */
.home-dir-group { margin-bottom: var(--space-4); }
.home-dir-group:last-child { margin-bottom: 0; }
.home-dir-title {
  display: flex; align-items: center; gap: var(--space-2);
  font-size: var(--font-xs); text-transform: uppercase; letter-spacing: .08em;
  font-weight: 600; color: var(--text-secondary); margin: 0 0 var(--space-2);
}
.home-dir-title .nav-icon { color: var(--text-muted); }
.home-links { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 2px var(--space-3); }
.home-link {
  display: flex; align-items: center; gap: var(--space-2); padding: 5px var(--space-2);
  border-radius: var(--radius-sm); text-decoration: none; color: var(--ink);
  font-size: var(--font-md); min-width: 0;
}
.home-link:hover { background: var(--bg); }

@media (max-width: 600px) {
  .home-links { grid-template-columns: 1fr; }
  .home-quick .btn { flex: 1 1 calc(50% - var(--space-2)); }
}

/* ---- stock count / verification ---- */
.sc-progress-track { height: 10px; background: var(--brand-light); border-radius: var(--radius-pill); overflow: hidden; }
.sc-progress-fill { height: 100%; background: var(--brand); border-radius: var(--radius-pill); transition: width .2s ease; }
.sc-progress-label { display: flex; justify-content: space-between; font-size: var(--font-sm); color: var(--muted); margin-bottom: 4px; }
.sc-search-results {
  border: 1px solid var(--line); border-radius: var(--radius-md); max-height: 320px; overflow-y: auto;
  background: var(--card); margin-top: var(--space-2);
}
.sc-search-item {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  padding: var(--space-3); border-bottom: 1px solid var(--line); width: 100%; text-align: left;
  background: none; border-left: none; border-right: none; border-top: none; cursor: pointer; font-size: var(--font-md);
  min-height: 52px;
}
.sc-search-item:last-child { border-bottom: none; }
.sc-search-item:hover, .sc-search-item:focus-visible { background: var(--brand-light); }
.sc-search-meta { font-size: var(--font-sm); color: var(--muted); }
.sc-qty-input {
  width: 100%; max-width: 130px; height: var(--control-height); text-align: right;
  font-size: var(--font-lg); font-variant-numeric: tabular-nums;
  border: 1px solid var(--line); border-radius: var(--radius-md); padding: 0 var(--space-3);
}
.sc-qty-input:focus { border-color: var(--brand); }
.sc-variance { font-variant-numeric: tabular-nums; font-weight: 700; }
.sc-variance.is-over { color: var(--warning-ink); }
.sc-variance.is-short { color: var(--err); }
.sc-variance.is-match { color: var(--ok); }
.sc-variance.is-unknown { color: var(--muted); font-weight: 400; font-style: italic; }
.sc-line-note {
  width: 100%; border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 6px var(--space-2); font-size: var(--font-base); min-height: 38px;
}
.sc-save-state { font-size: var(--font-sm); color: var(--muted); min-height: 18px; }
.sc-save-state.is-saving { color: var(--warning-ink); }
.sc-save-state.is-saved { color: var(--ok); }
.sc-save-state.is-error { color: var(--err); }

/* ---- Control Center: search + provenance ---- */
.cc-search-wrap { position: relative; margin-bottom: var(--space-3); }
.cc-search-input {
  width: 100%; height: var(--control-height); border: 1px solid var(--line); border-radius: var(--radius-md);
  padding: 0 var(--space-4) 0 38px; font-size: var(--font-md); background: var(--card);
}
.cc-search-input:focus { border-color: var(--brand); }
.cc-search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--muted); }
.cc-search-count { font-size: var(--font-sm); color: var(--muted); margin-top: 4px; }
.cc-scope-chips { display: flex; gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-3); }
.cc-scope-chip {
  display: inline-flex; align-items: center; gap: 5px; padding: 5px 12px; border-radius: var(--radius-pill);
  border: 1px solid var(--line); background: var(--card); font-size: var(--font-sm); font-weight: 600;
  color: var(--muted); cursor: pointer; min-height: 34px;
}
.cc-scope-chip.active { background: var(--brand); border-color: var(--brand); color: #fff; }
.cc-scope-chip:not(.active):hover { border-color: var(--brand); color: var(--brand); }
.cc-provenance {
  display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center;
  font-size: var(--font-sm); color: var(--muted); margin-top: 4px;
}
.cc-provenance .cc-prov-tag {
  padding: 2px 8px; border-radius: var(--radius-pill); background: var(--bg); border: 1px solid var(--line);
}
.cc-provenance .cc-prov-tag.is-override { background: var(--brand-light); color: var(--brand-dark); border-color: var(--brand-red-line); }
.cc-provenance .cc-prov-tag.is-default { font-style: italic; }

/* ---- topbar at phone widths ----
   The topbar is a single non-wrapping flex row carrying, at 390px: the
   menu button, the logo, the brand word, the scope chip, a low-stock pill,
   Tasks and the notification bell. Without these three rules the brand
   word wins the space and the bell — the only way to see an urgent
   notification — is pushed off the right edge. "No clipped primary
   actions" is the requirement; this is what enforces it. */
@media (max-width: 600px) {
  .topbar { flex-wrap: wrap; row-gap: 6px; padding: 8px var(--space-3); }
  .topbar-right { gap: 6px; }
}
@media (max-width: 420px) {
  /* Logo stays (it is the app's identity and costs 28px); the wordmark
     goes, because the sidebar shows it and the bell must not. */
  .topbar span.brand-word.mobile-brand-word { display: none; }
  .topbar .low-stock-alert { order: 10; flex-basis: 100%; margin-left: 0; }
}

/* A stacked table inside the drawer or a narrow card must not inherit the
   sticky header offset that only makes sense on a real table. */
@media (max-width: 760px) {
  table.ds-stack th { position: static; }
  /* A row header (<th> inside <tbody>) is rare here but would otherwise
     stay a table-cell inside a row that is now a block, and take the whole
     card apart. It behaves as the label line for its card. */
  table.ds-stack tbody th {
    display: block; width: 100%; text-align: left;
    padding: 7px var(--space-2); border-bottom: 1px solid var(--line);
  }
}

/* ---- the item-entry grid on a phone ----
   Returns, Exchanges, Receive Stock (GRN), Damages, Stock Transfer,
   Record Payment and half a dozen more are all the same screen —
   entry.html — and its item grid is a real spreadsheet: one column per
   field, the column header carrying the label, and the per-field <label>
   hidden because the header already says it.

   That trade only works while the header is on screen. At 390px it is
   not: .items-table has a 560px min-width, so a phone shows about three
   of the six columns and the labels have scrolled off to the right. A
   cashier taking a return on the shop floor was being asked to type into
   unlabelled boxes and to discover, by horizontal scrolling nothing
   indicates, that there are more of them.

   Below 700px each line becomes a card, every field gets its own label
   back, and the horizontal scroll goes away. No JavaScript is involved:
   the markup entry.js and form.js emit is already exactly right for
   this — a .field with a <label> in it — it was only ever being hidden. */
@media (max-width: 700px) {
  .items-table { min-width: 0; }
  .items-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
  .items-table, .items-table tbody, .items-table tr, .items-table td { display: block; width: 100%; }
  .items-table tr {
    border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--card);
    padding: var(--space-2) var(--space-2) var(--space-1); margin-bottom: var(--space-3);
    position: relative;
  }
  .items-table td { border-bottom: none; padding: 4px 0; }
  /* The variant picker cell is empty until a product with variants is
     chosen. As a block it would otherwise leave a gap that reads as a
     missing field. */
  .items-table td:empty { display: none; }
  .items-table .field { min-width: 0; margin-bottom: var(--space-1); }
  .items-table .field label {
    display: block; font-size: var(--font-sm); font-weight: 600; color: var(--muted);
    text-transform: uppercase; letter-spacing: .03em; margin-bottom: 3px;
  }
  .items-table .autocomplete input, .items-table .photo-list { min-width: 0; max-width: none; }
  /* Remove sits in the card's top-right corner as a 44px target rather
     than as a full-width block button that reads as a primary action. */
  .items-table .remove-line {
    position: absolute; top: 4px; right: 4px;
    min-width: 44px; min-height: 44px; font-size: 20px;
  }
  .items-table td:last-child { padding: 0; }
  /* Keep the first field clear of the remove target in the corner. */
  .items-table tr > td:first-child { padding-right: 48px; }
  /* The wrapper's horizontal scroll and border are meaningless now. */
  #itemsCard .table-scroll { overflow-x: visible; border: none; }
}

/* ----------------------------------------------------------------------
   Revision 9 (part 4)
   ---------------------------------------------------------------------- */

/* Day End's section index. Day End is fourteen stacked tables; on an iPad
   that is one unbroken column, and part 2's card layout makes it several
   times longer on a phone. The index sticks under the topbar and scrolls
   sideways rather than wrapping to four rows and eating the screen it is
   supposed to be helping somebody get past.

   scroll-margin-top on the sections is what stops a jump link landing a
   heading UNDERNEATH the sticky index it was just tapped in. */
.de-index {
  /* Under the topbar, not behind it. The topbar is sticky at top:0 with a
     higher z-index and a height that changes with the account and the
     width — nav.js measures it and publishes --topbar-h. */
  position: sticky; top: var(--topbar-h, 0px); z-index: 5;
  display: flex; gap: var(--space-2); overflow-x: auto; -webkit-overflow-scrolling: touch;
  padding: var(--space-2) 0; margin: var(--space-4) 0 var(--space-2);
  background: var(--card); border-bottom: 1px solid var(--line);
  scrollbar-width: thin;
}
.de-index a {
  flex: 0 0 auto; text-decoration: none; white-space: nowrap;
  display: inline-flex; align-items: center; min-height: 36px;
  padding: 0 var(--space-3); border: 1px solid var(--line); border-radius: var(--radius-pill);
  font-size: var(--font-base); color: var(--muted); background: var(--bg);
}
.de-index a:hover { color: var(--ink); border-color: var(--muted); }
/* A jump link has to clear BOTH sticky bars, or the heading it just took
   you to is the one thing you cannot see. 56px is the index's own height
   at its current padding and font size. */
.de-section { scroll-margin-top: calc(var(--topbar-h, 0px) + 56px); }
.de-section h3:focus { outline: none; }
.de-section h3:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; }

/* Customers directory. One card per person on a phone, a two/three-up grid
   from tablet width. Deliberately not a table: the useful facts about a
   customer are a name, a way to reach them and their agreed terms, which
   is a record, not a row to compare across. */
.cust-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-3); }
@media (min-width: 700px) { .cust-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1100px) { .cust-grid { grid-template-columns: repeat(3, 1fr); } }
.cust-card {
  border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--card);
  padding: var(--space-3); display: flex; flex-direction: column; gap: var(--space-2);
  text-align: left; width: 100%; font: inherit; color: inherit; cursor: pointer;
}
.cust-card:hover { border-color: var(--muted); }
.cust-card-name { font-weight: 700; font-size: var(--font-md); }
.cust-card-contact { font-size: var(--font-base); color: var(--muted); display: flex; flex-direction: column; gap: 2px; }
.cust-card-terms { display: flex; flex-wrap: wrap; gap: var(--space-1); }
.cust-term {
  font-size: var(--font-sm); border: 1px solid var(--line); border-radius: var(--radius-pill);
  padding: 2px var(--space-2); color: var(--muted); background: var(--bg);
}

/* ======================================================================
   Revision 10 — the density layer
   ======================================================================
   The launch-critical correction on the control issue: the portal read as
   a card wall, every figure at equal weight, with filters scattered across
   a topbar, a drawer and the page body.

   The rules below are the visual half of that (the structural half is in
   ds.js — dsKpiRow will not render a seventh figure, and dsSection starts
   closed). Three things they deliberately do:

     * FEWER SURFACES. One border, no shadow, no nested card-in-card. A
       repeated element gets tighter internal padding; the space goes
       BETWEEN sections instead, which is what actually creates hierarchy.
     * QUIETER COLOUR. Neutral charcoal dominates. Brand red is reserved
       for a primary CTA, a selected item and an alert — nothing else.
     * NOTHING SCROLLS SIDEWAYS EXCEPT ITS OWN CONTAINER. A wide table
       scrolls inside .ds-table-wrap; the page never does.
*/

/* ---- KPI row: the only figures above the fold --------------------- */
.ds-kpis {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(158px, 1fr));
  gap: var(--space-3);
  margin: 0 0 var(--space-5);
}
.ds-kpi {
  display: flex; flex-direction: column; gap: var(--space-1);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-5); box-shadow: var(--shadow-sm);
  color: var(--text-primary); text-decoration: none; min-width: 0;
}
a.ds-kpi:hover { border-color: var(--muted); }
/* Sentence case label, and the figure in the body face rather than the
   condensed display one. "PKR 2,134,550" set in Oswald is a headline; the
   reference sets it as a number, which is what it is. */
.ds-kpi-label {
  font-size: var(--font-base); font-weight: 500; color: var(--text-secondary);
  line-height: 1.3; overflow-wrap: anywhere;
}
.ds-kpi-value {
  font-size: 26px; font-weight: 600; line-height: 1.15; letter-spacing: -.01em;
  font-variant-numeric: tabular-nums; overflow-wrap: anywhere;
}
.ds-kpi-note { font-size: var(--font-xs); color: var(--muted); line-height: 1.35; overflow-wrap: anywhere; }
/* Alert is a thin left rule and a coloured number, not a saturated block.
   A wall of red tiles reads as "everything is broken", which is precisely
   the signal an alert is supposed to carry on its own. */
.ds-kpi.is-alert { border-left: 3px solid var(--err); }
.ds-kpi.is-alert .ds-kpi-value { color: var(--err); }
.ds-kpi.is-unknown .ds-kpi-value,
.ds-kpi.is-pending .ds-kpi-value {
  font-family: inherit; font-size: var(--font-lg); color: var(--muted); font-style: italic;
}
.ds-kpis-more { margin: calc(var(--space-5) * -1 + var(--space-2)) 0 var(--space-5); font-size: var(--font-base); color: var(--muted); }

/* ---- sections: one open, the rest collapsed ---------------------- */
.ds-section {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius-lg);
  margin-bottom: var(--space-4);
}
.ds-section-summary {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  cursor: pointer; list-style: none; min-width: 0;
}
.ds-section.is-primary > .ds-section-summary { cursor: default; border-bottom: 1px solid var(--line); }
.ds-section-summary::-webkit-details-marker { display: none; }
details.ds-section > summary:hover { background: var(--bg); border-radius: var(--radius-lg) var(--radius-lg) 0 0; }
details.ds-section:not([open]) > summary:hover { border-radius: var(--radius-lg); }
details.ds-section > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.ds-section-title {
  font-size: 15px; letter-spacing: 0; min-width: 0; overflow-wrap: anywhere;
  margin: 0; font-weight: 600;
}
.ds-section-count {
  font-size: var(--font-sm); font-weight: 700; color: var(--muted);
  background: var(--bg); border-radius: var(--radius-pill); padding: 1px 8px; flex: 0 0 auto;
}
.ds-section-note { font-size: var(--font-base); color: var(--muted); min-width: 0; overflow-wrap: anywhere; }
.ds-section-chevron { margin-left: auto; color: var(--muted); font-size: 12px; transition: transform .15s ease; flex: 0 0 auto; }
details.ds-section[open] > summary .ds-section-chevron { transform: rotate(180deg); }
.ds-section-summary .ds-section-actions { margin-left: auto; }
.ds-section-body { padding: var(--space-4); padding-top: var(--space-3); min-width: 0; }
.ds-section.is-primary > .ds-section-body { padding-top: var(--space-4); }
/* A section body must never widen the page. Wide content scrolls in its
   own wrapper — .ds-table-wrap already does exactly this. */
.ds-section-body > * { max-width: 100%; }

/* ---- one compact filter bar -------------------------------------- */
.ds-filterbar {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--space-3);
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-3); margin-bottom: var(--space-4);
}
/* Sticks under the topbar, whose real height nav.js measures and publishes
   as --topbar-h. A hard-coded offset hides the bar behind the topbar on
   exactly the accounts whose topbar wrapped to two rows. */
.ds-filterbar.is-sticky { position: sticky; top: var(--topbar-h, 0px); z-index: 15; }
.ds-filter-field { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ds-filter-field.ds-grow { flex: 1 1 180px; }
.ds-filter-label {
  font-size: var(--font-xs); text-transform: uppercase; letter-spacing: .05em;
  font-weight: 700; color: var(--muted);
}
.ds-filter-control {
  height: 34px; padding: 0 var(--space-2); font-size: var(--font-md);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--card); color: var(--ink); max-width: 100%;
}
.ds-filter-control:focus-visible { outline: none; border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-light); }
.ds-filter-note { font-size: var(--font-xs); color: var(--muted); }
.ds-filterbar-trailing { margin-left: auto; display: flex; align-items: center; gap: var(--space-2); }
.ds-filterbar-trailing .btn { width: auto; }

/* ---- responsive ---------------------------------------------------
   iPad: three figures a row. Phone: two. Never one — a single-column
   stack of six tiles pushes the primary section a full screen down. */
@media (max-width: 980px) {
  .ds-kpis { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 680px) {
  .ds-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); }
  .ds-kpi { padding: var(--space-2) var(--space-3); }
  .ds-kpi-value { font-size: 20px; }
  .ds-filterbar { gap: var(--space-2); padding: var(--space-2); }
  .ds-filter-field { flex: 1 1 calc(50% - var(--space-2)); }
  .ds-filterbar-trailing { margin-left: 0; flex: 1 1 100%; }
  .ds-section-body { padding: var(--space-3); }
  .ds-section-summary { padding: var(--space-3); gap: var(--space-2); }
}
@media print {
  .ds-filterbar { display: none !important; }
  .ds-section-chevron { display: none; }
  .ds-section { break-inside: avoid; }
}
/* A collapsed section still has to PRINT — paper has no chevron to click.
   The `open` attribute is set on beforeprint in ds-enhance.js rather than
   here, because whether a closed <details>'s children can be revealed by
   CSS alone is not consistent across engines. */

/* ====================================================================
   REVISION 11 — the app shell's two missing pieces.

   Global search, and a phone's own primary navigation. Both were named in
   the approved interaction direction and neither existed: the sidebar
   holds twelve groups and roughly eighty links, which grouping made
   navigable but not fast, and on a phone every single navigation cost a
   drawer tap first.

   Built on the tokens already declared at the top of this file. No new
   colour, no new font, no new dependency.
   ==================================================================== */

/* ---- the Control Center's front door -------------------------------
   The categories, as cards. Configuration is the one part of this product
   somebody arrives at without knowing what they are looking for, so the
   landing names what is inside each category rather than opening one of
   them and hoping it was the right one. */
.cc-landing { display: flex; flex-direction: column; gap: var(--space-6); }
.cc-landing-group { margin: 0; }
.cc-landing-title {
  margin: 0 0 var(--space-3); font-size: var(--font-sm); font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; color: var(--text-secondary);
}
.cc-landing-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(268px, 1fr)); gap: var(--space-3);
}
.cc-card {
  display: flex; align-items: flex-start; gap: var(--space-3); text-align: left;
  padding: var(--space-4); background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); cursor: pointer;
  font-family: inherit; color: var(--text-primary); position: relative;
  transition: border-color .12s ease, box-shadow .12s ease;
}
.cc-card:hover { border-color: var(--border-strong); box-shadow: var(--shadow-pop); }
.cc-card:focus-visible { outline: 2px solid var(--brand-red); outline-offset: 2px; }
.cc-card-icon {
  display: flex; align-items: center; justify-content: center; flex: 0 0 auto;
  width: 38px; height: 38px; border-radius: var(--radius-md);
  background: var(--surface-muted); border: 1px solid var(--border); color: var(--text-secondary);
}
.cc-card:hover .cc-card-icon { color: var(--brand-red); border-color: var(--brand-red-line); background: var(--brand-red-soft); }
.cc-card-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.cc-card-label { font-size: var(--font-md); font-weight: 600; display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.cc-card-blurb { font-size: var(--font-base); color: var(--text-secondary); line-height: 1.45; }
.cc-card-pending {
  position: absolute; top: var(--space-2); right: var(--space-2);
  font-size: var(--font-xs); font-weight: 600; padding: 2px 8px; border-radius: var(--radius-pill);
  background: var(--warning-soft); color: var(--warning-ink); border: 1px solid var(--warning-line);
}

/* ---- the warehouse workflow strip ----------------------------------
   Five stages in the order a stock request passes through them, each
   opening the list it counts. A connector between the marks rather than
   under the labels, so the line reads as the flow and not as an underline
   on the words. */
.wf-flow {
  display: flex; list-style: none; margin: 0 0 var(--space-5); padding: 0;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm); overflow-x: auto;
}
.wf-stage { flex: 1 1 0; min-width: 132px; position: relative; }
/* The connector, drawn from each stage to the next at the height of the
   marks. Not on the last one, which has nothing to point at. */
.wf-stage:not(:last-child)::after {
  content: ''; position: absolute; top: 34px; right: 0; width: 100%;
  border-top: 1px solid var(--border); transform: translateX(50%); z-index: 0;
}
.wf-stage button {
  position: relative; z-index: 1; width: 100%; background: none; border: 0; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; gap: var(--space-1);
  padding: var(--space-4) var(--space-2); font-family: inherit; color: var(--text-secondary);
  border-radius: var(--radius-lg);
}
.wf-stage button:hover:not(:disabled) { background: var(--surface-muted); color: var(--text-primary); }
.wf-stage button:disabled { cursor: default; }
.wf-stage button:focus-visible { outline: 2px solid var(--brand-red); outline-offset: -2px; }
.wf-stage-mark {
  display: flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; border-radius: 50%;
  background: var(--surface); border: 1px solid var(--border-strong); color: var(--text-muted);
}
.wf-stage-label { font-size: var(--font-base); font-weight: 500; text-align: center; }
.wf-stage-count { font-size: var(--font-xl); font-weight: 600; font-variant-numeric: tabular-nums; color: var(--text-primary); }
.wf-stage-count em { font-style: normal; color: var(--text-muted); font-size: var(--font-lg); }
/* A stage with work in it is the one worth looking at. Red on the mark
   only — a filled red stage would read as an error rather than a queue. */
.wf-stage.has-work .wf-stage-mark { border-color: var(--brand-red); color: var(--brand-red); }

@media (max-width: 600px) {
  .wf-stage { min-width: 116px; }
  .wf-stage button { padding: var(--space-3) var(--space-1); }
  .wf-stage-label { font-size: var(--font-sm); }
}

/* ---- global search ------------------------------------------------- */
.topbar-search {
  position: relative; display: flex; align-items: center; gap: var(--space-2);
  flex: 1 1 220px; max-width: 340px; min-width: 0;
}
/* The icon sits inside the field rather than beside it, which is what
   makes the control read as one search box instead of a glyph next to an
   unrelated input. */
.topbar-search-icon {
  position: absolute; left: var(--space-3); color: var(--text-muted);
  pointer-events: none; display: flex;
}
.topbar-search-input {
  width: 100%; min-width: 0; height: 38px;
  padding: 0 var(--space-3) 0 38px; border: 1px solid var(--border); border-radius: var(--radius-btn);
  background: var(--surface-muted); color: var(--text-primary); font-size: var(--font-base); font-family: inherit;
}
.topbar-search-input:focus-visible {
  outline: none; border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-light);
}
.topbar-search-results {
  position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 40;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-pop); overflow: hidden; max-height: 60vh; overflow-y: auto;
}
.topbar-search-hit {
  display: flex; align-items: center; gap: var(--space-2);
  padding: 10px var(--space-3); text-decoration: none; color: var(--ink);
  border-bottom: 1px solid var(--line); min-height: 44px;
}
.topbar-search-hit:last-child { border-bottom: none; }
.topbar-search-hit:hover, .topbar-search-hit:focus-visible { background: var(--brand-light); outline: none; }
.topbar-search-hit-icon { display: flex; color: var(--text-muted); flex: 0 0 auto; }
.topbar-search-hit:hover .topbar-search-hit-icon,
.topbar-search-hit:focus-visible .topbar-search-hit-icon { color: var(--brand-red); }
.topbar-search-hit-label { font-weight: 500; }
/* The group name is context, not a second title — it must never compete
   with the thing being named. */
.topbar-search-hit-group { margin-left: auto; font-size: var(--font-xs); color: var(--muted); }
.topbar-search-empty { padding: var(--space-3); margin: 0; font-size: var(--font-base); }

/* ---- the touch primary bar ----------------------------------------- */
/* Hidden by default and shown only where the sidebar has become a drawer.
   `display: none` also removes it from the accessibility tree, so a desktop
   user never meets a second "Primary" landmark duplicating the sidebar. */
.bottom-nav { display: none; }

@media (max-width: 680px) {
  /* Search is a full row of its own on a phone: sharing the topbar row
     with the bell, Tasks and the scope chip left it about 80px wide. At
     iPad width the topbar row is wide enough to hold it, so this stays a
     phone rule while the bar below does not. */
  .topbar-search { flex: 1 1 100%; max-width: none; order: 10; }
}

/* Revision 13 — the bar's breakpoint is the DRAWER's breakpoint.

   The sidebar goes off-canvas at 860px. The bar that replaces it appeared
   at 680px. Between the two — 681px to 860px, which is iPad mini (744),
   iPad 10.9"/Air portrait (820), iPad Pro 11" portrait (834) and the old
   768 — there was NO persistent navigation of any kind: the sidebar had
   already become a drawer and the bar had not yet arrived, so every
   navigation on an iPad in portrait cost a hamburger tap first.

   That is precisely the tax Revision 11 identified and removed, removed on
   one of the three device classes the requirement names. So the rule is
   now one invariant rather than two independent numbers: WHEREVER THE
   SIDEBAR IS A DRAWER, A PERSISTENT PRIMARY BAR EXISTS. Both widths are
   asserted against each other in rev13-responsive-shell.test.js, so
   moving one without the other fails rather than silently reopening the
   gap. */
@media (max-width: 860px) {
  .bottom-nav {
    display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 35;
    background: var(--card); border-top: 1px solid var(--line);
    /* Clears the home indicator on a notched phone. Falls back to 0 where
       the environment variable does not exist. */
    padding-bottom: env(safe-area-inset-bottom, 0px);
    box-shadow: 0 -2px 12px rgba(0,0,0,.06);
  }
  .bottom-nav-link {
    flex: 1 1 0; min-width: 0; display: flex; flex-direction: column;
    align-items: center; justify-content: center; gap: 2px;
    /* 44px is the smallest target a thumb reliably hits; 52 leaves room
       for the label underneath the icon without crowding it. */
    min-height: 52px; padding: 6px 2px;
    background: none; border: none; text-decoration: none;
    color: var(--muted); font-family: inherit; font-size: var(--font-xs); cursor: pointer;
  }
  .bottom-nav-link.active { color: var(--brand); }
  .bottom-nav-link:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
  .bottom-nav-icon { display: flex; }
  /* A label that wraps to two lines makes the bar jump height between
     pages; one clipped line is the lesser evil, and the icon carries the
     recognition anyway. */
  .bottom-nav-label { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

  /* Content must not end underneath the bar — a sticky primary action at
     the bottom of a form is exactly what the bar would cover. */
  body.has-bottom-nav .main { padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px)); }
}

/* A tablet is not a wide phone. Five targets stretched across 820px are
   164px each — a row of buttons so far apart that the bar reads as a
   toolbar rather than as navigation, and a thumb reaching the far end of
   it has crossed the whole screen. Centred and held to a thumb's span
   instead; the targets keep their full height and every one of them stays
   over 44px wide. */
@media (min-width: 681px) and (max-width: 860px) {
  .bottom-nav { justify-content: center; }
  .bottom-nav-link { flex: 0 1 132px; }
}

/* Revision 11 — a policy note: what the server will refuse, said in place,
   before anybody types. Deliberately a rule and a line of text rather than
   a card or a banner — it is a fact about the field above it, not a second
   surface competing with the form. */
.entry-policy-note {
  margin: var(--space-3) 0 0; padding: var(--space-2) var(--space-3);
  border-left: 3px solid var(--line); font-size: var(--font-sm);
  line-height: 1.45; color: var(--muted);
}
.entry-policy-note.is-blocked { border-left-color: var(--err); color: var(--ink); }

/* Revision 11 (A5) — the sales trend, and the caveat that has to travel
   with a headline figure. Plain inline SVG on the design system's own
   tokens: no charting library, and no second palette. */
.ds-trend { width: 100%; height: auto; display: block; }
.ds-trend-axis { stroke: var(--line); stroke-width: 1; }
.ds-trend-line { stroke: var(--brand); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.ds-note {
  margin: var(--space-3) 0 0; padding: var(--space-3);
  border-left: 3px solid var(--brand); background: var(--brand-light);
  font-size: var(--font-base); line-height: 1.5; color: var(--ink);
}

/* ---- Revision 17: the rest of the shared controls, on the devices the
   design lock §5 and this revision's till and attendance workflows
   actually run on.

   It sits at the FOOT of this stylesheet on purpose. Every selector below
   is a class, so specificity ties with the rule that defined the control
   in the first place, and a tie is broken by document order. .cc-scope-chip
   declares min-height: 34px around line 1900; a floor written above that
   line would lose to it, silently, and read as though it had worked.

   The sidebar block above was written for the SIDEBAR and stops there. Everything
   else a finger presses in this design system — every button variant, both
   segmented strips, the page tab strip, the autocomplete result a cashier
   taps to pick a product, the × that removes a line from a cart, the
   drawer's close button, the Control Center rail, its search results and,
   worst of all, the native checkbox that is the only control for a boolean
   setting or a permission grant — declared no height at all. Measured off
   this stylesheet's own numbers before the change: .btn-small ~28px,
   .ds-segmented button and .seg-btn ~30px, .ac-item ~34px,
   .recon-photo-btn ~34px, .tabs button ~36px, .cc-scope-chip 34px,
   .ds-drawer-close 22px, .cc-switch input 20px, .cc-grant input 18px. The
   text inputs beside them have been 44px since the token existed.

   The query is width OR coarse pointer, not width alone. A 12.9" iPad in
   landscape and a store till terminal are 1024-1366px wide and entirely
   touch — and the till is precisely the device this revision's shift
   open/close work is for, so a width-only rule would have missed it.

   Where a control is a <label> wrapping a small native input (.cc-switch,
   .cc-grant), the floor goes on the LABEL: it is the hit area, and a
   44px-square native checkbox looks like a mistake. The input itself
   still comes up to 24px, WCAG 2.5.8's minimum for a non-inline target,
   so it is hittable on its own too.

   Deliberately NOT here, in two groups:

   * .link-btn, .cc-crumbs button, .photo-chip button — padding:0 controls
     that sit inside a sentence. A 44px box around a word in running text
     breaks the line it lives in, which is the same reason WCAG 2.5.8
     excepts inline targets.
   * .picker-tile, .oo-card, .cust-card, .cc-card — cards, not controls.
     Their padding alone is 24-40px before a line of content, so the floor
     would be a no-op dressed up as a fix.

   Still open, and NOT fixed here: the clickable table row
   (.clickable-row, tr.ds-row-clickable, th.sortable-th) is a
   --table-row-height row, i.e. 38px, and raising that token changes the
   density of every table in the product — a design-lock decision rather
   than a stylesheet one. Under 760px the opted-in tables are already
   .ds-stack cards, so the exposure is the 760-860px band and the wide
   touch terminal. Recorded rather than quietly widened. */
@media (max-width: 860px), (pointer: coarse) {
  /* The one-off sized variants that carry class="btn" — .report-actions
     .btn, .ac-contact-block .btn, .day-status .btn, .items-toolbar .btn —
     are not listed: the base .btn declaration now covers all of them.
     .btn-small and .btn-ghost are, because they are the two that override
     the floor downwards to stay inside a table row. */
  .btn-small,
  .btn-ghost,
  .iconbtn,
  .seg-btn,
  .ds-segmented button,
  .tabs button, .tabs a,
  .ac-item,
  .recon-photo-btn,
  .ds-section-summary,
  .ds-drawer-close,
  .items-table .remove-line,
  .payment-row .rm,
  .cc-rail-item,
  .cc-results button,
  .cc-scope-chip,
  .cc-switch,
  .cc-grant,
  /* A row that opens a detail IS a tap target, and a dense ERP row on a
     phone is about 32px. On a stacked card layout the row is a block box
     and this applies directly; on a wide touch device (the till terminal)
     it is a table-row, where a minimum height is still honoured. */
  .ds-table tbody tr.is-clickable,
  .wf-stage button { min-height: var(--control-height); }

  /* Glyph-only controls — ☰, the bell, ×, the remove cross. A tall box is
     no use to them without a wide one: their width comes from a single
     character. Under 600px the topbar hides .menu-word/.topbar-btn-word
     too, so .iconbtn loses its width along with its label. */
  .iconbtn,
  .ds-drawer-close,
  .items-table .remove-line,
  .payment-row .rm { min-width: var(--control-height); justify-content: center; }
  .ds-drawer-close, .items-table .remove-line, .payment-row .rm { display: inline-flex; align-items: center; }

  /* A <button> centres its own text vertically; an <a> does not. .tabs is
     a flex row, so without this the tab label would sit at the top of its
     new 44px box with the active underline 44px below it. */
  .tabs button, .tabs a { display: inline-flex; align-items: center; }

  /* The rail's collapse handle is a deliberate 24px disc half-hanging off
     the sidebar edge; blowing it up to 44 would put a red button in the
     middle of the workspace. It gets the area without the appearance —
     10px on each side of 24 is exactly the floor. It is display:none
     under 861px, so this is for the wide touch device: a till terminal or
     an iPad Pro in landscape, where the rail is on screen and the handle
     is the only way out of the collapsed state. */
  .side-collapse-toggle::after { content: ''; position: absolute; inset: -10px; border-radius: 50%; }

  /* The label is the target; the box still has to be pressable on its own
     for anyone aiming at the tick rather than the words. */
  .cc-switch input, .cc-grant input { width: 24px; height: 24px; }

  /* .cc-switch is inline-flex, so a min-height with baseline-aligned
     content leaves the extra space under the text rather than around it. */
  .cc-switch { align-items: center; }
}

/* Paper has no navigation. */
@media print {
  .bottom-nav, .topbar-search { display: none !important; }
}

/* ----------------------------------------------------------------------
   People / Attendance (Revision 17)

   Three additions, and deliberately only three: everything else on the
   seven People screens is built from the design system that already
   exists (.ds-page-head, .ds-kpi, .ds-table, .ds-badge, .ds-filterbar,
   .ds-state). No new colour is introduced — every rule below resolves to
   a token defined in the block at the top of this file, so the palette
   stays the locked one.
   ---------------------------------------------------------------------- */

/* Within-area navigation. The People area is one workspace with seven
   screens and a manager moves between "who is in today" and "what am I
   approving" constantly; the sidebar can do it, but not without leaving
   the page you are reading. Scrolls horizontally on a phone rather than
   wrapping into a block that pushes the content off the first screen. */
.ds-subnav {
  display: flex; gap: 4px; overflow-x: auto; -webkit-overflow-scrolling: touch;
  border-bottom: 1px solid var(--line); margin: 0 0 16px; padding-bottom: 0;
  scrollbar-width: none;
}
.ds-subnav::-webkit-scrollbar { display: none; }
.ds-subnav-item {
  flex: 0 0 auto; padding: 10px 14px; font-size: var(--font-md); font-weight: 600;
  color: var(--muted); text-decoration: none; white-space: nowrap;
  border-bottom: 2px solid transparent; border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}
.ds-subnav-item:hover { color: var(--ink); background: var(--surface-muted); }
.ds-subnav-item.is-current { color: var(--brand); border-bottom-color: var(--brand); }
.ds-subnav-item:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }

/* A figure that could not be computed, inline in a table cell. Italic and
   muted so it reads as "no answer" rather than as a value — the whole
   point of the service returning null instead of 0 is lost if the screen
   draws the two the same way. */
.ds-unknown-inline { color: var(--muted); font-style: italic; }

/* An error that must not scroll away while somebody is still reading it,
   for the failures a toast is the wrong container for. */
.ds-inline-error {
  margin: 10px 0; padding: 10px 12px; border-radius: var(--radius-md);
  background: var(--danger-soft); color: var(--danger-ink);
  border: 1px solid var(--danger-line); font-size: var(--font-md);
}

/* The branch kiosk. A full-bleed screen showing one rotating code, meant
   to be left running on a tablet or a wall display by the door — so the
   code is as large as the viewport allows and everything else is small. */
.att-kiosk { display: flex; flex-direction: column; align-items: center; justify-content: center;
  min-height: 100vh; min-height: 100dvh; gap: 18px; padding: 24px; text-align: center; }
.att-kiosk-code { width: min(72vmin, 560px); aspect-ratio: 1; background: var(--surface);
  border-radius: var(--radius-lg); padding: 12px; box-shadow: var(--shadow-md); }
.att-kiosk-code svg { width: 100%; height: 100%; display: block; }
.att-kiosk-branch { font-size: var(--font-2xl); font-weight: 700; letter-spacing: -0.01em; }
.att-kiosk-note { color: var(--muted); max-width: 46ch; font-size: var(--font-md); }
.att-kiosk-countdown { font-variant-numeric: tabular-nums; color: var(--muted); font-size: var(--font-md); }
.att-kiosk-bar { width: min(72vmin, 560px); height: 4px; border-radius: var(--radius-pill);
  background: var(--surface-muted); overflow: hidden; }
.att-kiosk-bar span { display: block; height: 100%; background: var(--brand); transition: width .95s linear; }
/* The setup state. A screen with no location yet is a form, not a code, so
   it drops the full-viewport centring that exists to make a QR readable
   from across a shop floor — a manager filling this in is standing at the
   tablet, and a form pinned to the middle of a tall screen is worse for
   them than one that starts at the top. */
.att-kiosk-setup { min-height: 0; justify-content: flex-start; padding-top: 40px; }

/* The employee's own clock-in screen, which is a phone screen first. One
   decision per view and a target big enough to hit with a thumb while
   holding a bag. */
.att-punch { display: grid; gap: 12px; max-width: 460px; margin: 0 auto; }
.att-punch .btn { width: 100%; padding: 18px; font-size: var(--font-lg); }
.att-punch-state { text-align: center; padding: 18px; border-radius: var(--radius-md);
  background: var(--surface-muted); border: 1px solid var(--line); }
.att-punch-state .att-punch-status { font-size: var(--font-xl); font-weight: 700; }
.att-punch-state .att-punch-since { color: var(--muted); font-size: var(--font-md); margin-top: 4px; }

/* The correction form's punch list, and the superseded-punch row. A
   superseded punch is DIMMED rather than hidden: it is the evidence that
   the original survived the correction, and hiding it would defeat the
   append-only design it is there to demonstrate. */
.ds-checklist { display: grid; gap: 6px; margin-top: 4px; }
.ds-check { display: flex; align-items: center; gap: 8px; font-size: var(--font-md); font-weight: 500; }
.ds-check input { width: auto; margin: 0; }
.ds-proposed-row { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; flex-wrap: wrap; }
.ds-proposed-row .ds-filter-control { flex: 1 1 130px; min-width: 120px; }
tr.is-superseded td { opacity: .55; text-decoration: line-through; text-decoration-thickness: 1px; }
tr.is-superseded td:last-child { text-decoration: none; }

/* The payroll-summary blocker list. Reasons a period is not ready to pay,
   shown next to the person they belong to rather than as a single count at
   the top — "three people are blocked" is not something anybody can act
   on, and "Asif has a clock-in from the 4th that was never closed" is. */
.ds-blockers { margin: 6px 0 0; padding-left: 18px; font-size: var(--font-md); }
.ds-blockers li { margin-bottom: 4px; }
.ds-blockers strong { display: block; }
.ds-blockers .ds-unknown-inline { display: block; margin-top: 2px; }

/* ======================================================================
   Super Admin — View as Role (issue #1 comment 5458064775)

   Two surfaces, styled from opposite ends of the palette on purpose.

   The TOPBAR BUTTON is an ordinary .iconbtn until the mode is live, and
   then it takes the brand tint — it is the one control in the bar whose
   state changes the meaning of the whole page, and it should read as
   engaged rather than as selected.

   The STRIP is deliberately the loudest thing in the shell. Everything
   below it is a simulation, and the owner's failure mode is forgetting
   that and reading a Cashier's figures as the company's. It is charcoal
   rather than red: red is this design system's ONE accent and is already
   doing brand duty, and a full-width red band would train the eye to
   ignore it within a day. Charcoal borrows the sidebar's authority, marks
   the band as chrome rather than content, and lets the single red action
   button inside it be the thing that catches the eye.

   Sticky under the topbar, at the same z-index band, because a strip you
   can scroll past is a strip you will forget. --topbar-h already accounts
   for its height (nav.js measures both boxes into it), so a screen's own
   sticky header still lands below it.
   ====================================================================== */
.viewas-btn.is-active {
  background: var(--brand-red-soft); border-color: var(--brand-red-line); color: var(--brand-dark);
  font-weight: 600;
}
.viewas-btn.is-active:hover { background: var(--brand-red-line); }

.viewas-strip {
  position: sticky; top: var(--topbar-h, 56px); z-index: 19;
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--sidebar-bg); color: var(--sidebar-text-strong);
  border-bottom: 2px solid var(--brand-red);
}
.viewas-strip-mark { display: flex; flex-shrink: 0; color: var(--brand-red); }
.viewas-strip-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
/* NOT the display face. docs/UI-DESIGN-LOCK.md reserves Oswald for the
   wordmark and for printed document kinds; rev14-design-lock test 14
   fails on an interface heading that reaches for it. Weight and case
   carry the emphasis here instead, which is what the rest of the shell
   does. */
.viewas-strip-text strong {
  font-size: var(--font-md); font-weight: 700; letter-spacing: .02em;
}
/* The sub-line is the part that says whose name is on the records. It is
   dimmed but never hidden: at the narrowest widths the strip stacks rather
   than dropping it, because "you are still signed in as <you>" is the
   sentence that makes this a preview and not impersonation. */
.viewas-strip-sub { font-size: var(--font-sm); color: var(--sidebar-text); line-height: 1.35; }
/* Both of these are pressed on a till terminal and a phone, so the floor
   is unconditional rather than gated on a narrow viewport — a store
   terminal is 1024-1366px and entirely touch (see rev17-touch-targets
   test 4). */
.viewas-strip-actions { display: flex; gap: var(--space-2); flex-shrink: 0; }
.viewas-strip-actions button { min-height: var(--control-height); }
/* The one place in this file that needs white at partial alpha: a border
   on charcoal that has to read as a border and not as a second surface.
   `--sidebar-text-strong` is pure white, so this is that same token's
   colour taken down in opacity rather than restated as a new value — a
   literal hex here is how a design system grows a second palette, and
   rev11-app-shell test 23 fails on one deliberately (including one written
   inside a comment, which is why this sentence spells the colour out in
   words). */
.viewas-strip .btn-ghost {
  background: transparent; color: var(--sidebar-text-strong);
  border-color: rgba(255, 255, 255, .35);
}
.viewas-strip .btn-ghost:hover {
  border-color: var(--sidebar-text-strong); background: rgba(255, 255, 255, .08);
}

@media (max-width: 720px) {
  .viewas-strip { flex-wrap: wrap; align-items: flex-start; }
  .viewas-strip-text { flex-basis: calc(100% - 40px); }
  /* The actions go full width and side by side rather than shrinking:
     "Return to Super Admin" is the one control that must never be a
     44px-wide sliver on a phone. */
  .viewas-strip-actions { flex-basis: 100%; }
  .viewas-strip-actions button { flex: 1; justify-content: center; }
}

/* ---- the chooser ---- */
.viewas-dialog {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center; padding: var(--space-4);
  background: rgba(17, 18, 20, .55);
}
.viewas-dialog-card {
  background: var(--surface); border-radius: var(--radius-lg); box-shadow: var(--shadow-pop);
  padding: var(--space-5); width: min(560px, 100%);
  /* The role list can be long on a mature configuration and the card must
     never grow taller than the viewport — the confirm button ending up
     below the fold is how a dialog becomes a trap on a laptop. */
  max-height: min(80vh, 720px); display: flex; flex-direction: column; gap: var(--space-3);
}
.viewas-dialog-card h2 { margin: 0; font-size: var(--font-lg); font-weight: 700; }
.viewas-dialog-lede { margin: 0; font-size: var(--font-md); color: var(--text-secondary); line-height: 1.45; }
.viewas-role-list {
  display: grid; gap: var(--space-2); overflow-y: auto; min-height: 0;
  padding: 2px; /* so a focus ring on the first row is not clipped by the scroller */
}
.viewas-role {
  display: flex; flex-direction: column; gap: 2px; text-align: left;
  padding: var(--space-3); border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface); font-family: inherit; cursor: pointer;
  min-height: var(--control-height); /* the shared tap-target floor, not a restated 44 */
}
.viewas-role:hover { border-color: var(--brand-red-line); background: var(--brand-red-soft); }
.viewas-role.selected { border-color: var(--brand-red); background: var(--brand-red-soft); }
.viewas-role-name { font-weight: 600; font-size: var(--font-md); color: var(--text-primary); }
.viewas-role-meta { font-size: var(--font-sm); color: var(--text-secondary); }
/* Not an error — a fact about the role's configuration. A role with no
   permissions previews as an almost-empty portal, and the owner should be
   told that is the role rather than a broken preview. */
.viewas-role-warn { font-size: var(--font-sm); color: var(--warning); }
.viewas-scope { display: flex; flex-direction: column; gap: var(--space-2); }
.viewas-scope label { font-size: var(--font-md); font-weight: 600; }
.viewas-scope-note { margin: 0; font-size: var(--font-sm); color: var(--text-secondary); }
.viewas-dialog-err {
  margin: 0; padding: var(--space-2) var(--space-3); border-radius: var(--radius-md);
  background: var(--warning-soft); color: var(--warning); font-size: var(--font-md);
}
.viewas-dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); }

/* ---- Control Center: the technical half, folded away -----------------
   Comment 5458123243 asks for implementation detail to stay available to
   Super Admin/technical users and out of the way of everybody else. A
   <details> is the right control for that and needs almost nothing: the
   default marker is kept (it is the affordance), the summary gets a real
   tap target, and the body is set at the same size as the note it
   replaces so opening it does not reflow the row around it. */
.cc-tech { margin-top: var(--space-2); }
.cc-tech > summary {
  cursor: pointer; font-size: var(--font-sm); color: var(--text-secondary);
  min-height: var(--control-height); display: flex; align-items: center; gap: var(--space-1);
  width: fit-content;
}
.cc-tech > summary:hover { color: var(--brand-red); }
.cc-tech-body {
  font-size: var(--font-sm); color: var(--text-secondary); line-height: 1.5;
  padding: var(--space-2) var(--space-3); margin-top: var(--space-1);
  border-left: 2px solid var(--border); background: var(--bg);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
}
/* "Not active yet" is a statement about the setting, not a warning about
   the system — amber, not red, and never an alert icon. */
.cc-setting-note.is-inactive {
  color: var(--warning); background: var(--warning-soft);
  padding: var(--space-1) var(--space-2); border-radius: var(--radius-md);
  width: fit-content; font-size: var(--font-sm);
}

/* ---- Admin: which surface is the system of record --------------------
   A statement, not a warning. Using the Admin screen is not an error
   today — the note says where the governed copy of the same capability
   lives so somebody making a policy change goes to the place that records
   it. Neutral surface and a plain link; an amber banner here would read as
   "this screen is broken", which it is not. */
.admin-canonical {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3);
  padding: var(--space-3) var(--space-4); margin-bottom: var(--space-4);
  background: var(--surface); border: 1px solid var(--border);
  border-left: 3px solid var(--brand-red); border-radius: var(--radius-md);
}
.admin-canonical-text { flex: 1; min-width: 220px; font-size: var(--font-md); color: var(--text-secondary); line-height: 1.45; }
.admin-canonical-link {
  display: inline-flex; align-items: center; min-height: var(--control-height);
  padding: 0 var(--space-3); border: 1px solid var(--border); border-radius: var(--radius-md);
  color: var(--text-primary); text-decoration: none; font-size: var(--font-md); font-weight: 600;
  background: var(--bg);
}
.admin-canonical-link:hover { border-color: var(--brand-red); color: var(--brand-red); }

/* ======================================================================
   dsDataTable — the shared operational table (comment 5458220609)

   One stylesheet block for every screen that adopts the primitive, which
   is the point: the owner asked for the many hand-built tables to be
   consolidated "into shared reusable UI primitives ... table shell,
   toolbar/filter bar, status badge, pagination, field group, form
   actions, empty/error/loading state".
   ====================================================================== */
.ds-dt { display: flex; flex-direction: column; gap: var(--space-3); }

.ds-dt-toolbar {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--space-3);
}
.ds-dt-search { flex: 1 1 220px; min-width: 0; }
.ds-dt-filter { display: flex; flex-direction: column; gap: 4px; min-width: 150px; }
.ds-dt-actions { margin-left: auto; display: flex; gap: var(--space-2); flex-wrap: wrap; }

/* The count line. Small, but it is the answer to "did my filter work?" and
   it is `aria-live` so the answer is also spoken. */
.ds-dt-count { margin: 0; font-size: var(--font-sm); color: var(--text-secondary); }

/* Sticky header. The wrapper is the scroll container, so `top: 0` here is
   the top of the scroller and NOT the page — a header pinned to the page
   would slide under the topbar and the role-preview strip. */
.ds-dt-sticky { max-height: 70vh; overflow-y: auto; }
.ds-dt-sticky thead th {
  position: sticky; top: 0; z-index: 2;
  background: var(--card); box-shadow: inset 0 -1px 0 var(--line);
}

/* A sortable heading is a button, because it does something. Full-height
   so the whole cell is the target rather than the six characters of the
   label. */
.ds-sort {
  display: inline-flex; align-items: center; gap: 6px; width: 100%;
  background: none; border: 0; padding: 0; margin: 0; font: inherit; color: inherit;
  cursor: pointer; text-align: inherit; min-height: var(--control-height);
}
.ds-sort:hover { color: var(--brand-red); }
.ds-sort.is-active { color: var(--brand-red); font-weight: 700; }
.ds-sort-mark { font-size: var(--font-sm); opacity: .65; }
.ds-sort.is-active .ds-sort-mark { opacity: 1; }
th.num .ds-sort { justify-content: flex-end; }

.ds-table tbody tr.is-clickable { cursor: pointer; }
.ds-table tbody tr.is-clickable:hover { background: var(--brand-red-soft); }
.ds-table tbody tr.is-clickable:focus-within { outline: 2px solid var(--brand-red); outline-offset: -2px; }

/* ---- selection + bulk actions (comment 5458272562) ---- */
.ds-select-cell { width: 40px; text-align: center; }
.ds-select-cell input { width: 18px; height: 18px; }
.ds-dt-bulk {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3);
  padding: var(--space-2) var(--space-3);
  background: var(--brand-red-soft); border: 1px solid var(--brand-red-line);
  border-radius: var(--radius-md);
  /* Sticky so the actions stay reachable while scrolling a long selection
     — selecting forty products and then having to scroll back to the top
     to act on them is the failure this avoids. */
  position: sticky; top: var(--topbar-h, 56px); z-index: 3;
}
.ds-dt-bulk-count { font-size: var(--font-md); }
.ds-dt-bulk-actions { margin-left: auto; display: flex; gap: var(--space-2); flex-wrap: wrap; }

/* ---- pagination ---- */
.ds-dt-pager {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--space-3);
}
.ds-dt-pagesize { display: flex; align-items: center; gap: var(--space-2); font-size: var(--font-sm); color: var(--text-secondary); }
.ds-dt-pagesize select { width: auto; }
.ds-dt-pagebtns { display: flex; align-items: center; gap: var(--space-3); }
.ds-dt-pageno { font-size: var(--font-sm); color: var(--text-secondary); white-space: nowrap; }

@media (max-width: 680px) {
  .ds-dt-toolbar { gap: var(--space-2); }
  .ds-dt-search, .ds-dt-filter { flex: 1 1 100%; min-width: 0; }
  .ds-dt-actions { margin-left: 0; flex: 1 1 100%; }
  /* The sticky bulk bar becomes a bottom sheet on a phone: at the top it
     would sit under the drawer's own chrome, and the thumb is at the
     bottom. */
  .ds-dt-bulk { position: sticky; top: auto; bottom: 0; }
  .ds-dt-bulk-actions { margin-left: 0; flex: 1 1 100%; }
  .ds-dt-bulk-actions .btn { flex: 1; }
  .ds-dt-pager { justify-content: center; }
  /* The sticky column header has nothing to stick to once the table has
     reflowed into cards, and `position: sticky` on a stacked cell pins a
     stray label mid-scroll. */
  .ds-dt-sticky { max-height: none; overflow-y: visible; }
  .ds-dt-sticky thead th { position: static; }
}

/* ======================================================================
   Product Management — Channels & Visibility (comment 5458304988)

   One row per channel, each stating where it stands before offering any
   control. The channels ProShop does not control per product carry a
   sentence saying so instead of a switch — the owner's rule from comment
   5458123243 ("never make an unimplemented setting look active") applies
   to a channel toggle at least as much as to a settings row.
   ====================================================================== */
.pm-channel {
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--border);
  display: flex; flex-direction: column; gap: var(--space-2);
}
.pm-channel:last-of-type { border-bottom: 0; }
.pm-channel-main { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.pm-channel-name { font-weight: 600; font-size: var(--font-md); }
.pm-channel-note { margin: 0; font-size: var(--font-sm); color: var(--text-secondary); line-height: 1.45; }

.pm-branches { display: flex; flex-direction: column; gap: var(--space-1); }
.pm-branch, .pm-switch {
  display: flex; align-items: center; gap: var(--space-2);
  min-height: var(--control-height); cursor: pointer; font-size: var(--font-md);
}
.pm-branch input, .pm-switch input { width: 18px; height: 18px; }
.pm-branch-state { margin-left: auto; font-size: var(--font-sm); color: var(--text-secondary); }

.pm-sub { font-size: var(--font-md); font-weight: 700; margin: var(--space-4) 0 var(--space-2); }
.pm-detail-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; }
.pm-price { margin: 0; font-size: var(--font-lg); font-weight: 700; }

/* The readiness list. Done and missing are distinguished by an icon and by
   the words "still needed", not by colour alone. */
.pm-ready { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-1); }
.pm-ready li { display: flex; align-items: center; gap: var(--space-2); font-size: var(--font-md); }
.pm-ready li.is-done { color: var(--text-secondary); }
.pm-ready li.is-done span { color: var(--ok); }
.pm-ready li.is-missing span { color: var(--warning); }

/* The content-agent block says what is NOT built. Neutral surface, because
   it is a statement of scope and not a fault. */
.pm-agent {
  margin-top: var(--space-3); padding: var(--space-3);
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-md);
}
.pm-agent h4 { margin: 0 0 var(--space-2); font-size: var(--font-md); font-weight: 700; }
.pm-agent p { margin: 0 0 var(--space-2); font-size: var(--font-md); line-height: 1.45; }
.pm-agent p:last-child { margin-bottom: 0; }

.pm-bulk-confirm { margin-top: var(--space-4); display: flex; flex-direction: column; gap: var(--space-2); }
.pm-bulk-summary { margin: 0; font-size: var(--font-md); font-weight: 600; }
/* Every product that did not change, named. A bulk result that says "3 of
   40 failed" and stops is a result nobody can act on. */
.pm-fail { margin: 0; padding-left: var(--space-4); font-size: var(--font-md); color: var(--err); }
/* Revision 17 — the content agent's drafted listing, shown as the payload
   it actually is. A definition list rather than a form: this is what WOULD
   be published, and editing it belongs on the product record, so a reader
   should not be invited to type into it here. */
.pm-payload { margin: var(--space-3) 0; display: grid; grid-template-columns: max-content 1fr; gap: var(--space-1) var(--space-3); }
.pm-payload dt { font-size: var(--font-sm); color: var(--text-secondary); font-weight: 600; }
.pm-payload dd { margin: 0; font-size: var(--font-md); overflow-wrap: anywhere; }
@media (max-width: 520px) { .pm-payload { grid-template-columns: 1fr; gap: 0 0; } .pm-payload dd { margin-bottom: var(--space-2); } }
.pm-agent-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-3); }
.pm-agent-actions .btn { width: auto; }
.pm-fail li { margin-bottom: var(--space-1); }

/* ======================================================================
   The shared entry form — FORM_UX (comment 5458223754)

   The owner reviewed Create Transfer and listed what was missing: a
   summary before submission, a clear action hierarchy, a sticky action
   bar, and validation that appears where the eye starts rather than as a
   toast that fades. These styles serve the SHARED form, so all fifteen
   transaction types get them at once.
   ====================================================================== */

/* ---- multi-field validation summary ---- */
.entry-errors {
  padding: var(--space-3) var(--space-4); margin-bottom: var(--space-4);
  background: var(--danger-soft); color: var(--ink);
  border: 1px solid var(--err); border-left-width: 3px;
  border-radius: var(--radius-md);
}
.entry-errors strong { display: block; margin-bottom: var(--space-2); font-size: var(--font-md); }
.entry-errors ul { margin: 0; padding-left: var(--space-4); font-size: var(--font-md); line-height: 1.5; }
/* The summary is focusable so validation can move focus to it. It must not
   then draw a focus ring around a whole block for a mouse user. */
#entryErrors:focus { outline: none; }
#entryErrors:focus-visible { outline: 2px solid var(--brand-red); outline-offset: 2px; }

/* ---- what is about to be saved ---- */
.entry-summary {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: var(--space-4);
  margin-bottom: var(--space-4);
  display: flex; flex-direction: column; gap: var(--space-3);
}
.entry-summary-title { margin: 0; font-size: var(--font-md); font-weight: 700; }
.entry-sum-stats { display: flex; flex-wrap: wrap; gap: var(--space-4); }
.entry-sum-stat { font-size: var(--font-md); color: var(--text-secondary); }
.entry-sum-stat strong { font-size: var(--font-lg); color: var(--text-primary); }
.entry-sum-context { display: flex; flex-wrap: wrap; gap: var(--space-4); margin: 0; }
.entry-sum-context div { display: flex; flex-direction: column; gap: 2px; }
.entry-sum-context dt { font-size: var(--font-sm); color: var(--text-secondary); }
.entry-sum-context dd { margin: 0; font-size: var(--font-md); font-weight: 600; }
.entry-sum-warn { margin: 0; font-size: var(--font-md); color: var(--warning); }

/* ---- the action bar ----
   Sticky at the bottom of the viewport rather than at the end of the
   document: on a fifteen-line transfer the submit button sat below the
   fold, and reaching it meant scrolling past every line you had just
   checked. The bar is inside .container so it inherits the page's own
   width, and it carries a top border rather than a shadow so it reads as
   the end of the form and not as a floating toolbar. */
.entry-actionbar {
  position: sticky; bottom: 0; z-index: 10;
  margin: var(--space-4) calc(var(--space-4) * -1) 0;
  padding: var(--space-3) var(--space-4);
  background: var(--card); border-top: 1px solid var(--line);
}
.entry-actionbar-inner {
  display: flex; align-items: center; gap: var(--space-3);
  max-width: 1180px; margin: 0 auto;
}
/* Cancel is a link, because it navigates. Primary sits at the end, where
   the reading order puts the last thing you do. */
.entry-actionbar .btn { width: auto; }
.entry-actionbar #submitBtn { margin-left: auto; min-width: 160px; }
.entry-actionbar-note { font-size: var(--font-sm); color: var(--text-secondary); }

@media (max-width: 680px) {
  .entry-actionbar-inner { gap: var(--space-2); }
  /* The note is the first thing to go: the two buttons must each stay a
     real target, and "3 lines ready" is already stated in the summary
     panel directly above. */
  .entry-actionbar-note { display: none; }
  .entry-actionbar #submitBtn { flex: 1; min-width: 0; }
  .entry-sum-stats, .entry-sum-context { gap: var(--space-3); }
}

/* A page with a sticky bottom bar AND the phone's bottom navigation would
   stack two bars on top of each other, with the nav (z-index 35) drawn
   over the action bar (z-index 10) — so the primary action of the form
   would sit UNDERNEATH the navigation and could not be pressed at all.
   The 56px and the safe-area inset are the bar's own two measurements,
   restated from the .bottom-nav rule above rather than guessed: a phone
   with a home indicator makes the nav taller than 56px, and the action
   bar has to clear the whole of it. Scoped to the same breakpoint,
   because .bottom-nav is display:none above it and there is nothing to
   clear. */
@media (max-width: 680px) {
  body.has-bottom-nav .entry-actionbar { bottom: calc(56px + env(safe-area-inset-bottom, 0px)); }
}

/* ---- Control Center: role experience (comment 5458080845) -------------
   role -> area -> screen -> action -> scope, in one drawer. The navigation
   half is drawn from navVisibleSections(), the same function the sidebar
   uses, so this view and the portal cannot disagree. */
.cc-role-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; justify-content: flex-end; }
.cc-rx-head { font-size: var(--font-md); font-weight: 700; margin: var(--space-4) 0 var(--space-2); }
.cc-rx-nav { margin: 0; padding-left: var(--space-4); font-size: var(--font-md); line-height: 1.6; }
.cc-rx-nav > li { margin-bottom: var(--space-2); }
.cc-rx-nav ul { margin: 2px 0 0; padding-left: var(--space-4); color: var(--text-secondary); }
.cc-rx-actions {
  margin: 0; padding: 0; list-style: none;
  display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-2);
}
.cc-rx-actions code {
  font-size: var(--font-sm); background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius-md); padding: 2px 6px;
}
.cc-rx-foot {
  margin-top: var(--space-4); padding-top: var(--space-3); border-top: 1px solid var(--border);
  display: flex; flex-direction: column; gap: var(--space-2);
}
.cc-rx-foot p { margin: 0; font-size: var(--font-sm); }
