:root {
  /* Color - accent darkened from the original #3378dd: at that value,
     white-on-accent (primary buttons) measured ~4.3:1 contrast, just under
     the 4.5:1 WCAG AA threshold for normal-size text. This shade (the old
     hover color) measures ~5.9:1. */
  --color-bg: #f5f6f7;
  --color-surface: #ffffff;
  --color-surface-sunken: #fafafa;
  --color-border: rgba(0,0,0,0.08);
  --color-border-strong: rgba(0,0,0,0.16);
  --color-text-primary: #1a1a1e;
  --color-text-secondary: #63636a;
  /* Was #93939a (3.05:1 on white - fails WCAG AA's 4.5:1 for normal text);
     darkened to actually pass, verified with a real contrast calculation
     rather than eyeballed. */
  --color-text-tertiary: #6e6e74;
  /* --color-accent (blue, #2a63b8) and --color-info (blue, #185fa5) both
     REMOVED (2026-07-21, palette-fix pass) - a firm, explicit brand
     constraint: AWT's palette is gold/amber, white, and black/near-black
     ONLY, with red reserved exclusively for destructive/danger contexts.
     Every former consumer of either token was individually re-mapped to
     --color-brand-amber/-soft, --color-text-primary, or --color-neutral/
     -soft depending on its own context (dark chrome vs. light canvas,
     active/highlighted vs. plain informational) - see
     public/palette-fix-report.txt for the full before/after list. Do not
     reintroduce either token or any other blue value anywhere in this
     file or app.js. */

  --color-success: #1a7a3d;   --color-success-soft: #e3f5e9;
  /* Was #b06a00 (3.84:1 on white as .btn-archive text - fails AA); this
     shade measures 5.13:1 on white and 4.60:1 on --color-warning-soft
     (the badge case), both verified passing. */
  --color-warning: #9e5f00;   --color-warning-soft: #fdf1e0;
  --color-danger:  #c0342b;   --color-danger-soft:  #fce8e8;
  --color-neutral: #666;      --color-neutral-soft: #f0f0f0;

  /* Spacing - already an 8px grid with a 4px half-step (the industry-
     standard pattern, same as Stripe/Tailwind/Material); kept as-is. */
  --space-1: 4px;  --space-2: 8px;  --space-3: 12px; --space-4: 16px;
  --space-5: 20px; --space-6: 24px; --space-8: 32px; --space-10: 40px; --space-12: 48px;

  /* Radius - tightened from the previous 8/14/18/24/pill scale, which read
     as consumer/friendly (Linear/Stripe/Katana all use tighter 6-12px
     radii and rectangular, not pill, buttons). --radius-pill is kept only
     for genuinely pill-shaped elements (badges, toasts, the env indicator) -
     .btn now uses --radius-md instead of --radius-pill (see Buttons below). */
  --radius-sm: 6px; --radius-md: 8px; --radius-lg: 10px; --radius-xl: 12px; --radius-pill: 999px;

  /* Shadow - flattened from the previous soft/floaty multi-layer set
     (was up to 0 20px 56px on modals) to tighter, lower-blur elevation -
     reads as precise/enterprise rather than soft/consumer. */
  --shadow-xs: 0 1px 2px rgba(0,0,0,0.04);
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.06), 0 1px 1px rgba(0,0,0,0.03);
  --shadow-md: 0 2px 6px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04);
  --shadow-lg: 0 8px 24px rgba(0,0,0,0.12), 0 2px 6px rgba(0,0,0,0.05);

  /* Type - a genuinely stepped scale, each tier visually distinct from its
     neighbor (previously text-md/15px sat only 1px from text-base/14px -
     not a real hierarchy step, just noise). Named by role, not just size,
     since that's how they're actually used:
       xs   - microcopy, badges, helper text under a value
       sm   - labels, secondary/tertiary text, table headers
       base - body text, inputs, buttons (the default)
       md   - card titles, sub-section headings inside a card
       lg   - section titles (Summary/Materials/Costs inside a detail modal)
       xl   - page titles (h2)
       2xl  - KPI stat-card values
       3xl  - reserved for a single hero number, rarely used */
  --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --text-xs: 11px; --text-sm: 12px; --text-base: 14px; --text-md: 16px;
  --text-lg: 18px; --text-xl: 22px; --text-2xl: 28px; --text-3xl: 34px;
  --font-medium: 500; --font-semibold: 600; --font-bold: 700;

  /* Controls - single source of truth for button/input sizing so the two
     stay pixel-matched in height (both use the md pair by default). Kept
     on the 4px grid (5px was not a multiple of 4). */
  --control-padding-y-md: 8px; --control-padding-x-md: 16px;
  --control-padding-y-sm: 4px; --control-padding-x-sm: 12px;

  /* Fixed input widths matched to what they actually hold (rule: never
     uniform widths) - all on the 8px grid. */
  --field-w-tiny: 64px;    /* unit */
  --field-w-small: 96px;   /* quantity, date-adjacent numbers */
  --field-w-medium: 160px; /* cost amount, batch number, single price */
  --field-w-large: 240px;  /* material/product name when not flex-grown */

  /* Brand amber (2026-07-21 vision-redesign, Phase 0 - public/vision-
     redesign-spec.txt Part 2.1/2.1.2). Verified contrast, not eyeballed:
     #FFD328 vs --color-text-primary (#1a1a1e) = 12.09:1 (AAA). Amber must
     never carry white text (1.44:1, fails even the 3:1 UI-component
     minimum) - see the spec's RULE 1/2/3. Purely additive at this stage -
     nothing references these tokens yet (Phase 0 of the spec's Part 7). */
  --color-brand-amber:       #FFD328;
  --color-brand-amber-hover: #E6BD00; /* ~10.4:1 with dark text, still AAA */
  --color-brand-amber-soft:  rgba(255,211,40,0.16);
  --color-brand-amber-on:    var(--color-text-primary); /* text-on-amber alias, so no call site hardcodes #1a1a1e */

  /* Chrome (dark sidebar/top-bar surface) - semantic aliases for values
     already in use by .sidebar today, promoted to named tokens so the
     sidebar and the new top bar (Phase 1) are guaranteed pixel-identical
     in tone rather than just visually similar. */
  --color-chrome-bg:         var(--color-text-primary); /* = .sidebar's existing background today */
  --color-chrome-bg-raised:  #232328; /* one step lighter, for hover/control-resting fills on chrome */
  --color-chrome-text:       rgba(255,255,255,0.92);
  --color-chrome-text-muted: rgba(255,255,255,0.5); /* = .sidebar-user .username's existing value today */
  --color-chrome-border:     rgba(255,255,255,0.1); /* = .sidebar-header's existing border-bottom value today */

  /* Hairline shadow for the new top bar, shown only once page content has
     scrolled beneath it (see .table-scroll's sticky-header precedent for
     the same "elevate only when it means something" rule). */
  --shadow-chrome: 0 1px 0 rgba(0,0,0,0.06);
}

* { box-sizing: border-box; margin: 0; padding: 0; }
body {
  font-family: var(--font-family);
  background: var(--color-bg);
  color: var(--color-text-primary);
  font-size: var(--text-base);
  -webkit-font-smoothing: antialiased;
}

/* Tabular numbers for money/quantity columns so digits align */
.num { font-variant-numeric: tabular-nums; font-feature-settings: "tnum"; }

/* #app-shell now wraps .topbar (Part 3.1) above .app, instead of .app being
   the outermost element - it, not .app, needs to be the min-height:100vh
   flex column so the sidebar still stretches to fill the full viewport
   height below the top bar. */
#app-shell { display: flex; flex-direction: column; min-height: 100vh; }
.app { display: flex; flex: 1; }

/* ---------- Sidebar ---------- */
.sidebar { width: 224px; background: var(--color-text-primary); padding: var(--space-4) 0; flex-shrink: 0; }
.sidebar-header { display: flex; align-items: center; gap: var(--space-2); padding: 0 var(--space-4) var(--space-4); border-bottom: 1px solid rgba(255,255,255,0.1); margin-bottom: var(--space-3); }
/* Sized down from the previous --text-base (2026-07-21 vision-redesign
   Phase 1, spec Part 3.3) - the sidebar-header now does exactly one job
   (navigation, not brand identity), since the env badge and account block
   that used to share this header moved to the new top bar (see .topbar
   below). This wordmark stays only as a quiet confirmation of what app
   you're in while scrolled past the top bar's own wordmark.
   The real AWT chevron mark was added back here (2026-07-21, logo pass) -
   this reverses Part 2.7's original "appears exactly once per screen, not
   repeated in the sidebar" rule, on explicit instruction once the real
   brand asset (not the earlier typographic placeholder that rule was
   written to justify) was supplied, with the sidebar copy sized down
   (.sidebar-glyph, 22px) relative to the top bar's (.topbar-glyph, 28px)
   so it still reads as secondary confirmation, not a second equal-weight
   landmark. */
.sidebar-header h1 { color: #fff; font-size: var(--text-sm); font-weight: var(--font-semibold); letter-spacing: -0.01em; }
.sidebar-glyph { width: 22px; height: 22px; flex-shrink: 0; }
.sidebar-glyph svg { width: 100%; height: 100%; display: block; }

.env-badge {
  display: inline-flex; align-items: center; gap: var(--space-2);
  margin-top: var(--space-2); padding: 4px 12px;
  border-radius: var(--radius-pill); font-size: 10px; font-weight: var(--font-semibold);
  letter-spacing: 0.03em; text-transform: uppercase;
}
/* The two states swapped hues, deliberately: the SAFER state must look safer.
   Live means every Confirm moves real stock and posts real accounting entries,
   and it was rendering green - which reads as "fine" at a glance, on the one
   indicator whose whole job is to tell an operator the opposite. Dry run, the
   state where nothing reaches Zoho, was the one wearing caution amber.
   No new colour values: these are the same two palettes that were already here
   and already legible on the dark topbar, exchanged. The 10px / uppercase /
   letter-spacing micro-label treatment above is untouched, so this stays a calm
   status chip - a cautionary hue, not a louder badge. Live is set at the same
   0.3 alpha the green previously used, so caution is never fainter than the
   reassurance it replaced. */
.env-badge.dry-run { background: rgba(26,122,61,0.3); color: #7ee2a0; }
.env-badge.live { background: rgba(176,106,0,0.3); color: #ffcb80; }
.env-badge .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }

.nav-section { padding: 0 var(--space-4); margin: var(--space-3) 0 var(--space-1); font-size: 10px; font-weight: var(--font-semibold); text-transform: uppercase; letter-spacing: 0.06em; color: rgba(255,255,255,0.35); }
.nav-section:first-of-type { margin-top: 0; }

.nav-item {
  display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-4);
  color: rgba(255,255,255,0.7); cursor: pointer; font-size: var(--text-base);
  transition: background 0.15s, color 0.15s; border-right: 3px solid transparent;
}
.nav-item:hover { background: rgba(255,255,255,0.08); color: #fff; }
/* border-right-color changed from --color-accent (blue) to --color-brand-
   amber back in Phase 1 (2026-07-21, spec Part 3.3/RULE 3) - the highest-
   frequency, most-repeated-per-session amber usage in the product (every
   nav click shows it), at its strongest-contrast placement (12.09:1
   against this dark surface). The background wash was left as
   --color-accent-soft (blue) at that time, uncaught - Phase 1's own report
   said only border-right-color changed. Fixed here (2026-07-21, palette-
   fix pass): background is now --color-brand-amber-soft, unifying the
   active state's border and wash under one color instead of mixing amber
   and blue on the same element - RULE 3 explicitly endorses amber as the
   dominant, freely-used accent on this dark chrome surface. */
.nav-item.active { background: var(--color-brand-amber-soft); color: #fff; border-right-color: var(--color-brand-amber); }
.nav-item .icon { width: 16px; height: 16px; flex-shrink: 0; opacity: 0.9; }

/* One consistent keyboard-focus ring across every interactive element that
   can receive it - buttons, inputs (already had one via box-shadow, kept
   as-is below), sidebar nav (now keyboard-operable, see app.js), drawer
   nav, and clickable table rows. :focus-visible (not :focus) so it only
   shows for keyboard/programmatic focus, not every mouse click.
   Split into two groups (2026-07-21, palette-fix pass - was one shared
   rule using --color-accent/blue for all of them): elements that sit on
   the dark chrome surface (.nav-item, and the top bar's own brand/search/
   bell/account controls) get an amber ring - RULE 3 explicitly endorses
   amber as the dominant, freely-used accent there. Everything else here
   sits on the light canvas (.btn, .modal-detail-nav-link, clickable table
   rows, alert-banner links, and .cmdk-result - a result row inside the
   command palette's own white popover, not the dark bar that opens it) -
   RULE 2 forbids amber as a thin ring/line on a light surface, so those
   get a black/near-black ring instead. */
.nav-item:focus-visible, .topbar-brand:focus-visible, .cmdk-trigger:focus-visible,
.topbar-bell:focus-visible, .topbar-account-trigger:focus-visible {
  outline: 2px solid var(--color-brand-amber); outline-offset: 2px;
}
.btn:focus-visible, .modal-detail-nav-link:focus-visible,
tbody tr.is-clickable:focus-visible, .alert-banner a:focus-visible,
.cmdk-result:focus-visible {
  outline: 2px solid var(--color-text-primary); outline-offset: 2px;
}
.nav-item:focus-visible { outline-offset: -2px; }

/* ---------- Top app bar (2026-07-21 vision-redesign Phase 1, spec Part 3.1) ----------
   Persistent, --color-chrome-bg surface, sitting above the sidebar+main row
   (see index.html - .topbar is now a sibling of .app, not inside it) so the
   two dark-chrome regions read as one continuous "L" shape (Part 2.3), not
   two independently-styled boxes. position:sticky (not fixed) - the
   document itself is the scroll container (no overflow-y set on body/.main
   before this), so sticky-to-viewport-top works without a separate scroll
   wrapper. Content is fixed/identical on every screen by design (see the
   spec's own "a bar whose contents change per page stops being a reliable
   landmark" reasoning) - nothing here is page-specific. */
.topbar {
  height: 56px; flex-shrink: 0; position: sticky; top: 0; z-index: 60;
  display: flex; align-items: center; gap: var(--space-4);
  padding: 0 var(--space-4); background: var(--color-chrome-bg);
  transition: box-shadow 0.15s;
}
/* Hairline shadow only once page content has scrolled beneath the bar - the
   same "elevate only when it means something" rule .table-scroll's sticky
   header already uses (see --shadow-chrome's own comment in :root). Class
   toggled by a plain window-scroll listener in app.js. */
.topbar.is-scrolled { box-shadow: var(--shadow-chrome); }

.topbar-brand { display: flex; align-items: center; gap: var(--space-2); cursor: pointer; flex-shrink: 0; }
/* The real AWT chevron mark (2026-07-21, logo pass) - AWT_LOGO_MARK in
   app.js, inline SVG (no upload/static-asset file needed), injected once
   into this and .sidebar-glyph at script load since both are static
   containers in index.html. Was a plain amber-filled letter box (the
   spec's own documented safe first-pass default, Part 5, "swappable for a
   real asset later without any other change") until the real mark was
   supplied - no background/color box needed now, the mark carries its own
   two-tone color directly. */
.topbar-glyph { width: 28px; height: 28px; flex-shrink: 0; }
.topbar-glyph svg { width: 100%; height: 100%; display: block; }
.topbar-wordmark { color: var(--color-chrome-text); font-size: var(--text-base); font-weight: var(--font-semibold); letter-spacing: -0.01em; white-space: nowrap; }
/* Discreet credit line, placed beside the wordmark (not below it) rather
   than growing .topbar-brand to two rows - the top bar's 56px height is an
   established value referenced elsewhere (Part 3.1's own sticky-bar
   comment), and a same-row placement gets the credit in without touching
   it. --text-xs + the chrome surface's own muted-text token (already used
   for .topbar-bell's badge/.cmdk-trigger's placeholder text) keeps it
   quiet relative to the wordmark/section title next to it; the left
   border is a hairline separator, not a competing accent color. Hidden at
   the same tablet breakpoint that already drops the wordmark/title (below)
   since a credit line with no product name next to it reads as orphaned,
   and that breakpoint's whole point is reclaiming space for the controls
   that actually still need it at that width. */
.topbar-credit {
  color: var(--color-chrome-text-muted); font-size: var(--text-xs); white-space: nowrap;
  border-left: 1px solid rgba(255,255,255,0.15); padding-left: var(--space-2); margin-left: 2px;
}
/* Current section title - replaces each page's own h2 as the primary "where
   am I" signal (Part 3.1); the page's own <h2> stays for the subtitle/
   page-specific actions but is no longer the only place the location is
   stated. Plain text, updated by setTopbarTitle() in app.js on every
   navigate() call. */
.topbar-title { color: var(--color-chrome-text); font-size: var(--text-md); font-weight: var(--font-medium); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.topbar-spacer { flex: 1; min-width: var(--space-4); }

/* Command palette trigger (Part 3.1/3.2) - the Linear/GitHub convention:
   a wide, subtly-bordered pill on --color-chrome-bg-raised so it reads as a
   control sitting on the chrome surface, not a flat color match. */
.cmdk-trigger {
  display: flex; align-items: center; gap: var(--space-2);
  width: 320px; max-width: 36vw; flex-shrink: 1;
  background: var(--color-chrome-bg-raised); border: 1px solid var(--color-chrome-border);
  border-radius: var(--radius-md); padding: var(--space-2) var(--space-3);
  color: var(--color-chrome-text-muted); font-size: var(--text-sm); font-family: inherit;
  cursor: pointer; transition: background 0.15s, color 0.15s;
}
.cmdk-trigger:hover { background: #2c2c32; color: var(--color-chrome-text); }
.cmdk-trigger svg { width: 15px; height: 15px; flex-shrink: 0; }
.cmdk-trigger-label { flex: 1; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cmdk-kbd { font-size: 10px; font-weight: var(--font-semibold); flex-shrink: 0; opacity: 0.7; }

.topbar-actions { display: flex; align-items: center; gap: var(--space-3); flex-shrink: 0; }
/* The env badge moves here from the sidebar header (Part 3.1 - "arguably
   the single most operationally important piece of state in the entire
   app," now permanently visible instead of only when the sidebar header
   isn't scrolled past). Same .env-badge component, just a new host context -
   its own margin-top (meant for stacking under the old sidebar h1) doesn't
   apply in this flex row. */
.topbar .env-badge { margin-top: 0; }

.topbar-bell { position: relative; background: none; border: none; padding: 4px; color: var(--color-chrome-text); cursor: pointer; display: flex; align-items: center; border-radius: var(--radius-sm); }
.topbar-bell:hover { background: var(--color-chrome-bg-raised); }
.topbar-bell svg { width: 18px; height: 18px; }
.topbar-bell-count {
  position: absolute; top: 0; right: 0; min-width: 15px; height: 15px; padding: 0 3px;
  border-radius: var(--radius-pill); background: var(--color-danger); color: #fff;
  font-size: 9px; font-weight: var(--font-semibold); line-height: 15px; text-align: center;
}

/* Account menu (Part 3.1) - replaces the old bare "username ... Log out"
   text pairing at the sidebar bottom with the GitHub/Notion/Linear-standard
   circular-initials-avatar + dropdown shape. Populated by app.js's
   bootApp()/renderAccountMenu(), not static markup. */
.topbar-account { position: relative; }
.topbar-account-trigger { display: flex; align-items: center; cursor: pointer; background: none; border: none; padding: 0; border-radius: 50%; }
.topbar-avatar {
  width: 28px; height: 28px; border-radius: 50%; flex-shrink: 0;
  background: var(--color-brand-amber); color: var(--color-brand-amber-on);
  display: flex; align-items: center; justify-content: center;
  font-size: var(--text-xs); font-weight: var(--font-semibold); letter-spacing: 0.02em;
}
.account-menu {
  position: absolute; top: calc(100% + var(--space-2)); right: 0; z-index: 70;
  min-width: 168px; background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius-md); box-shadow: var(--shadow-md); overflow: hidden;
}
.account-menu-username { padding: var(--space-3) var(--space-4); font-size: var(--text-sm); color: var(--color-text-secondary); border-bottom: 1px solid var(--color-border); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.account-menu button {
  display: block; width: 100%; text-align: left; background: none; border: none;
  padding: var(--space-3) var(--space-4); font-size: var(--text-base); color: var(--color-text-primary); cursor: pointer;
}
.account-menu button:hover { background: var(--color-surface-sunken); }

/* ---------- Command palette (2026-07-21 vision-redesign Phase 1, spec Part 3.2) ----------
   Reuses the existing .modal-overlay exactly (centered overlay treatment,
   click-outside-to-close via openModal's own handler) with a new panel
   variant that aligns near the top of the viewport instead of dead-center -
   the Linear/GitHub convention this pattern is drawn from. Ships with
   page-navigation results only in this phase (3.2's own noted Phase 4
   dependency - record search needs a new backend search endpoint that
   hasn't been scoped/approved yet); the "GO TO" group mirrors the sidebar's
   nav items 1:1 by reading them directly from the DOM (see
   commandPaletteEntries() in app.js), so this is never a second, driftable
   source of truth for what pages exist. */
.modal-panel.modal-cmdk {
  width: 560px; max-width: 90vw; max-height: 70vh;
  align-self: flex-start; margin-top: 96px;
  padding: 0; overflow: hidden; display: flex; flex-direction: column;
  animation: modal-in 0.1s ease-out;
}
.cmdk-input-row { flex-shrink: 0; display: flex; align-items: center; gap: var(--space-3); padding: var(--space-4) var(--space-5); border-bottom: 1px solid var(--color-border); }
.cmdk-input-row svg { width: 18px; height: 18px; color: var(--color-text-tertiary); flex-shrink: 0; }
.cmdk-input-row input { flex: 1; border: none; outline: none; font-size: var(--text-md); font-family: inherit; color: var(--color-text-primary); background: none; }
.cmdk-results { flex: 1; overflow-y: auto; padding: var(--space-2) 0 var(--space-4); }
.cmdk-group-label { padding: var(--space-3) var(--space-5) var(--space-2); font-size: 10px; font-weight: var(--font-semibold); text-transform: uppercase; letter-spacing: 0.06em; color: var(--color-text-tertiary); }
.cmdk-result { padding: var(--space-3) var(--space-5); font-size: var(--text-base); color: var(--color-text-primary); cursor: pointer; display: flex; align-items: baseline; gap: var(--space-2); }
.cmdk-result:hover, .cmdk-result.is-active { background: var(--color-surface-sunken); }
.cmdk-empty { padding: var(--space-3) var(--space-5); font-size: var(--text-sm); color: var(--color-text-tertiary); }
/* Phase 4 (record search) - a result row can carry a second, muted field
   (batch status, SKU, employee/machine code) alongside its main label. */
.cmdk-result-label { flex-shrink: 0; }
.cmdk-result-sublabel { font-size: var(--text-sm); color: var(--color-text-tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Inline spinner shown in the palette's own record-results section while
   a debounced search is in flight - reuses the existing spin keyframe
   (.loading::before), not a new animation. */
.cmdk-loading { padding: var(--space-3) var(--space-5); font-size: var(--text-sm); color: var(--color-text-tertiary); display: flex; align-items: center; gap: var(--space-2); }
.cmdk-spinner {
  width: 14px; height: 14px; border-radius: 50%; flex-shrink: 0;
  border: 2px solid var(--color-border-strong); border-top-color: var(--color-text-primary);
  animation: spin 0.7s linear infinite;
}

.main { flex: 1; padding: var(--space-8) var(--space-10); overflow-x: auto; max-width: 1400px; }

/* ---------- Page header ---------- */
.page-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--space-6); gap: var(--space-4); }
.page-header h2 { font-size: var(--text-xl); font-weight: var(--font-semibold); letter-spacing: -0.01em; }
.page-header .subtitle { font-size: var(--text-sm); color: var(--color-text-secondary); margin-top: 2px; }
.page-header-actions { display: flex; gap: var(--space-2); flex-shrink: 0; }

/* Phase 5 (spec 4.5.1) - Board/Table view toggle, Production Orders and
   Packaging Orders. Two-state segmented control, not two separate buttons -
   only one is ever "active" at a time. */
.view-toggle { display: inline-flex; border: 1px solid var(--color-border-strong); border-radius: var(--radius-md); overflow: hidden; flex-shrink: 0; }
.view-toggle-btn { padding: var(--control-padding-y-md) var(--control-padding-x-md); background: var(--color-surface); border: none; cursor: pointer; font-size: var(--text-base); font-weight: var(--font-medium); color: var(--color-text-secondary); }
.view-toggle-btn:hover { background: var(--color-surface-sunken); }
.view-toggle-btn.active { background: var(--color-text-primary); color: #fff; }
.view-toggle-btn + .view-toggle-btn { border-left: 1px solid var(--color-border-strong); }

/* Phase 5 (spec 4.5.1/4.7) - the status board (Kanban pattern), Production
   Orders and Packaging Orders' new default view. Columns are a fixed set
   (Draft/In progress/Completed/Errors) - never reorderable, and cards are
   deliberately NOT draggable (no drag handlers exist anywhere here) - the
   spec's own explicit rejection: every status transition is gated by real
   backend rules (QC pass, yield variance, stock pre-flight) a drag gesture
   can't represent, so the interaction model must not imply otherwise. */
.board { display: flex; gap: var(--space-4); align-items: flex-start; overflow-x: auto; padding: var(--space-4); }
.board-column { flex: 1 1 220px; min-width: 220px; background: var(--color-surface-sunken); border-radius: var(--radius-lg); padding: var(--space-3); }
.board-column-head { font-size: var(--text-sm); font-weight: var(--font-semibold); text-transform: uppercase; letter-spacing: 0.04em; color: var(--color-text-secondary); padding: var(--space-2) var(--space-2) var(--space-3); }
.board-column-count { color: var(--color-text-tertiary); font-weight: var(--font-medium); }
.board-column-empty { padding: var(--space-4) var(--space-2); text-align: center; font-size: var(--text-sm); color: var(--color-text-tertiary); }
.board-card {
  background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-md);
  padding: var(--space-3); margin-bottom: var(--space-3); cursor: pointer; box-shadow: var(--shadow-xs);
  transition: box-shadow 0.15s, border-color 0.15s;
}
.board-card:last-child { margin-bottom: 0; }
.board-card:hover { box-shadow: var(--shadow-sm); border-color: var(--color-border-strong); }
.board-card-batch { font-weight: var(--font-semibold); margin-bottom: 2px; }
.board-card-product { color: var(--color-text-secondary); font-size: var(--text-sm); margin-bottom: var(--space-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.board-card-secondary { font-size: var(--text-sm); color: var(--color-text-tertiary); }
.board-card-inflight { color: var(--color-warning); font-weight: var(--font-medium); }
.board-card-error { color: var(--color-danger); font-weight: var(--font-medium); display: inline-flex; align-items: center; gap: 4px; }
/* Packaging Orders' own extra rule (spec 4.7): a card landing in Errors
   gets a visibly STRONGER warning than Production Orders' equivalent card
   (which only shows the same red text above) - packaging orders have no
   automated or bulk-manual retry path at all today (CLAUDE.md 3.3), so
   "stuck with errors" here really does mean stuck until someone manually
   intervenes, and the visual weight should say so honestly. */
.board-card.is-danger-strong { border: 2px solid var(--color-danger); }

/* ---------- Buttons ---------- */
.btn {
  background: var(--color-surface); border: 1px solid var(--color-border-strong);
  padding: var(--control-padding-y-md) var(--control-padding-x-md); border-radius: var(--radius-md);
  font-size: var(--text-base); line-height: 1.3;
  font-weight: var(--font-medium); cursor: pointer; display: inline-flex; align-items: center;
  gap: var(--space-2); color: var(--color-text-primary); transition: background 0.15s, box-shadow 0.15s, opacity 0.15s, border-color 0.15s;
}
.btn:hover { background: var(--color-surface-sunken); }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
.btn:disabled:hover { background: var(--color-surface); }

/* Amber fill + dark text (RULE 1), app-wide (2026-07-21, palette-fix pass) -
   was blue+white text everywhere except the login page (Phase 1 deliberately
   scoped its amber recolor to .login-card .btn-primary only, reasoning that
   "no phase schedules an app-wide primary-button recolor"). That reasoning
   is explicitly overridden now: blue is banned from this app's UI with no
   exception, and amber-filled-with-dark-text is the only option left in the
   strict amber/white/black/gray(+danger-red) palette for a primary-action
   button. This is the single most visible change in this pass - every
   "+ New ___", Apply, Save, Complete, Retry, etc. button in the app is
   amber now, not just the login button. The .login-card-specific override
   that used to exist below this rule is gone - it's now identical to this
   base rule, so it was removed rather than kept as a dead duplicate. */
.btn-primary { background: var(--color-brand-amber); color: var(--color-brand-amber-on); border: none; box-shadow: var(--shadow-xs); }
.btn-primary:hover { background: var(--color-brand-amber-hover); }
/* Without this, the generic .btn:disabled:hover rule below (higher
   specificity than .btn-primary alone) resets a disabled+hovered primary
   button's background to plain white instead of keeping it dimmed-accent -
   a visible flash-to-white on hover while "disabled". */
.btn-primary:disabled:hover { background: var(--color-brand-amber); }

.btn-danger { background: var(--color-surface); color: var(--color-danger); border: 1px solid var(--color-danger); }
.btn-danger:hover { background: var(--color-danger-soft); }
.btn-danger:disabled:hover { background: var(--color-surface); }

/* Archive is visually distinct from Delete (amber vs red) so the two
   destructive-adjacent actions are never confusable at a glance. */
.btn-archive { background: var(--color-surface); color: var(--color-warning); border: 1px solid var(--color-warning); }
.btn-archive:hover { background: var(--color-warning-soft); }
.btn-archive:disabled:hover { background: var(--color-surface); }

.btn-sm { padding: var(--control-padding-y-sm) var(--control-padding-x-sm); font-size: var(--text-sm); }

/* In-flight state for a submit/action button - hides the label (rather
   than shrinking the button) and overlays a spinner, reusing the same
   spin keyframe as the page-level .loading indicator. Paired with
   btn.disabled=true in JS so a slow request can't be double-submitted. */
.btn-loading { position: relative; color: transparent !important; }
.btn-loading::after {
  content: ''; position: absolute; left: 50%; top: 50%; width: 14px; height: 14px;
  margin: -7px 0 0 -7px; border-radius: 50%; border: 2px solid rgba(0,0,0,0.15);
  border-top-color: var(--color-text-primary); animation: spin 0.7s linear infinite;
}
/* Was a white spinner track/highlight (correct contrast on the old blue
   fill) - now dark, matching RULE 1's dark-text-on-amber requirement
   (2026-07-21, palette-fix pass). Same values the login-specific override
   already used below before it became redundant and was removed. */
.btn-primary.btn-loading::after { border-color: rgba(0,0,0,0.2); border-top-color: var(--color-brand-amber-on); }
/* currentColor won't work here - .btn-loading's own color is transparent
   (that's what hides the label), and ::after inherits it - so these need
   the actual variable, not a computed inherited color. */
.btn-danger.btn-loading::after { border-top-color: var(--color-danger); }
.btn-archive.btn-loading::after { border-top-color: var(--color-warning); }

/* ---------- Stats / cards ---------- */
/* min lowered from 180 to 160 - at 180 a 6-card grid (e.g. the sync-log
   status breakdown) measured live at 1136px available width only fit 5
   per row, orphaning the 6th card alone on its own row with the rest of
   that row empty. 160 lets 6 fit (6*160+5*16 gap = 1040 < 1136) without
   the cards reading as cramped - confirmed live, not just computed. */
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--space-4); margin-bottom: var(--space-6); }
.stats-grid.no-margin { margin-bottom: 0; }
/* A slim supporting line under a stat grid for figures that matter but
   don't need their own full KPI card (e.g. cost/unit, selling price under
   the order-detail Summary's 5 primary cards). */
.stat-caption { font-size: var(--text-sm); color: var(--color-text-secondary); margin-top: var(--space-3); display: flex; gap: var(--space-4); flex-wrap: wrap; }
.stat-caption strong { color: var(--color-text-primary); font-weight: var(--font-semibold); }
/* Reports page - plain CSS width-based bars (no charting library), sized
   relative to the max value in their own column. Track sits below the
   number, not overlapping it, so it reads fine even at narrow widths. */
.report-bar-cell { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
.report-bar-track { width: 100%; max-width: 120px; height: 5px; border-radius: 3px; background: var(--color-border); overflow: hidden; }
/* Default/neutral fill was --color-accent (blue) - now --color-neutral
   (plain dark gray), not amber: these bars appear on every row of every
   cost column, and using brand amber for a value shown that often/densely
   would contradict RULE 3's own "restrained... rather than everywhere"
   intent for amber's non-chrome use (2026-07-21, palette-fix pass). */
.report-bar-fill { height: 100%; border-radius: 3px; background: var(--color-neutral); }
.report-bar-fill.success { background: var(--color-success); }
.report-bar-fill.danger { background: var(--color-danger); }
.report-bar-fill.warning { background: var(--color-warning); }
.report-bar-fill.neutral { background: var(--color-neutral); }

/* Phase 3 (spec 3.7) - the "trend-chart" component, same plain-SVG
   philosophy as .report-bar-* above, just two more shapes: a small
   sparkline inside a stat-card, and a bigger multi-series line chart on
   Reports. Both are inline SVG built by sparklineSvg()/trendChartSvg() in
   app.js - no charting library added. */
.stat-card-sparkline { margin-top: var(--space-2); line-height: 0; }
.sparkline { width: 100%; height: 32px; display: block; }
.trend-chart { padding: 0 var(--space-4) var(--space-4); }
.trend-chart-svg { width: 100%; height: 220px; display: block; }
.trend-chart-legend { display: flex; flex-wrap: wrap; gap: var(--space-4); margin-top: var(--space-2); font-size: var(--text-xs); color: var(--color-text-secondary); }
.trend-chart-legend-item { display: inline-flex; align-items: center; gap: 6px; }
.trend-chart-swatch { display: inline-block; width: 8px; height: 8px; border-radius: 50%; }

.stat-card { background: var(--color-surface); border-radius: var(--radius-lg); padding: var(--space-5); border: 1px solid var(--color-border); box-shadow: var(--shadow-sm); }
.stat-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-2); margin-bottom: var(--space-2); }
.stat-card .label { font-size: var(--text-sm); color: var(--color-text-secondary); }
.stat-card .value { font-size: var(--text-2xl); font-weight: var(--font-semibold); letter-spacing: -0.01em; }
.stat-card .value.num { font-variant-numeric: tabular-nums; text-align: left; }
.stat-card-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); margin-top: var(--space-2); flex-wrap: wrap; }
.stat-card .trend { font-size: var(--text-xs); font-weight: var(--font-medium); white-space: nowrap; }
.stat-card .trend.good { color: var(--color-success); }
.stat-card .trend.bad { color: var(--color-danger); }
.stat-card .trend.flat { color: var(--color-text-tertiary); }
.stat-card .secondary { font-size: var(--text-xs); color: var(--color-text-tertiary); }

/* Phase 2 (vision-redesign-spec.txt 3.4's "infolet" pattern): a KPI card
   that also acts as a one-click filter shortcut for the table below it.
   REVISED (2026-07-21, palette-fix pass) - this originally used
   --color-accent (blue) for the active-filter ring, reasoned at the time
   as "amber can't be a thin accent on light canvas (RULE 2), blue already
   owns that job." That reasoning is explicitly overridden now: blue is
   banned outright, with no exception. Hover gets a plain dark-neutral
   border (a light touch, not yet "selected"). Active gets a solid
   dark/near-black border (definition, RULE-2-safe - not a colored ring)
   plus a subtle --color-brand-amber-soft background wash (the "fill,"
   not "ring," option RULE 1/2 already allow for amber on light surfaces) -
   this reads as a real selected/highlighted state without amber ever
   being the border/ring color itself. */
.stat-card.is-clickable { cursor: pointer; transition: box-shadow 0.15s, border-color 0.15s, background 0.15s; }
.stat-card.is-clickable:hover { border-color: var(--color-border-strong); box-shadow: var(--shadow-md); }
.stat-card.is-active { border-color: var(--color-text-primary); background: var(--color-brand-amber-soft); }

/* Icon chip in each KPI card's top-right corner - tone matches the metric's
   current status (danger/warning/success/info/neutral), independent of the
   trend arrow's own good/bad coloring. */
.icon-wrap { width: 32px; height: 32px; border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.icon-wrap svg { width: 16px; height: 16px; }
.icon-wrap.neutral { background: var(--color-neutral-soft); color: var(--color-neutral); }
.icon-wrap.success { background: var(--color-success-soft); color: var(--color-success); }
.icon-wrap.warning { background: var(--color-warning-soft); color: var(--color-warning); }
.icon-wrap.danger  { background: var(--color-danger-soft);  color: var(--color-danger); }
/* Was --color-info-soft/--color-info (blue) - now a darker neutral than
   .icon-wrap.neutral above (2026-07-21, palette-fix pass), so the one
   current call site (Dashboard's "On the floor right now," when something
   is actively in progress) still reads as distinct from plain/idle
   neutral, without introducing color. */
.icon-wrap.info    { background: var(--color-neutral-soft);  color: var(--color-text-primary); }

.dashboard-section-label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text-secondary); margin: var(--space-6) 0 var(--space-3); }
.dashboard-section-label:first-of-type { margin-top: 0; }

.card { background: var(--color-surface); border-radius: var(--radius-lg); border: 1px solid var(--color-border); box-shadow: var(--shadow-sm); overflow: hidden; }
/* Stacked cards space themselves apart automatically - no more repeated
   inline style="margin-bottom:16px" at every call site (and every one of
   those was a hardcoded px value standing in for var(--space-4) anyway). */
.card + .card { margin-top: var(--space-4); }
/* A small heading inside a card, above its table (e.g. dashboard's "Waste"
   card) - was a one-off inline style with hardcoded padding at its single
   call site. */
.card-section-head { padding: var(--space-4) var(--space-4) 0; }
.card-section-head h4 { font-size: var(--text-base); font-weight: var(--font-semibold); }

/* ---------- Tables ---------- */
/* Wraps every <table> inside a .card. Contains horizontal overflow to the
   table itself (fixes a real bug: .main was the only overflow-x:auto
   ancestor, so a table wider than the viewport dragged the page header and
   stat cards along with it when scrolling right - now only the table
   scrolls).
   max-height + overflow-y:auto (added in the 2026-07-20 polish pass) makes
   this wrapper the table's own bounded scroll region, so thead's
   position:sticky actually has something to stick to. Before this, only
   overflow-x was set - CSS's overflow computed-value rules force the
   unset overflow-y (initial value 'visible') to also compute to 'auto'
   the moment overflow-x isn't 'visible', so this wrapper was ALREADY an
   independent scroll container on both axes even without max-height, it
   just never needed a vertical scrollbar because nothing bounded its
   height - meaning the sticky header was sticking relative to a box that
   grew to fit all content and never itself scrolled, while the actual
   visible scrolling happened one level up on the page. Confirmed live
   (not just reasoned about): scrolling the 69-row Items page moved the
   header off-screen with the rest of the table instead of pinning it.
   Bounding the height here is what makes the sticky context real. */
.table-scroll { overflow-x: auto; overflow-y: auto; max-height: 70vh; }
table { width: 100%; border-collapse: collapse; font-size: var(--text-base); }
thead th {
  text-align: left; padding: var(--space-3) var(--space-4); background: var(--color-surface-sunken);
  color: var(--color-text-secondary); font-weight: var(--font-medium); border-bottom: 1px solid var(--color-border);
  font-size: var(--text-sm); text-transform: uppercase; letter-spacing: 0.04em;
  position: sticky; top: 0; z-index: 1;
}
tbody td { padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--color-border); }
/* Opt-in per column (money/quantity cells) rather than a blanket rule,
   since not every table's columns are numeric. */
td.num, th.num { text-align: right; }
/* Phase 2 (spec 3.4's zone 3): click-to-sort column headers, built on the
   existing thead th styling - no new visual language, just an interaction
   added to what's already there. The ↕/↑/↓ glyph itself carries all the
   "is this the active sort column" signal; is-sortable's own hover just
   confirms the header is clickable. */
.is-sortable { cursor: pointer; user-select: none; }
.is-sortable:hover { color: var(--color-text-primary); }
.sort-indicator { display: inline-block; margin-left: 2px; opacity: 0.6; }
tbody tr:hover { background: var(--color-surface-sunken); }
tbody tr:last-child td { border-bottom: none; }
tbody tr.is-archived { opacity: 0.55; }
/* Applied to rows that navigate somewhere on click (in addition to, not
   instead of, their existing explicit action button - the row click is a
   convenience, the button remains the unambiguous affordance). */
tbody tr.is-clickable { cursor: pointer; }
/* Was a `style="display:flex;gap:6px;justify-content:flex-end"` repeated
   verbatim at every row's action cell across every table - one class now. */
.row-actions { display: flex; gap: var(--space-2); justify-content: flex-end; }

/* ---------- Badges ---------- */
.badge { display: inline-block; padding: 4px 12px; border-radius: var(--radius-pill); font-size: var(--text-xs); font-weight: var(--font-medium); }
.badge-green { background: var(--color-success-soft); color: var(--color-success); }
.badge-amber { background: var(--color-warning-soft); color: var(--color-warning); }
.badge-gray  { background: var(--color-neutral-soft); color: var(--color-neutral); }
.badge-red   { background: var(--color-danger-soft); color: var(--color-danger); }
/* .badge-blue (background: --color-info-soft, color: --color-info) is
   gone (2026-07-21, palette-fix pass) - it had two call sites that
   actually communicate different things, so it was split into two new
   classes per-context rather than one blind recolor:
   - .badge-brand: a solid brand-amber pill with dark text (RULE 1's
     "filled shape, dark text" - not a soft tint, since RULE 2 forbids
     amber as a colored-text-on-light-tint treatment) for the "Default"
     BOM badge - a genuine highlight/notable state, consistent with amber
     already meaning "active/selected/highlighted" everywhere else in the
     app (nav-item.active, KPI is-active).
   - .badge-dark: neutral-soft background with near-black text (bolder
     than plain .badge-gray) for the "Synced" batch/order badge - purely
     informational, not a highlight, so amber was deliberately not used
     here; needs to read as distinct from the existing "Not synced"
     .badge-gray it sits next to. */
.badge-brand { background: var(--color-brand-amber); color: var(--color-brand-amber-on); }
.badge-dark  { background: var(--color-neutral-soft); color: var(--color-text-primary); }

/* ---------- Alert banners (dashboard / sync log "impossible to miss") ---------- */
.alert-banner {
  display: flex; align-items: flex-start; gap: var(--space-3);
  padding: var(--space-4) var(--space-5); border-radius: var(--radius-lg);
  border-left: 4px solid; margin-bottom: var(--space-4); font-size: var(--text-base);
}
.alert-banner .alert-title { font-weight: var(--font-semibold); margin-bottom: 2px; display: flex; align-items: center; gap: var(--space-2); }
/* Small inline status icon (replaces emoji) - sits next to text, not
   centered/blocked like .empty-state/.error-state's larger standalone icon. */
.icon-inline { width: 14px; height: 14px; flex-shrink: 0; vertical-align: -2px; }
.alert-banner .alert-body { color: var(--color-text-secondary); }
.alert-banner a { color: inherit; text-decoration: underline; font-weight: var(--font-medium); cursor: pointer; }
.alert-banner.danger  { background: var(--color-danger-soft);  border-color: var(--color-danger);  color: var(--color-danger); }
.alert-banner.warning { background: var(--color-warning-soft); border-color: var(--color-warning); color: var(--color-warning); }
/* Was --color-info-soft/--color-info (blue) - not currently invoked
   anywhere in app.js (confirmed by grep before making this change), but
   fixed anyway per "remove blue from the palette entirely," not just its
   currently-reachable uses. Now a neutral/dark scheme matching the same
   informational mapping used elsewhere in this pass. */
.alert-banner.info    { background: var(--color-neutral-soft);  border-color: var(--color-border-strong);  color: var(--color-text-primary); }
.alert-banner.success { background: var(--color-success-soft); border-color: var(--color-success); color: var(--color-success); }

/* ---------- Ambient (non-hover) first-encounter hint ---------- */
/* Visible-by-default caption, distinct from .help-tip (hover/focus-only,
   see helpTip() in app.js) - public/bom-to-production-order-onboarding-
   assessment.txt Part 5 recommendation 2: a first-time user hesitating
   over which of two similar fields is "the real one" doesn't yet know a
   tooltip icon holds the answer, so this is shown without any interaction
   required. Session-scoped/dismissible (dismissPlannedQtyHint() in
   app.js) rather than a permanent fixture - stays gone once dismissed
   until the page reloads, since this app has no localStorage/persistent-
   client-state convention to extend for a true "seen before" flag.
   Deliberately lighter-weight than .alert-banner (no colored left border,
   smaller text) - this is routine guidance, not a warning. */
.ambient-hint {
  display: flex; align-items: flex-start; gap: var(--space-2);
  padding: var(--space-3) var(--space-4); border-radius: var(--radius-md);
  background: var(--color-neutral-soft); border: 1px solid var(--color-border-strong);
  color: var(--color-text-secondary); font-size: var(--text-sm); line-height: 1.45;
  margin: var(--space-1) 0 var(--space-3);
}
.ambient-hint strong, .ambient-hint em { color: var(--color-text-primary); font-style: normal; }
.ambient-hint-dismiss {
  flex-shrink: 0; border: none; background: transparent; cursor: pointer;
  color: var(--color-text-tertiary); font-size: 16px; line-height: 1; padding: 0 var(--space-1);
}
.ambient-hint-dismiss:hover, .ambient-hint-dismiss:focus { color: var(--color-text-primary); }

/* ---------- Empty state ---------- */
.empty-state { text-align: center; padding: 56px 20px; color: var(--color-text-tertiary); }
.empty-state .icon { width: 32px; height: 32px; margin: 0 auto var(--space-3); opacity: 0.5; display: block; }
.empty-state .empty-title { color: var(--color-text-secondary); font-weight: var(--font-medium); margin-bottom: 2px; }
.empty-state .empty-hint { font-size: var(--text-sm); }
.empty-state .empty-action { margin-top: var(--space-4); }
/* A page-load failure (network/API error) - distinct from an empty-but-
   successful result, and includes an actual way to recover instead of a
   dead end (previously just static text). */
.error-state { text-align: center; padding: 56px 20px; }
.error-state .icon { width: 32px; height: 32px; margin: 0 auto var(--space-3); color: var(--color-danger); display: block; }
.error-state .error-title { color: var(--color-text-primary); font-weight: var(--font-medium); margin-bottom: 4px; }
.error-state .error-detail { font-size: var(--text-sm); color: var(--color-text-secondary); margin-bottom: var(--space-4); }

/* ---------- Loading state (spinner + label) ---------- */
.loading {
  text-align: center;
  padding: var(--space-10);
  color: var(--color-text-tertiary);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
}
.loading::before {
  content: '';
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 3px solid var(--color-border-strong);
  border-top-color: var(--color-text-primary); /* was --color-accent (blue) */
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------- Skeleton loader ---------- */
/* Used for the initial full-page navigation load in place of the plain
   spinner, shaped like the page-header + table it's about to become so the
   transition doesn't jump. The spinner (.loading above) is kept for
   contexts a skeleton doesn't fit as well (in-modal fetches). */
.skeleton-block { background: linear-gradient(90deg, var(--color-border) 25%, var(--color-surface-sunken) 37%, var(--color-border) 63%); background-size: 400% 100%; animation: skeleton-shine 1.4s ease infinite; border-radius: var(--radius-sm); }
@keyframes skeleton-shine { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
.skeleton-page-header { height: 28px; width: 220px; margin-bottom: var(--space-2); }
.skeleton-page-subtitle { height: 14px; width: 140px; margin-bottom: var(--space-6); }
.skeleton-row { height: 40px; margin-bottom: 1px; }

/* ---------- Modals (single canonical definition) ---------- */
.modal-overlay {
  position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.4);
  display: flex; justify-content: center; align-items: center; z-index: 100; padding: var(--space-5);
}
.modal-panel {
  width: 560px; max-width: 90vw; max-height: 85vh; overflow-y: auto;
  background: var(--color-surface); border-radius: var(--radius-xl);
  box-shadow: var(--shadow-lg); padding: var(--space-6);
}
.modal-panel.modal-wide { width: 720px; }
.modal-panel h3 { font-size: var(--text-lg); font-weight: var(--font-semibold); margin-bottom: var(--space-5); letter-spacing: -0.01em; }

/* Large centered detail modal - production order detail, and any other
   multi-section detail view (BOM detail, etc.) going forward. Explicitly
   NOT a drawer: centered like every other modal, just wider and taller,
   with its own internal header/nav/footer pinned via flex-shrink:0 around
   a scrolling body so they read as "sticky" without position:sticky
   nested inside a scroll container (simpler, and avoids the ambiguous
   overflow-pairing edge cases that come with that approach). Uses the
   same .modal-overlay (centered) as every other modal - no separate
   overlay variant needed. */
.modal-panel.modal-detail {
  width: 1200px; max-width: 92vw; max-height: 85vh;
  padding: 0; overflow: hidden;
  display: flex; flex-direction: column;
  animation: modal-in 0.15s ease-out;
}
@keyframes modal-in { from { opacity: 0; transform: translateY(8px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } }
.modal-detail-header { flex-shrink: 0; padding: var(--space-5) var(--space-6); border-bottom: 1px solid var(--color-border); display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-4); }
.modal-detail-header h3 { font-size: var(--text-xl); font-weight: var(--font-semibold); letter-spacing: -0.01em; margin-bottom: var(--space-1); }
.modal-detail-subtitle { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); color: var(--color-text-secondary); }
/* Lifecycle stepper - purely derived from status/QC state (see
   stepperHtml() in app.js), not a stored field. Dots connected by a
   line; done=filled+check, current=outlined+dark, upcoming=muted. */
.stepper-wrap { padding: var(--space-4) var(--space-6) 0; }
.stepper { display: flex; align-items: flex-start; }
.stepper-item { display: flex; flex-direction: column; align-items: center; gap: 6px; min-width: 72px; }
.stepper-dot { width: 26px; height: 26px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: var(--text-sm); font-weight: var(--font-semibold); border: 2px solid var(--color-border); color: var(--color-text-tertiary); background: var(--color-surface); flex-shrink: 0; }
.stepper-item.done .stepper-dot { background: var(--color-success); border-color: var(--color-success); color: #fff; }
/* Was --color-accent (blue) for all three properties (2026-07-21, palette-
   fix pass) - now solid black/near-black instead. Not amber: this dot's
   colored ring is exactly the "thin colored line on light canvas" shape
   RULE 2 restricts amber from, and this stepper repeats on every order
   detail view, which would spread amber far more than a restrained,
   occasional highlight. */
.stepper-item.current .stepper-dot { border-color: var(--color-text-primary); color: var(--color-text-primary); background: var(--color-surface); }
.stepper-item.upcoming .stepper-dot { }
.stepper-label { font-size: var(--text-xs); color: var(--color-text-tertiary); white-space: nowrap; }
.stepper-item.done .stepper-label, .stepper-item.current .stepper-label { color: var(--color-text-secondary); font-weight: var(--font-medium); }
.stepper-line { flex: 1; height: 2px; background: var(--color-border); margin-top: 13px; min-width: 16px; }
.stepper-note { font-size: var(--text-sm); color: var(--color-text-secondary); margin-top: var(--space-2); }
/* One-line "what this button does" text, sitting directly under a primary
   action - contextual guidance in the workflow itself, not a separate
   help page. */
.action-hint { font-size: var(--text-xs); color: var(--color-text-tertiary); margin-top: 4px; max-width: 220px; text-align: right; }
/* Left-aligned variant, for a hint sitting under a left-aligned control
   (e.g. the modal-detail-footer's Delete button, on the left; the QC
   form's Submit button, not in a footer at all) instead of the default
   right-aligned Complete-button case. Was `style="text-align:left"`
   repeated at 4 call sites. */
.action-hint.left { text-align: left; max-width: none; }

.modal-detail-nav { flex-shrink: 0; display: flex; gap: var(--space-5); padding: 0 var(--space-6); border-bottom: 1px solid var(--color-border); overflow-x: auto; }
.modal-detail-nav-link { padding: var(--space-3) 0; font-size: var(--text-sm); font-weight: var(--font-medium); color: var(--color-text-secondary); cursor: pointer; border-bottom: 2px solid transparent; white-space: nowrap; transition: color 0.15s, border-color 0.15s; }
.modal-detail-nav-link:hover { color: var(--color-text-primary); }
/* Was --color-accent (blue) for both text and underline (2026-07-21,
   palette-fix pass) - now solid black. Not amber: an underline is exactly
   the "thin line on light canvas" RULE 2 restricts amber from. */
.modal-detail-nav-link.active { color: var(--color-text-primary); border-bottom-color: var(--color-text-primary); }
/* Sunken, not white - so the white section cards inside read as genuinely
   distinct surfaces (rule: section identity), not one continuous sheet. */
.modal-detail-body { flex: 1; overflow-y: auto; padding: var(--space-6); background: var(--color-bg); }
/* space-between so the danger action (left) is never adjacent to the
   primary action (right) - separated by real distance, not just color. */
.modal-detail-footer { flex-shrink: 0; padding: var(--space-4) var(--space-6); border-top: 1px solid var(--color-border); display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); background: var(--color-surface); }
.modal-detail-footer-right { display: flex; gap: var(--space-2); }

/* Phase 5 (spec 4.5.2) - Production Order detail's new FULL PAGE container.
   Deliberately separate from .modal-detail-header/-body/-footer above,
   which are flex-shrink:0/flex:1 children of a fixed-height modal panel -
   wrong shape for a plain page in #main-content's normal document flow
   (flex:1 does nothing without a flex parent; overflow-y:auto would add an
   unwanted inner scrollbar instead of using the page's own scroll). The
   parts that don't need that distinction - .modal-detail-nav/-nav-link,
   .modal-detail-subtitle, .detail-section, the stepper - are reused
   completely unchanged, exactly as the spec asks ("the existing
   .modal-detail-nav tab styling is reused... unchanged visually"). */
.detail-page-header { display: flex; align-items: flex-start; gap: var(--space-4); padding-bottom: var(--space-5); margin-bottom: var(--space-5); border-bottom: 1px solid var(--color-border); }
.detail-page-back { flex-shrink: 0; margin-top: 1px; }
.detail-page-header h2 { font-size: var(--text-xl); font-weight: var(--font-semibold); letter-spacing: -0.01em; margin-bottom: var(--space-1); }
.detail-page-footer { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); padding-top: var(--space-5); margin-top: var(--space-6); border-top: 1px solid var(--color-border); }
.detail-page-footer-right { display: flex; gap: var(--space-2); align-items: center; }

/* Small inline bits inside the materials qty-edit rows - were one-off
   inline styles repeated at every row. */
.unit-suffix { color: var(--color-text-secondary); font-size: var(--text-sm); }
.std-qty-hint { color: var(--color-text-tertiary); font-size: var(--text-sm); }

/* Consequence details shown inside confirmAction() dialogs */
.confirm-details {
  background: var(--color-surface-sunken); border-radius: var(--radius-md);
  padding: var(--space-4); margin-bottom: var(--space-4); font-size: var(--text-sm); color: var(--color-text-secondary);
}
.confirm-details ul { padding-left: var(--space-4); margin-top: var(--space-2); }
/* The description paragraph inside confirmAction()/showZohoDeleteBlocked()
   dialogs - was `style="color:var(--color-text-secondary);margin-bottom:12px"`
   repeated verbatim at 5 call sites (2026-07-20 UI refactor pass), with the
   12px never actually expressed as the --space-3 token it happens to equal. */
.confirm-message { color: var(--color-text-secondary); margin-bottom: var(--space-3); }

/* A read-only value shown where an input would otherwise go (e.g. "Unit"
   next to an editable "Quantity", or a Zoho-sourced field the user can't
   edit) - height-matched to a real .form-group input via the same
   vertical padding, so a row mixing editable and read-only fields still
   lines up. Was `style="padding:9px 0;font-size:var(--text-base);
   color:var(--color-text-secondary)"` (or a slight variant) repeated
   verbatim at 7 call sites. */
.form-static-value { padding: 9px 0; font-size: var(--text-base); color: var(--color-text-secondary); }

/* ---------- Forms ---------- */
.form-group { margin-bottom: var(--space-4); }
.form-group label { display: block; font-size: var(--text-sm); font-weight: var(--font-medium); color: var(--color-text-secondary); margin-bottom: 6px; }
.form-group input, .form-group select, .form-group textarea, .form-control {
  width: 100%; padding: var(--control-padding-y-md) var(--control-padding-x-md); border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm); font-size: var(--text-base); font-family: inherit; line-height: 1.3;
  background: var(--color-surface); color: var(--color-text-primary);
  transition: border-color 0.15s, box-shadow 0.15s;
}
/* Was --color-accent (blue) border + accent-soft glow (2026-07-21,
   palette-fix pass). The border is now solid black - RULE 2 forbids amber
   as a thin border/line on a light surface, so the crisp edge itself stays
   monochrome - but the diffuse glow is --color-brand-amber-soft, per the
   user's own explicit allowance for form focus rings specifically ("black/
   near-black OR amber-soft"). This is every text/date/select input in the
   app - the highest-frequency remaining place blue used to appear. */
.form-group input:focus, .form-group select:focus, .form-group textarea:focus, .form-control:focus {
  outline: none; border-color: var(--color-text-primary); box-shadow: 0 0 0 3px var(--color-brand-amber-soft);
}
.form-row { display: flex; gap: var(--space-3); }
.form-row .form-group { flex: 1; }

/* Input widths matched to what they actually hold - never uniform.
   Quantity/date-adjacent numbers are small, a unit is tiny, cost amounts
   and batch numbers are medium, material/product names need real room. */
.field-tiny   { width: var(--field-w-tiny); flex: none; }
.field-small  { width: var(--field-w-small); flex: none; }
.field-medium { width: var(--field-w-medium); flex: none; }
.field-large  { width: var(--field-w-large); flex: none; }
.field-grow   { flex: 1; min-width: 160px; }

/* ---------- Editable materials grid (production order detail) ---------- */
/* Replaces the old one-row-per-material-with-its-own-Update-button pattern:
   a compact table where the Actual column is directly editable inline and
   ONE Save button at the section level batches every row's current value
   into the existing per-item PUT calls (client-side batching only - the
   API itself is unchanged, see saveConsumptionChanges()/saveOutputChanges()
   in app.js). */
.editable-grid td.editable-cell { padding: var(--space-2) var(--space-4); }
.editable-grid input[type="number"] {
  width: 100%; min-width: 0; padding: var(--space-2) var(--space-3);
  text-align: right; font-variant-numeric: tabular-nums;
}
/* Was --color-accent border + accent-soft fill (blue), marking an unsaved
   edit. Now a plain dark border + neutral-soft fill (2026-07-21, palette-
   fix pass) - this is purely informational ("you changed this, not yet
   saved"), not a highlight, and can apply to several cells in the same
   grid at once, so amber (a restrained, occasional accent) was avoided
   here specifically to not spread it across many cells simultaneously. */
.editable-grid input.is-dirty { border-color: var(--color-text-primary); background: var(--color-neutral-soft); }
.variance-pos  { color: var(--color-warning); font-weight: var(--font-medium); }
/* Was --color-info (blue) - now plain dark text (2026-07-21, palette-fix
   pass), distinct from the muted --color-text-tertiary zero-state below
   without introducing a new color. */
.variance-neg  { color: var(--color-text-primary); font-weight: var(--font-medium); }
.variance-zero { color: var(--color-text-tertiary); }

/* A visually real section, not just a text label between fields (the
   previous .form-section-label was a bare gray line with a margin - didn't
   read as a "group" to a user scanning the form). Used in the BOM and
   production-order creation forms; --space-4 gap below matches every other
   block-level spacing in the form so sections read as one rhythm, not a
   special case. */
.form-section {
  border: 1px solid var(--color-border); border-radius: var(--radius-md);
  padding: var(--space-4); margin-bottom: var(--space-4); background: var(--color-surface-sunken);
}
.form-section:last-of-type { margin-bottom: var(--space-4); }
.form-section-title {
  font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text-primary);
  margin-bottom: var(--space-3); display: flex; align-items: baseline; gap: var(--space-2);
}
/* Standalone now (2026-07-20 refactor), not just a .form-section-title
   descendant - it's used as a general "muted explanatory aside next to a
   label" (e.g. the BOM item row's Fixed-qty checkbox hint) wherever a
   field needs one, not only inside a section title. The old nested-only
   selector meant any other usage silently rendered with no hint styling
   at all - full label-sized text, not visually secondary. */
.form-section-hint { font-size: var(--text-xs); font-weight: var(--font-medium); color: var(--color-text-tertiary); }
.form-section-title.required-title::after { content: '*'; color: var(--color-danger); }

/* Operator-help-text-rewrite pass (2026-07-22): the one tooltip mechanism
   for every "what do I enter here" hint in the app, replacing the old
   always-visible .form-section-hint/.std-qty-hint explanation paragraphs
   and the native `title=` attribute (which never showed for keyboard-only
   users - a real accessibility gap, not just a style choice). A real
   <button> so it's focusable/activatable without extra tabindex/ARIA
   plumbing; the bubble is a sibling shown via :hover/:focus on the icon,
   not JS-positioned, so it can never get out of sync with the icon it
   belongs to. See public/operator-help-text-rewrite-report.txt. */
.help-tip { position: relative; display: inline-flex; vertical-align: middle; margin-left: 5px; }
.help-tip-icon {
  width: 15px; height: 15px; padding: 0; flex-shrink: 0;
  border: 1px solid var(--color-border-strong); border-radius: 50%;
  background: var(--color-surface); color: var(--color-text-tertiary);
  font-size: 10px; font-weight: var(--font-semibold); font-style: italic;
  font-family: Georgia, 'Times New Roman', serif; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: help;
}
.help-tip-icon:hover, .help-tip-icon:focus {
  border-color: var(--color-text-primary); color: var(--color-text-primary);
}
.help-tip-icon:focus { outline: 2px solid var(--color-text-primary); outline-offset: 2px; }
.help-tip-bubble {
  position: absolute; bottom: calc(100% + 7px); left: 50%; transform: translateX(-50%);
  width: max-content; max-width: 240px;
  background: var(--color-text-primary); color: var(--color-surface);
  font-size: var(--text-xs); font-weight: 400; text-align: left;
  padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm);
  box-shadow: 0 4px 16px rgba(0,0,0,0.2);
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 0.12s ease;
  z-index: 250;
}
.help-tip-bubble::after {
  content: ''; position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
  border: 5px solid transparent; border-top-color: var(--color-text-primary);
}
.help-tip-icon:hover + .help-tip-bubble, .help-tip-icon:focus + .help-tip-bubble {
  opacity: 1; visibility: visible;
}

/* Two columns on desktop; collapses to one column at the tablet breakpoint
   (see the responsive rules near the end of this file). */
.form-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 var(--space-3); }

.form-group-checkbox { display: flex; align-items: center; }
.form-group-checkbox label { display: flex; align-items: center; gap: var(--space-2); margin: 0; font-weight: var(--font-medium); color: var(--color-text-primary); font-size: var(--text-base); cursor: pointer; }
.form-group-checkbox input[type="checkbox"] { width: auto; }

/* Pair with the `required` HTML attribute - never one without the other,
   so "must fill this in" is both machine-enforced and visible before the
   user hits submit. Class-based (not :has()) so it works everywhere. */
.form-group.required > label::after { content: ' *'; color: var(--color-danger); }

.modal-actions { display: flex; gap: var(--space-2); justify-content: flex-end; margin-top: var(--space-6); padding-top: var(--space-4); border-top: 1px solid var(--color-border); }

/* White + bordered rather than sunken, since this now normally sits inside
   a .form-section (which is itself sunken) - needs to visually stand out
   from its container instead of blending into it. */
/* Grid, not flex-wrap (visual-polish pass, replacing the previous flex-wrap
   layout) - the old layout wrapped the Remove button onto its own line
   whenever a row's columns didn't all fit on one line (live-confirmed: in
   Batch Recipe mode, Material+Batch qty+Unit+Per-unit's combined width
   exceeded the row's available width, so the Remove button alone wrapped
   below - "Material | Qty | Unit" on one line, a lone "X" on the next).
   A grid with a fixed column template can't wrap a single column onto its
   own line the way flex-wrap can, so Remove now always stays in the row's
   last column regardless of mode. align-items:end bottom-aligns every
   column's control - including Remove, which (unlike its siblings) has no
   label above it, so it would otherwise sit high in a taller row. */
.bom-item-row { display: grid; grid-template-columns: minmax(200px, 1fr) var(--field-w-small) var(--field-w-tiny) auto; align-items: end; gap: var(--space-3); margin-bottom: var(--space-3); padding: var(--space-4); background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-md); }
/* Batch Recipe rows (addBomItemRow('batch')) add one extra column (Per
   unit) between Unit and Remove - the only structural difference between
   the two modes' rows. */
.bom-item-row.mode-batch { grid-template-columns: minmax(200px, 1fr) var(--field-w-small) var(--field-w-tiny) var(--field-w-small) auto; }
.bom-item-row:last-of-type { margin-bottom: var(--space-3); }
.bom-item-row .form-group { margin-bottom: 0; }
/* Grid items stretch to their column's width by default, which already
   matches field-grow/field-small/field-tiny's own explicit widths (set for
   this row's flex-based predecessor, still used unchanged elsewhere - e.g.
   .form-row - so left alone rather than forked into a grid-only variant). */
.bom-item-row .btn-danger { justify-self: start; }
/* Fixed-qty (Batch Recipe rows only) - still needs its own full-width line
   below the row's main columns (there's nowhere else for a checkbox+label
   to sit in a 5-column grid without crowding a numeric column), but reads
   as part of THIS row now rather than a visually detached sub-section: no
   divider border, tight spacing, smaller/muted label text (previously
   full-size body text behind a border-top divider, which read as its own
   block rather than a row option - the reasoning module's own live-tested
   `grid-column:1/-1` replaces the old flex-basis:100% for the same
   full-width-line effect under the new grid layout). */
.bom-item-row .row-secondary-line { grid-column: 1 / -1; margin-top: var(--space-1); padding-top: var(--space-1); border-top: none; }
.bom-item-row .row-secondary-line .form-group-checkbox label { font-size: var(--text-sm); font-weight: var(--font-medium); color: var(--color-text-secondary); }

/* New-BOM choice screen (Design B, bom-form-ux-redesign-proposal.txt;
   visual-polish pass on top of it) - two equal-size cards, side by side,
   sized and weighted as the first real decision in the flow rather than
   two small panels: bigger padding, an icon, a bolder title, a stronger
   hover/focus state. The whole card is the only click target - no inner
   "Use" button (removed in the polish pass; having both a clickable card
   AND a clickable button inside it was two controls doing the same thing).
   role="button"/tabindex/:focus-visible restore the keyboard/screen-reader
   affordance a real <button> gave for free. Stacks to one column at the
   same 900px breakpoint every other two-column form grid in this file
   already collapses at (.form-grid-2, .stats-grid, below). */
.bom-choice-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-5); margin-bottom: var(--space-5); }
.bom-choice-card {
  display: flex; flex-direction: column; align-items: flex-start; text-align: left;
  gap: var(--space-2); padding: var(--space-6); border: 1px solid var(--color-border);
  border-radius: var(--radius-lg); background: var(--color-surface); cursor: pointer;
  transition: box-shadow 0.15s, border-color 0.15s, transform 0.1s;
}
.bom-choice-card:hover { border-color: var(--color-text-primary); box-shadow: var(--shadow-md); transform: translateY(-1px); }
.bom-choice-card:active { transform: translateY(0); }
.bom-choice-card:focus-visible { outline: none; border-color: var(--color-text-primary); box-shadow: 0 0 0 3px var(--color-brand-amber-soft); }
.bom-choice-card-icon { width: 44px; height: 44px; border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; background: var(--color-neutral-soft); color: var(--color-text-primary); flex-shrink: 0; margin-bottom: var(--space-1); }
.bom-choice-card-icon svg { width: 22px; height: 22px; }
.bom-choice-card-title { font-size: var(--text-lg); font-weight: var(--font-semibold); letter-spacing: -0.01em; }
.bom-choice-card-badge-slot { min-height: 22px; } /* reserves the "Recommended" badge's row on both cards so titles/descriptions/CTAs stay aligned even on the card that has no badge */
.bom-choice-card-desc { font-size: var(--text-sm); color: var(--color-text-secondary); flex: 1; }
.bom-choice-card-cta { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text-primary); margin-top: var(--space-2); }

.bom-form-back { margin-bottom: var(--space-3); }
/* Extra breathing room for the BOM create form specifically (visual-polish
   pass) - scoped to .bom-form so it doesn't ripple into every other
   modal's .form-section (production/packaging order forms etc. share that
   base class and were not part of this pass). */
.bom-form .form-section { padding: var(--space-5); margin-bottom: var(--space-5); }
.bom-form .modal-actions { margin-top: var(--space-2); }
/* Recipe Size (Batch Recipe form only) - the first thing the operator
   should notice, ahead of Materials below it; bumped up from
   .form-section-title's normal --text-sm so it visually outranks the
   Materials heading instead of reading as equal weight. */
.form-section-title-lg { font-size: var(--text-md); }

/* Wrapping mini-forms (Add cost / Add waste / Submit QC on the production
   order detail modal) - a labeled, breathable alternative to a single
   cramped flex row with placeholder-only inputs. */
.inline-form { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: flex-end; padding: var(--space-3); background: var(--color-surface-sunken); border-radius: var(--radius-md); margin-bottom: var(--space-3); }
.inline-form .form-group { margin-bottom: 0; }

/* A filter/toolbar row living flush at the top of a .card (e.g. Sync
   Log's status/operation/batch filters + Apply/Clear/Retry buttons) -
   same flex+align-items:flex-end alignment trick as .inline-form (labeled
   fields and label-less buttons bottom-align without a magic margin-top
   hack - was `style="margin-top:20px"` on every button here before the
   2026-07-20 refactor), but flat against the card's own white surface
   instead of .inline-form's sunken inset box, since this sits directly
   inside a .card, not inside a form section. */
.filter-bar { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: flex-end; padding: var(--space-4); border-bottom: 1px solid var(--color-border); }
.filter-bar .form-group { margin-bottom: 0; }

.tag-list { display: flex; flex-wrap: wrap; gap: var(--space-2); }

/* A section that reads as a genuinely distinct card - border + its own
   surface color against .modal-detail-body's sunken background - not just
   a heading floating in a continuous scroll (rule: section identity).
   Works both inside the large detail modal and standalone (e.g. BOM
   detail's modal-wide panel), since it carries its own background/border
   regardless of what it sits on. */
.detail-section { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: var(--space-5); margin-bottom: var(--space-5); }
.detail-section:last-child { margin-bottom: 0; }
.detail-section-head { margin-bottom: var(--space-4); padding-bottom: var(--space-3); border-bottom: 1px solid var(--color-border); }
.detail-section h4 { font-size: var(--text-lg); font-weight: var(--font-semibold); color: var(--color-text-primary); }
.detail-section-desc { font-size: var(--text-sm); color: var(--color-text-secondary); margin-top: 2px; }
/* When a section has no head block (BOM detail's simpler read-only
   sections, sync-log entries), h4 still needs its own spacing. */
.detail-section > h4:first-child { margin-bottom: var(--space-3); }

/* Help page only. dl/dt/dd for the "what each page does" glossary and
   ol/li for numbered task steps - the global `* { margin:0; padding:0 }`
   reset strips the browser's own list-marker indent, so both need it back
   explicitly or numbers/labels render with nothing to hang off of. */
.help-topic-list dt { font-weight: var(--font-semibold); color: var(--color-text-primary); margin-top: var(--space-4); }
.help-topic-list dt:first-child { margin-top: 0; }
.help-topic-list dd { font-size: var(--text-sm); color: var(--color-text-secondary); margin-top: 2px; }
.help-task { margin-top: var(--space-5); }
.help-task:first-child { margin-top: 0; }
.help-task h4 { font-size: var(--text-base); font-weight: var(--font-semibold); margin-bottom: var(--space-2); }
.help-steps { padding-left: var(--space-5); display: flex; flex-direction: column; gap: var(--space-2); }
.help-steps li { font-size: var(--text-sm); color: var(--color-text-secondary); line-height: 1.5; }
.help-steps strong { color: var(--color-text-primary); font-weight: var(--font-semibold); }

/* Phase 6 (spec 4.11) - sticky left-hand section index + a client-side
   filter, replacing the previous one-continuous-scroll page. .help-content
   keeps the exact same stacked .card sections it always had (unchanged
   here); .help-nav is the new part. */
.help-layout { display: flex; gap: var(--space-6); align-items: flex-start; }
.help-nav { position: sticky; top: var(--space-4); width: 200px; flex-shrink: 0; display: flex; flex-direction: column; }
.help-nav-link { padding: var(--space-2) var(--space-3); font-size: var(--text-sm); font-weight: var(--font-medium); color: var(--color-text-secondary); cursor: pointer; border-radius: var(--radius-sm); border-left: 2px solid transparent; }
.help-nav-link:hover { background: var(--color-surface-sunken); color: var(--color-text-primary); }
.help-nav-link.active { color: var(--color-text-primary); background: var(--color-surface-sunken); border-left-color: var(--color-text-primary); font-weight: var(--font-semibold); }
.help-content { flex: 1; min-width: 0; }
.help-empty { padding: var(--space-3) 0; font-size: var(--text-sm); color: var(--color-text-tertiary); }

/* Same 900px tablet breakpoint every other narrow-viewport rule in this
   file already uses (see .sidebar/.topbar above) - the sticky sidebar
   becomes a horizontal sticky bar instead, matching that existing
   convention rather than inventing a new breakpoint value. */
@media (max-width: 900px) {
  .help-layout { flex-direction: column; }
  .help-nav {
    position: sticky; top: 0; width: 100%; flex-direction: row; align-items: center;
    gap: var(--space-2); overflow-x: auto; background: var(--color-bg);
    padding: var(--space-2) 0; z-index: 5; border-bottom: 1px solid var(--color-border);
  }
  .help-nav .form-group { margin-bottom: 0; flex-shrink: 0; width: 200px; }
  .help-nav-link { white-space: nowrap; border-left: none; border-bottom: 2px solid transparent; }
  .help-nav-link.active { border-left-color: transparent; border-bottom-color: var(--color-text-primary); }
}

.kv-row { display: flex; justify-content: space-between; align-items: center; padding: var(--space-2) 0; font-size: var(--text-base); border-bottom: 1px solid var(--color-border); gap: var(--space-3); }
.kv-row:last-child { border-bottom: none; }
.kv-row .k { color: var(--color-text-secondary); }
.kv-row > span:last-child { font-weight: var(--font-semibold); font-variant-numeric: tabular-nums; }

/* ---------- Zoho sync detail (shared renderSyncEntry component) ---------- */
.sync-entry { border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: var(--space-4); margin-bottom: var(--space-3); }
.sync-entry-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-2); gap: var(--space-3); }
.sync-entry-head .title { font-weight: var(--font-medium); }
.balance-flow { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-base); }
.balance-flow .arrow { color: var(--color-text-tertiary); }
.journal-table { width: 100%; font-size: var(--text-sm); margin-top: var(--space-2); border-collapse: collapse; }
.journal-table th { text-align: left; padding: 4px var(--space-2); color: var(--color-text-secondary); font-weight: var(--font-medium); }
.journal-table td { padding: 4px var(--space-2); border-top: 1px solid var(--color-border); }
.journal-table .debit { color: var(--color-danger); }
.journal-table .credit { color: var(--color-success); }
/* Was --color-accent (blue), no underline until hover. Now black with an
   always-on underline (2026-07-21, palette-fix pass) - per the explicit
   instruction that a link needs the underline to read as a link once color
   is no longer doing that job; not amber, since this links out to an
   external system (Zoho), not something brand-relevant. */
.zoho-link { display: inline-flex; align-items: center; gap: 4px; color: var(--color-text-primary); font-size: var(--text-sm); font-weight: var(--font-medium); text-decoration: underline; margin-top: var(--space-2); }
.sync-error-text { font-size: var(--text-sm); color: var(--color-danger); background: var(--color-danger-soft); border-radius: var(--radius-sm); padding: var(--space-2) var(--space-3); margin-top: var(--space-2); white-space: pre-wrap; word-break: break-word; }

/* ---------- Toast (replaces alert()) ---------- */
#toast-root { position: fixed; bottom: var(--space-6); right: var(--space-6); z-index: 200; display: flex; flex-direction: column; gap: var(--space-2); }
.toast {
  min-width: 260px; max-width: 380px; padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md); box-shadow: var(--shadow-md); font-size: var(--text-base);
  color: #fff; background: var(--color-text-primary); animation: toast-in 0.2s ease-out;
}
.toast.success { background: var(--color-success); }
.toast.error { background: var(--color-danger); }
/* .toast.info (background: --color-info, blue) removed (2026-07-21,
   palette-fix pass) rather than recolored - the base .toast rule above
   already defaults to var(--color-text-primary) (black), which is exactly
   the correct "informational/neutral" color this pass would have picked
   anyway, so the override was simply redundant once blue was off the
   table. The one real call site (toast('No changes to save.', 'info'))
   now falls through to that black default unchanged. */
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }

/* ---------- Dashboard live-view specifics ---------- */
.status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
.status-dot.live { background: var(--color-success); box-shadow: 0 0 0 3px var(--color-success-soft); animation: pulse 2s infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }

/* ---------- Responsive ---------- */
/* Previously zero @media queries existed at all (audit finding C1) - .main
   was the only overflow-x:auto ancestor, so any content wider than the
   remaining space (sidebar + padding eaten first) dragged the whole page
   header/stat-cards sideways along with it. Tables are now individually
   contained via .table-scroll (see the Tables section above) regardless of
   viewport width; the breakpoints below handle everything else that was
   sized for a full desktop width - the sidebar, two-column forms, and the
   order-detail drawer. */

/* Laptop: tighten outer spacing before anything needs to actually reflow. */
@media (max-width: 1180px) {
  .main { padding: var(--space-6) var(--space-6); }
  /* Live-verified regression (2026-07-21, credit-line addition): the
     credit line's extra width in .topbar-brand pushed the longest section
     titles ("Production orders," 17 chars) into visible ellipsis-clipping
     between this breakpoint and the existing 900px one, where they
     previously fit. The credit is expendable decoration - drop it here,
     before it can crowd the section title, which is real navigational
     information and stays visible down to 900px same as before. */
  .topbar-credit { display: none; }
}

/* Tablet: sidebar narrows (not a separate icon-only mode - that would need
   real interaction/QA to get right without leaving labels illegible), forms
   drop to one column, the order-detail drawer and wide modals take the
   full viewport width instead of a fixed desktop px value. */
@media (max-width: 900px) {
  .sidebar { width: 180px; }
  .sidebar-header h1 { font-size: var(--text-sm); }
  .nav-item { padding: var(--space-2) var(--space-3); font-size: var(--text-sm); }
  .nav-item .icon { width: 15px; height: 15px; }
  .main { padding: var(--space-4); }

  /* Top bar: drop the wordmark (the glyph alone still orients) and the
     section title before the search pill starts fighting the account/bell
     controls for space - same "narrow the chrome, don't hide its controls"
     approach the sidebar above already takes. */
  .topbar { gap: var(--space-2); padding: 0 var(--space-3); }
  .topbar-wordmark, .topbar-title { display: none; } /* .topbar-credit already hidden above 900px, at the 1180px breakpoint */
  .cmdk-trigger { width: auto; max-width: none; }
  .cmdk-trigger-label { display: none; }

  .form-grid-2 { grid-template-columns: 1fr; }
  .form-row { flex-direction: column; gap: 0; }

  .modal-panel.modal-wide, .modal-panel.modal-detail { width: 100%; max-width: 96vw; }
  .modal-detail-header, .modal-detail-nav, .modal-detail-body, .modal-detail-footer { padding-left: var(--space-4); padding-right: var(--space-4); }

  .stats-grid { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }

  .bom-choice-grid { grid-template-columns: 1fr; }
}

/* ---------- Login screen ---------- */
/* Full-bleed dark chrome background (2026-07-21 vision-redesign Phase 1,
   spec Part 4.1) - the first surface any user or evaluator ever sees is now
   the brand's real dark/amber identity, not a placeholder-gray page. The
   login card itself stays light/white (unchanged below) so the form remains
   high-contrast and unambiguous to interact with - the win here is the
   *background* carrying identity, not the form (a dark-on-dark login form
   would be a real usability regression, per the spec's own reasoning).
   flex-direction:column (was row-only, single centered child) now stacks
   the brand block above the card. */
.login-screen {
  min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--space-6); background: var(--color-chrome-bg); padding: var(--space-5);
}
/* Brand block above the card - the one screen with no functional chrome
   competing for attention, so amber gets to be large and prominent here
   (RULE 3), unlike its restrained canvas-only use elsewhere. */
.login-brand { display: flex; flex-direction: column; align-items: center; gap: var(--space-3); }
/* The real AWT chevron mark (2026-07-21, logo pass) - largest placement of
   the three (56px vs 28px/22px in the top bar/sidebar), per this being
   "the one screen with no functional chrome competing for attention"
   (comment above, unchanged reasoning - was true of the amber-box
   placeholder and remains true of the real mark). */
.login-glyph { width: 56px; height: 56px; flex-shrink: 0; }
.login-glyph svg { width: 100%; height: 100%; display: block; }
.login-wordmark { color: var(--color-brand-amber); font-size: var(--text-lg); font-weight: var(--font-semibold); letter-spacing: -0.01em; }
.login-card {
  width: 360px; max-width: 100%; background: var(--color-surface);
  border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md); padding: var(--space-8) var(--space-6);
}
/* Was "AWT Manufacturing" (the wordmark, now promoted to .login-brand above
   the card) - the card's own heading is now the actual task instruction,
   same role docs/CODING_STANDARDS' "Sign in to continue" subtitle used to
   play, just promoted to the card's h1 since the wordmark no longer needs
   the slot. */
.login-card h1 { font-size: var(--text-lg); font-weight: var(--font-semibold); letter-spacing: -0.01em; margin-bottom: var(--space-5); text-align: center; }
.login-error {
  background: var(--color-danger-soft); color: var(--color-danger); border-radius: var(--radius-sm);
  padding: var(--space-3); font-size: var(--text-sm); margin-bottom: var(--space-4); display: none;
}
.login-error.visible { display: block; }
.login-card .btn-primary { width: 100%; justify-content: center; margin-top: var(--space-2); }
/* The color-specific overrides that used to live here (amber background/
   hover/disabled/spinner) are gone (2026-07-21, palette-fix pass) - the
   base .btn-primary rule is amber everywhere now, so they became an exact,
   dead duplicate the moment that changed. This layout-only rule (width/
   justify/margin) is the one part of the old login-specific block that
   still does something the base rule doesn't. */

/* Role affordances (2026-07-24). Every DELETE under /api and the bulk sync
   retry are admin-only server-side; an operator used to be shown those buttons
   and got a 403 on click. app.js stamps body[data-role] from GET /api/auth/me
   and this hides them, so a control rendered at any time is covered without a
   re-render hook. Presentation only - the server check is what enforces it.
   Written as :not([data-role="admin"]) rather than [data-role="operator"] so
   the controls are hidden before boot has set the attribute at all, and stay
   hidden for any role value a future build might introduce. */
body:not([data-role="admin"]) .admin-only { display: none !important; }

/* Collapsible optional form sections (<details>/<summary>). Used for the
   Assignment block on both create forms, which is three optional fields
   sitting between the required ones. Native <details> is deliberate over a
   JS toggle: keyboard and screen-reader behaviour come for free, and the
   section still submits its fields normally while collapsed. */
.form-section-collapsible > summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.form-section-collapsible > summary::-webkit-details-marker { display: none; }
.form-section-collapsible > summary::before {
  content: '';
  width: 0; height: 0;
  border-left: 5px solid currentColor;
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  opacity: 0.55;
  transition: transform 0.15s ease;
  flex: none;
}
.form-section-collapsible[open] > summary::before { transform: rotate(90deg); }
.form-section-collapsible > summary:hover::before { opacity: 1; }

/* Read-only assistant widget (P4 MVP). Floating so it doesn't consume layout
   space on any page; the whole thing is absent from the DOM when the feature
   has no API key configured. */
.assistant-fab {
  position: fixed; right: var(--space-4); bottom: var(--space-4);
  width: 44px; height: 44px; border-radius: 50%;
  border: 1px solid var(--border); background: var(--surface); color: var(--text);
  font-size: var(--text-lg); font-weight: 600; cursor: pointer;
  box-shadow: 0 2px 8px rgba(0,0,0,0.18); z-index: 40;
}
.assistant-fab:hover { background: var(--surface-hover); }
.assistant-panel {
  position: fixed; right: var(--space-4); bottom: calc(var(--space-4) + 56px);
  width: min(380px, calc(100vw - var(--space-6)));
  max-height: min(520px, calc(100vh - 140px));
  display: flex; flex-direction: column;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: 0 8px 28px rgba(0,0,0,0.22);
  z-index: 41;
}
.assistant-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--space-3); border-bottom: 1px solid var(--border);
}
.assistant-note {
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-sm); color: var(--text-muted);
  border-bottom: 1px solid var(--border);
}
.assistant-log {
  flex: 1; overflow-y: auto; padding: var(--space-3);
  display: flex; flex-direction: column; gap: var(--space-2);
  font-size: var(--text-sm); min-height: 80px;
}
.assistant-msg { padding: var(--space-2); border-radius: var(--radius-sm); white-space: pre-wrap; }
.assistant-msg-you { background: var(--surface-hover); align-self: flex-end; max-width: 85%; }
.assistant-msg-bot { background: transparent; border: 1px solid var(--border); }
.assistant-form { display: flex; gap: var(--space-2); padding: var(--space-3); border-top: 1px solid var(--border); }
.assistant-form input { flex: 1; min-width: 0; }

/* ---- Fast Production (public/fast-production.js) ------------------------
   Additive only: every selector is fp- prefixed, so no existing screen can be
   restyled by anything here. */
/* Matches .detail-section-desc (line 1118) exactly - the app's own subtitle
   under a section heading. The previous -4px top margin pulled it flush against
   the heading; nothing else here is a new value: the bottom gap is --space-4,
   the same block rhythm .form-group already uses. */
.fp-sub { font-size: var(--text-sm); color: var(--color-text-secondary); margin-top: 2px; margin-bottom: var(--space-4); }
.fp-qty { display: flex; }
.fp-qty input { flex: 1; border-top-right-radius: 0; border-bottom-right-radius: 0; }
.fp-unit { flex: none; display: grid; place-items: center; padding: 0 var(--space-4); min-width: 56px; background: var(--color-surface-sunken); border: 1px solid var(--color-border-strong); border-left: none; border-radius: 0 var(--radius-sm) var(--radius-sm) 0; font-weight: var(--font-medium); color: var(--color-text-secondary); }
.fp-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; margin-top: var(--space-5); }
.fp-wide { flex: 1; min-width: 220px; }
.fp-recipe { border: 1px solid var(--color-border); border-radius: var(--radius-md); overflow: hidden; margin: 0 0 var(--space-4); }
.fp-recipe-head { background: var(--color-surface-sunken); padding: var(--space-2) var(--space-4); font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text-secondary); }
.fp-recipe table { width: 100%; border-collapse: collapse; }
.fp-recipe td { padding: var(--space-3) var(--space-4); border-top: 1px solid var(--color-border); }
.fp-recipe td.fp-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.fp-recipe tr.fp-short td { background: #fdeceb; }
.fp-remaining { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-4); padding: var(--space-4); border-radius: var(--radius-md); background: var(--color-surface-sunken); font-weight: var(--font-semibold); margin-top: var(--space-4); }
.fp-remaining.over { background: #fdeceb; color: #b42318; }
.fp-remaining strong { font-variant-numeric: tabular-nums; font-size: 18px; white-space: nowrap; }
.fp-details { margin: 0 0 var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-md); }
.fp-details > summary { padding: var(--space-3) var(--space-4); cursor: pointer; font-weight: var(--font-medium); color: var(--color-text-secondary); }
.fp-details[open] > summary { border-bottom: 1px solid var(--border, #d8dbe0); }
.fp-details-body { padding: var(--space-4); }
.fp-done { padding: var(--space-4) var(--space-5); background: var(--color-surface-sunken); }
.fp-done h3 { margin: 0; font-size: 15px; font-weight: 600; color: var(--text-muted, #6b7280); }
.fp-tick { color: #1a7f4b; font-weight: 700; margin-right: 8px; }

/* A one-line hint under a field. Additive: introduced with the Planned quantity
   simplification, where a dismissible three-line banner was replaced by a single
   sentence (the full text still lives in that field's ? tooltip). Sized and
   coloured from the same tokens as .detail-section-desc so it reads as the app's
   own voice rather than a new one. */
.form-hint { font-size: var(--text-sm); color: var(--color-text-secondary); margin-top: 6px; }
