/* Custom ERP Stylesheet */

/* Stage 40.4: the Google Fonts @import that used to sit here has been
   removed, and it was the single most expensive thing on the critical path.
   A CSS @import is discovered only after styles.css itself has downloaded and
   started parsing, so it cost three chained round trips before first paint -
   styles.css, then fonts.googleapis.com's CSS, then the woff2 from
   fonts.gstatic.com - each one blocking, each one to a third party.

   The replacement is the platform's own UI font, which is what a premium
   native app actually uses: San Francisco on Apple devices, Segoe UI Variable
   on Windows 11, Roboto on Android. Zero bytes, zero requests, no FOUT, and
   text renders in the first paint instead of after it. It also let the
   Content-Security-Policy drop its two Google Fonts exceptions
   (internal/server/middleware.go), so the app now loads no third-party
   resource of any kind. */

:root {
  --primary-color: #44546a;
  --primary-hover: #34435a;
  --primary-light: #eef1f4;
  --bg-color: #f8fafc;
  --panel-bg: #ffffff;
  --surface-subtle: #f1f5f9;
  --text-main: #0f172a;
  --text-muted: #64748b;
  --border-color: #e2e8f0;
  --border-hover: #cbd5e1;
  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.02);
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
  --shadow-lg: 0 10px 15px -3px rgba(15, 23, 42, 0.08), 0 4px 6px -2px rgba(15, 23, 42, 0.04);
  /* The platform UI font, best-first. -apple-system/BlinkMacSystemFont resolve
     to San Francisco; "Segoe UI Variable Text" is Windows 11's refreshed UI
     face (Segoe UI behind it for Windows 10). Inter is kept last-but-one so a
     machine that happens to have it installed still uses it. */
  --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text", "Segoe UI", Roboto, "Helvetica Neue", Inter, sans-serif;
  /* Tabular figures for money columns, so digits line up down a column. */
  --font-numeric: "SF Mono", ui-monospace, "Cascadia Mono", Consolas, monospace;
  --sidebar-width: 270px;
  --transition-speed: 0.25s;
  /* Interaction feedback (hover, press) has to land inside the ~100ms a human
     reads as instant. --transition-speed is the *layout* duration - sidebar
     width, panel slides, overlays - where 250ms reads as deliberate. Driving
     hover states off the layout duration is why the UI felt like it was
     lagging a beat behind the cursor, so the hot paths below use these. */
  --transition-fast: 0.11s;
  --transition-med: 0.16s;
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);

  /* Semantic status + on-surface colors (Stage 28.5). Every status color in
     the sheet routes through these instead of a literal hex, so a theme only
     ever has to override tokens - see the "no literal colors below the token
     blocks" rule documented above the dark block. `-color` is foreground on a
     normal panel, `-strong` is foreground on the matching `-soft-bg` tint. */
  --on-primary: #ffffff;
  --text-placeholder: #cbd5e1;
  --danger-color: #ef4444;
  --danger-hover: #dc2626;
  --danger-strong: #b91c1c;
  --danger-soft-bg: #fef2f2;
  --danger-soft-border: #fecaca;
  --warning-color: #d97706;
  /* -strong is foreground ON --warning-soft-bg, so it cannot be the same
     value as -color: #d97706 on #fef3c7 measures 2.86:1, well under the
     4.5:1 AA minimum for normal text (WCAG SC 1.4.3). #92400e is 6.37:1 and
     still reads as the same amber. Caught by
     internal/server/frontend_accessibility_test.go, not by eye. */
  --warning-strong: #92400e;
  --warning-soft-bg: #fef3c7;
  --warning-soft-border: #fde68a;
  --success-color: #059669;
  --success-strong: #065f46;
  --success-soft-bg: #d1fae5;
}

/* Dark theme tokens (Stage 28.2). The token block above is the single source
   of color for the whole app, so re-theming is purely a matter of overriding
   these variables - no component rule changes. Applied when the user explicitly
   picks Dark (:root[data-theme="dark"]) OR leaves it on System and the OS
   prefers dark. An explicit Light choice ([data-theme="light"]) always wins over
   the OS preference. The two blocks below hold the same values (one for the
   explicit choice, one for the System-follows-OS case) - kept in sync by hand.

   RULE (Stage 28.5, after a batch of unreadable-in-dark text was found in the
   confirm dialog, badges and <pre> blocks): no rule below these blocks may use
   a literal color for text, background or border. Always add/reuse a token
   here and reference it with var(). The only sanctioned exceptions are the
   print-only .receipt / .sticker-label rules (always ink-on-paper) and
   translucent rgba() scrims/glows, which read correctly on either theme. */
:root[data-theme="dark"] {
  --primary-color: #6b82a8;
  --primary-hover: #7f96bd;
  --primary-light: #26324a;
  --bg-color: #0f172a;
  --panel-bg: #1a2436;
  --surface-subtle: #212d42;
  --text-main: #e6ebf2;
  --text-muted: #94a3b8;
  --border-color: #334155;
  --border-hover: #475569;
  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.30);
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.40), 0 2px 4px -1px rgba(0, 0, 0, 0.30);
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.50), 0 4px 6px -2px rgba(0, 0, 0, 0.40);
  /* Dark themes lift the accent so it reads against a dark panel, which makes
     white-on-accent fail: #ffffff on #6b82a8 is 3.90:1. The fix is the
     standard on-colour flip - dark text on a light accent - rather than
     darkening the accent, which would have cost it its contrast against
     --panel-bg and made primary buttons recede into the page. 4.58:1. */
  --on-primary: #0f172a;
  --text-placeholder: #64748b;
  --danger-color: #f87171;
  --danger-hover: #ef4444;
  --danger-strong: #fca5a5;
  --danger-soft-bg: #3b1f22;
  --danger-soft-border: #7f2d2d;
  --warning-color: #fbbf24;
  --warning-strong: #fcd34d;
  --warning-soft-bg: #3a2c12;
  --warning-soft-border: #7c5c11;
  --success-color: #34d399;
  --success-strong: #6ee7b7;
  --success-soft-bg: #12352c;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --primary-color: #6b82a8;
    --primary-hover: #7f96bd;
    --primary-light: #26324a;
    --bg-color: #0f172a;
    --panel-bg: #1a2436;
    --surface-subtle: #212d42;
    --text-main: #e6ebf2;
    --text-muted: #94a3b8;
    --border-color: #334155;
    --border-hover: #475569;
    --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.30);
    --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.40), 0 2px 4px -1px rgba(0, 0, 0, 0.30);
    --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.50), 0 4px 6px -2px rgba(0, 0, 0, 0.40);
    /* Same on-colour flip as the explicit dark block above, for the same
       measured reason - white on this accent is 3.90:1. The two dark blocks
       must agree; a token that is correct in one and not the other is exactly
       the drift the token-block rule exists to prevent. */
    --on-primary: #0f172a;
    --text-placeholder: #64748b;
    --danger-color: #f87171;
    --danger-hover: #ef4444;
    --danger-strong: #fca5a5;
    --danger-soft-bg: #3b1f22;
    --danger-soft-border: #7f2d2d;
    --warning-color: #fbbf24;
    --warning-strong: #fcd34d;
    --warning-soft-bg: #3a2c12;
    --warning-soft-border: #7c5c11;
    --success-color: #34d399;
    --success-strong: #6ee7b7;
    --success-soft-bg: #12352c;
  }
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: var(--font-family);
  background-color: var(--bg-color);
  color: var(--text-main);
  overflow: hidden;
  height: 100vh;
  display: flex;
}

/* App Container Layout */
.app-container {
  flex: 1;
  display: flex;
  width: 100%;
  height: 100vh;
  min-height: 0;
  min-width: 0;
  overflow: hidden;
}

/* Sidebar Navigation */
.sidebar {
  width: var(--sidebar-width);
  background-color: var(--panel-bg);
  border-right: 1px solid var(--border-color);
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  /* The flyouts are fixed-position descendants. Keep the sidebar's stacking
     layer above sticky table headers so an open menu remains fully visible and
     clickable over every table view. */
  z-index: 40;
  transition: width var(--transition-speed) ease;
  position: sticky;
  top: 0;
  height: 100vh;
}

.sidebar-header {
  padding: 20px;
  display: flex;
  align-items: center;
  gap: 12px;
  border-bottom: 1px solid var(--border-color);
}

.sidebar-logo {
  font-weight: 700;
  font-size: 20px;
  color: var(--text-main);
  letter-spacing: -0.5px;
}

/* Brand badge (sidebar header + login screen) */
.brand-badge {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 8px;
  background-color: var(--primary-color);
  color: var(--on-primary);
  flex-shrink: 0;
}

.brand-badge svg {
  width: 19px;
  height: 19px;
}

.sidebar-menu {
  flex: 1;
  overflow-y: auto;
  padding: 16px 12px;
  list-style: none;
}

.menu-item-container {
  margin-bottom: 4px;
}

/* 22.6: role-filtered sidebar - hidden rather than removed so re-running
   applySidebarPermissions() after a permission change doesn't need to
   rebuild the DOM. */
.menu-item-container.perm-hidden,
.menu-flyout > li.perm-hidden {
  display: none;
}

/* Stage 27: module-entitlement-filtered sidebar - same hidden-not-removed
   approach as .perm-hidden above, kept as a separate class (rather than
   reusing .perm-hidden) so applySidebarPermissions() and
   applyModuleEntitlements() can each toggle their own condition
   independently without one overwriting the other's hide decision. */
.menu-item-container.module-hidden,
.menu-flyout > li.module-hidden {
  display: none;
}

.menu-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  color: var(--text-muted);
  text-decoration: none;
  font-weight: 500;
  font-size: 14px;
  border-radius: 7px;
  cursor: pointer;
  /* Named properties, not `all`: `all` also transitions padding, border-width
     and font-weight, so the .active rule below animated the row's geometry on
     every navigation instead of just its colour. */
  transition: background-color var(--transition-fast) ease,
              color var(--transition-fast) ease,
              border-left-color var(--transition-fast) ease;
  border-left: 3px solid transparent;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

.menu-item:hover {
  background-color: var(--bg-color);
  color: var(--text-main);
}

/* Press feedback. Without it a click on a row that navigates somewhere slow
   looks identical to a click that missed, which is what makes people click
   again - and a second click on a nav row is never what they wanted. */
.menu-item:active {
  background-color: var(--surface-subtle);
}

.menu-item.active {
  background-color: var(--primary-light);
  color: var(--primary-color);
  border-left-color: var(--primary-color);
  font-weight: 600;
}

.menu-item svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

.submenu-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 16px;
  color: var(--text-muted);
  text-decoration: none;
  font-weight: 500;
  font-size: 13.5px;
  border-radius: 6px;
  cursor: pointer;
  transition: background-color var(--transition-fast) ease,
              color var(--transition-fast) ease;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

.submenu-item:hover {
  color: var(--text-main);
  background-color: var(--surface-subtle);
}

.submenu-item:active {
  background-color: var(--border-color);
}

.submenu-item.active {
  color: var(--primary-color);
  background-color: var(--primary-light);
}

.menu-item-arrow {
  margin-left: auto;
  transition: transform var(--transition-med) var(--ease-out);
}

/* Module hover/flyout menu (Stage 20 nav redesign): the left sidebar shows
   only module-level entries; each module's actual screens live in a flyout
   that appears beside it on hover (or click, for keyboard/touch users -
   see setupModuleFlyouts() in app.js). position:fixed with JS-computed
   top/left (not CSS position:absolute) deliberately, so the flyout is never
   clipped by .sidebar-menu's own overflow-y:auto scroll container. */
.menu-item-group {
  justify-content: space-between;
}

.flyout-arrow {
  transform: rotate(0deg);
  transition: transform var(--transition-med) var(--ease-out);
}

.has-flyout.flyout-open .flyout-arrow {
  transform: rotate(90deg);
}

.menu-flyout {
  position: fixed;
  display: none;
  list-style: none;
  min-width: 210px;
  /* max-height is set per-instance by openFlyout() in app.js, based on the
     trigger's actual vertical position - a flat viewport-height value here
     would still let a flyout anchored low in the sidebar run off-screen. */
  overflow-y: auto;
  background-color: var(--panel-bg);
  border: 1px solid var(--border-color);
  border-radius: 8px;
  box-shadow: 0 10px 30px rgba(15, 23, 42, 0.16);
  padding: 6px;
  z-index: 100;
}

/* The panel used to appear by flipping display:none -> block with no motion at
   all, while everything around it moved at 250ms. A short slide in the
   direction it comes from reads as the menu belonging to the row you're on;
   closing stays instant, because animating a menu out while the pointer is
   already reaching for the next one is exactly the kind of lag being fixed
   here. transform/opacity only, so it never affects the scrollHeight
   openFlyout() measures right after adding this class. */
.menu-flyout.open {
  display: block;
  animation: flyout-in var(--transition-med) var(--ease-out);
}

@keyframes flyout-in {
  from { opacity: 0; transform: translateX(-6px); }
  to   { opacity: 1; transform: translateX(0); }
}

/* Invisible hover bridge across the gap between a module row and its flyout
   (Stage 28.5). Pure hit area - never painted; sized/positioned by
   openFlyout() in app.js. Sits just under the flyout's own z-index so it can
   never cover a menu item, and only exists while a flyout is open. */
.menu-flyout-bridge {
  position: fixed;
  display: none;
  background: transparent;
  z-index: 99;
}

.menu-flyout .menu-item,
.menu-flyout .submenu-item {
  border-left: none;
  white-space: nowrap;
}

.sidebar-footer {
  padding: 12px;
  border-top: 1px solid var(--border-color);
}

.account-menu {
  position: relative;
}

.account-trigger {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px;
  border: none;
  background: transparent;
  border-radius: 8px;
  cursor: pointer;
  text-align: left;
  font-family: var(--font-family);
  /* A <button> inherits the UA default button colour, not the page text
     colour, so without this the username inside rendered pure black - fine
     on white, but 1.35:1 against the dark-mode sidebar, i.e. unreadable.
     .user-role escaped it only because it sets its own colour. */
  color: var(--text-main);
  transition: background-color var(--transition-fast) ease;
}

.account-trigger:hover,
.account-menu.open .account-trigger {
  background-color: var(--bg-color);
}

.account-trigger .user-info {
  flex: 1;
  min-width: 0;
}

.account-trigger .user-name {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.account-trigger-chevron {
  color: var(--text-muted);
  flex-shrink: 0;
  transition: transform var(--transition-med) ease;
}

.account-menu.open .account-trigger-chevron {
  transform: rotate(180deg);
}

.account-popover {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 0;
  right: 0;
  background-color: var(--panel-bg);
  border: 1px solid var(--border-color);
  border-radius: 8px;
  box-shadow: var(--shadow-lg);
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  z-index: 30;
}

.account-popover-header {
  display: flex;
  flex-direction: column;
  padding: 8px 10px 10px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--border-color);
}

.account-popover-name {
  font-weight: 600;
  font-size: 13.5px;
  color: var(--text-main);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.account-popover-email {
  font-size: 12px;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.account-popover-item {
  display: block;
  width: 100%;
  padding: 9px 10px;
  border: none;
  background: transparent;
  border-radius: 6px;
  font-family: var(--font-family);
  font-size: 13.5px;
  font-weight: 500;
  color: var(--text-main);
  text-align: left;
  cursor: pointer;
  transition: background-color var(--transition-fast) ease;
}

.account-popover-item:hover {
  background-color: var(--bg-color);
}

.account-popover-item-danger {
  color: var(--danger-color);
}

.account-popover-item-danger:hover {
  background-color: var(--danger-soft-bg);
}

/* Theme selector (Stage 28.2) - a light/dark/system segmented control in the
   account popover. Built from the existing token vocabulary so it themes itself. */
.account-popover-theme {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 6px 10px 10px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--border-color);
}

.account-popover-theme-label {
  font-size: 12.5px;
  font-weight: 500;
  color: var(--text-muted);
}

.theme-seg {
  display: inline-flex;
  border: 1px solid var(--border-color);
  border-radius: 7px;
  overflow: hidden;
  background: var(--bg-color);
}

.theme-seg-btn {
  border: none;
  background: transparent;
  font-family: var(--font-family);
  font-size: 12px;
  font-weight: 500;
  color: var(--text-muted);
  padding: 5px 9px;
  cursor: pointer;
  transition: background-color var(--transition-fast) ease, color var(--transition-fast) ease;
}

.theme-seg-btn + .theme-seg-btn {
  border-left: 1px solid var(--border-color);
}

.theme-seg-btn:hover {
  color: var(--text-main);
}

.theme-seg-btn.active {
  background: var(--primary-color);
  color: var(--on-primary);
}

.signout-overlay {
  position: fixed;
  inset: 0;
  background-color: var(--bg-color);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 200;
}

.signout-overlay-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  color: var(--text-muted);
  font-size: 14px;
}

.signout-spinner {
  width: 28px;
  height: 28px;
  border: 3px solid var(--border-color);
  border-top-color: var(--primary-color);
  border-radius: 50%;
  animation: signoutSpin 0.7s linear infinite;
}

@keyframes signoutSpin {
  to { transform: rotate(360deg); }
}

.avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background-color: var(--primary-light);
  color: var(--primary-color);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 14px;
}

.user-info {
  display: flex;
  flex-direction: column;
}

.user-name {
  font-weight: 600;
  font-size: 13px;
}

.user-role {
  font-size: 11px;
  color: var(--text-muted);
}

/* Main Content Area */
.main-content {
  flex: 1;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow: hidden;
}

/* Header */
.app-header {
  height: 70px;
  background-color: var(--panel-bg);
  border-bottom: 1px solid var(--border-color);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 32px;
  flex-shrink: 0;
  position: sticky;
  top: 0;
  z-index: 20;
}

.header-search {
  display: flex;
  align-items: center;
  gap: 8px;
  background-color: var(--surface-subtle);
  border: 1px solid transparent;
  padding: 8px 16px;
  border-radius: 6px;
  width: 300px;
  transition: background-color var(--transition-fast) ease, border-color var(--transition-fast) ease, box-shadow var(--transition-fast) ease;
}

.header-search:focus-within {
  background-color: var(--panel-bg);
  border-color: var(--primary-color);
  box-shadow: 0 0 0 3px rgba(68, 84, 106, 0.15);
}

.header-search input {
  border: none;
  background: transparent;
  outline: none;
  font-family: var(--font-family);
  font-size: 14px;
  color: var(--text-main);
  width: 100%;
}

.header-search input::placeholder {
  color: var(--text-muted);
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 16px;
}

/* Page Containers */
.page-container {
  flex: 1;
  min-height: 0;
  padding: 24px;
  overflow-y: auto;
  overflow-x: hidden;
  overflow-anchor: none;
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.page-title-section {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.page-title {
  font-size: 24px;
  font-weight: 700;
  letter-spacing: -0.5px;
}

.page-subtitle {
  font-size: 14px;
  color: var(--text-muted);
}

/* Buttons */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border-radius: 6px;
  font-family: var(--font-family);
  font-size: 14px;
  font-weight: 550;
  cursor: pointer;
  transition: background-color var(--transition-fast) ease,
              border-color var(--transition-fast) ease,
              box-shadow var(--transition-fast) ease,
              color var(--transition-fast) ease,
              transform var(--transition-fast) var(--ease-out);
  border: 1px solid transparent;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

/* Press feedback on every button variant at once. A button that looks the
   same the instant it is pressed as it did before reads as "didn't register",
   and the user clicks again - which on a submit is a duplicate request, not a
   cosmetic problem. */
.btn:active:not(:disabled) {
  transform: translateY(1px);
}

.btn-primary {
  background-color: var(--primary-color);
  color: var(--on-primary);
}

.btn-primary:hover {
  background-color: var(--primary-hover);
  box-shadow: var(--shadow-sm);
}

.btn-secondary {
  background-color: var(--surface-subtle);
  color: var(--text-main);
  border-color: var(--border-color);
}

.btn-secondary:hover {
  background-color: var(--border-color);
}

.btn-outline {
  background-color: transparent;
  border-color: var(--border-color);
  color: var(--text-main);
}

.btn-outline:hover {
  background-color: var(--bg-color);
  border-color: var(--border-hover);
}

/* Icon-only button (e.g. the header help button): .btn's own padding/gap are
   sized for an icon+label pair like Sync next to it, which stretches a
   single icon into an off-centre rectangle. Equal padding on all sides
   keeps the same square footprint and border style, just centred. */
.btn-icon {
  padding: 8px;
  gap: 0;
}

.btn-danger {
  background-color: var(--danger-color);
  color: var(--on-primary);
}

.btn-danger:hover {
  background-color: var(--danger-hover);
}

/* Dashboard Stats Row */
.dashboard-stats-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 16px;
  margin-bottom: 24px;
}

.stat-card {
  background-color: var(--panel-bg);
  border: 1px solid var(--border-color);
  border-radius: 8px;
  padding: 20px;
  display: flex;
  align-items: center;
  gap: 16px;
  box-shadow: var(--shadow-sm);
}

.stat-info {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.stat-val {
  font-size: 24px;
  font-weight: 700;
  color: var(--text-main);
}

.stat-label {
  font-size: 13px;
  color: var(--text-muted);
}

/* A .stat-card only becomes interactive when JS wires it up via
   makeClickable(), which sets role="button" - scoping the focus/hover state
   to that attribute keeps every purely-informational stat-card (the large
   majority) untouched. */
.stat-card[role="button"]:hover,
.stat-card[role="button"]:focus-visible {
  border-color: var(--primary-color);
  box-shadow: 0 0 0 3px rgba(68, 84, 106, 0.15);
}

/* box-shadow is not repainted under forced-colors (Windows High Contrast) -
   verified live: a plain <div role="button"> (unlike a real <button>, which
   gets its own UA-level forced-colors focus treatment) then shows no visible
   difference at all between focused and unfocused. An explicit outline is the
   one focus-indicator property forced-colors mode does remap to a system
   colour, so it stays the real fallback here rather than only cosmetic. */
.stat-card[role="button"]:focus-visible {
  outline: 2px solid var(--primary-color);
  outline-offset: 1px;
}

/* Glowing Pulsing status dot */
.pulse-dot {
  width: 10px;
  height: 10px;
  background-color: var(--success-color);
  border-radius: 50%;
  display: inline-block;
  box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4);
  animation: pulseAnimation 2s infinite;
}

@keyframes pulseAnimation {
  0% {
    transform: scale(0.95);
    box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
  }
  70% {
    transform: scale(1);
    box-shadow: 0 0 0 8px rgba(16, 185, 129, 0);
  }
  100% {
    transform: scale(0.95);
    box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
  }
}

/* .dashboard-grid / .dashboard-card / .card-content were the retired Dashboard
   landing screen's tile grid and were removed with it (2026-08-01).
   .dashboard-stats-row below is unrelated and still in use by Finance, the
   Reports exec dashboard, System Status and Tenant Usage. */

.card-title {
  font-size: 16px;
  font-weight: 600;
  color: var(--text-main);
}

.card-desc {
  font-size: 13px;
  color: var(--text-muted);
  line-height: 1.5;
}

/* Home (BLD-033) stats row - scoped override of the shared .dashboard-stats-row
   grid above (Finance/exec-dashboard/System Status/Tenant Usage all keep the
   unmodified version). Home only ever renders one stat-card (the pending-
   approvals count), and the shared grid's auto-fit/1fr columns stretch a
   lone card to the full row width - a mostly-empty panel reads as more
   important than "1". Capped here only, same precedent as .oms-tile below. */
.home-stats-row .stat-card {
  max-width: 280px;
}

/* Home (BLD-033) quick-action tiles - a NEW grid, deliberately not named
   .dashboard-grid/.dashboard-card (the retired Dashboard's own classes,
   removed 2026-08-01 with that screen - see app.js's DEFAULT_VIEW comment
   for why this is not the same thing). Reuses .card-title/.card-desc above
   and the .table-panel/border/shadow tokens every other panel already uses. */
.home-action-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 12px;
}

.home-action-card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  text-align: left;
  background-color: var(--panel-bg);
  border: 1px solid var(--border-color);
  border-radius: 8px;
  padding: 16px;
  cursor: pointer;
  font: inherit;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.home-action-card:hover,
.home-action-card:focus-visible {
  border-color: var(--primary-color);
  box-shadow: var(--shadow-sm);
  outline: none;
}

.home-recent-list {
  display: flex;
  flex-direction: column;
}

.home-recent-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  text-align: left;
  background: none;
  border: none;
  border-top: 1px solid var(--border-color);
  padding: 10px 4px;
  cursor: pointer;
  font: inherit;
  color: var(--text-main);
}

.home-recent-item:first-child {
  border-top: none;
}

.home-recent-item:hover,
.home-recent-item:focus-visible {
  color: var(--primary-color);
  outline: none;
}

/* A text-color change alone is not a visible focus indicator for keyboard
   users (WCAG 2.4.7/1.4.11) - found during the BLD-038 sweep of this new
   (BLD-033) surface. box-shadow alone does not survive forced-colors
   (Windows High Contrast - verified live, same as .stat-card[role="button"]
   above), so this pairs it with a real outline rather than relying on the
   ring for both cases. */
.home-recent-item:focus-visible {
  box-shadow: inset 0 0 0 2px rgba(68, 84, 106, 0.35);
  outline: 2px solid var(--primary-color);
  outline-offset: -2px;
  border-radius: 4px;
}

/* Data Table Panel */
.table-panel {
  background-color: var(--panel-bg);
  border: 1px solid var(--border-color);
  border-radius: 8px;
  box-shadow: var(--shadow-sm);
  overflow: visible;
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  min-height: 200px;
}

.table-controls {
  padding: 14px 20px;
  border-bottom: 1px solid var(--border-color);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}

.search-box {
  display: flex;
  align-items: center;
  gap: 8px;
  border: 1px solid var(--border-color);
  border-radius: 6px;
  padding: 8px 12px;
  width: 280px;
  transition: border-color var(--transition-fast) ease, box-shadow var(--transition-fast) ease;
}

.search-box:focus-within {
  border-color: var(--primary-color);
  box-shadow: 0 0 0 3px rgba(68, 84, 106, 0.15);
}

.search-box input {
  border: none;
  background: transparent;
  outline: none;
  font-size: 13.5px;
  width: 100%;
}

.table-actions {
  display: flex;
  align-items: center;
  gap: 12px;
}

.bulk-edit-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-left: auto;
  font-size: 13px;
  color: var(--text-muted);
}

.table-wrapper {
  overflow: visible;
}

table {
  width: 100%;
  /* separate + zero spacing, not collapse: a collapsed border model breaks
     `position: sticky` on <th> in Chromium once the table's scroll ancestor
     isn't its own immediate parent (true now that the whole page scrolls
     instead of each table owning its own inner scrollbar) - this keeps the
     same collapsed visual look without that breakage. */
  border-collapse: separate;
  border-spacing: 0;
  text-align: left;
}

/* Sticky header: the table's own scroll ancestor is .page-container (Stage 26
   P2 UI pass, 2026-07-26) - the whole page scrolls as one unit instead of
   each table owning a cramped inner scrollbar, and this header row freezes at
   the top of that scroll.
   Stage 41 fixes the "scrolled-past rows paint over the frozen header" bug
   that was previously written off here as unfixable. The diagnosis was right
   - CSS2.1 17.5.1 has the browser paint table backgrounds in table-layer
   order (table -> column groups -> row groups -> rows -> cells), and a row
   group is painted in that layer whatever its `position` is, so a sticky
   <thead> is positioned correctly but never occludes the <td>s sliding under
   it - but the conclusion was wrong. The fix is to move `position: sticky`
   down one level onto the <th> cells: a positioned cell with a z-index leaves
   the table layer entirely and joins the positioned-descendants layer, which
   paints above every in-flow <td>. That is why the previous attempts (opaque
   td backgrounds, forced compositor layer, isolation:isolate) had no effect -
   they all fought the paint order rather than leaving it.
   The <thead> keeps its own sticky/z-index so any header decoration that
   isn't a <th> still travels with it. */
thead {
  position: sticky;
  top: 0;
  z-index: 15;
}

th {
  /* Must stay opaque, and must stay a solid colour rather than a token that
     could resolve to a transparent value - it is the only thing hiding the
     rows underneath. */
  background-color: var(--bg-color);
  position: sticky;
  top: 0;
  z-index: 16;
  padding: 10px 16px;
  font-size: 11.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--text-muted);
  /* box-shadow, not only border-bottom: with border-collapse:separate the
     border belongs to the cell box and can leave a hairline of the row
     beneath showing through at fractional scroll offsets / non-integer device
     pixel ratios. The inset shadow paints inside the cell, so the seam is
     solid at every zoom level. */
  border-bottom: 1px solid var(--border-color);
  box-shadow: inset 0 -1px 0 var(--border-color);
}

/* A <th> used as a row header inside a body row (none today, but the generic
   record/report renderers can emit one) must not freeze at the top of the
   page as if it were a column header. */
tbody th {
  position: static;
  z-index: auto;
}

td {
  padding: 10px 16px;
  font-size: 13.5px;
  border-bottom: 1px solid var(--border-color);
  color: var(--text-main);
  vertical-align: middle;
  background-color: var(--panel-bg);
  /* BLD-034/MC-083: table-layout stays auto (a fixed layout would force
     every column to the same width regardless of content), so nothing
     otherwise stops one long unbroken value - a long SKU, a URL, a CJK/
     Arabic string with no spaces - from stretching its column arbitrarily
     wide and squeezing every other column in the row down to near nothing.
     max-width only caps growth (short content is unaffected); overflow-wrap
     lets a value that hits the cap wrap instead of getting clipped by the
     page container's overflow-x:hidden. */
  max-width: 320px;
  overflow-wrap: break-word;
}

tr:last-child td {
  border-bottom: none;
}

tr:hover td {
  background-color: var(--bg-color);
}

/* Badges */
.badge {
  display: inline-flex;
  align-items: center;
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 11px;
  font-weight: 600;
}

.badge-success {
  background-color: var(--success-soft-bg);
  color: var(--success-strong);
}

.badge-secondary {
  background-color: var(--surface-subtle);
  color: var(--text-main);
}

/* Action buttons in tables */
.action-btn {
  border: none;
  background: transparent;
  padding: 6px;
  border-radius: 4px;
  cursor: pointer;
  color: var(--text-muted);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background-color var(--transition-fast) ease,
              color var(--transition-fast) ease,
              transform var(--transition-fast) var(--ease-out);
  -webkit-tap-highlight-color: transparent;
}

.action-btn:hover {
  background-color: var(--surface-subtle);
  color: var(--text-main);
}

.action-btn:active:not(:disabled) {
  transform: scale(0.9);
}

.action-btn-danger:hover {
  background-color: var(--danger-soft-bg);
  color: var(--danger-color);
}

/* Copy-to-clipboard affordance on values users commonly re-search elsewhere
   (SKU, PO number, etc.) - stays invisible until the cell is hovered/focused
   so it doesn't visually clutter a table at rest. */
.copyable-cell {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  max-width: 100%;
}

.copy-chip {
  border: none;
  background: transparent;
  padding: 2px;
  border-radius: 3px;
  cursor: pointer;
  color: var(--text-muted);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  flex-shrink: 0;
  transition: opacity var(--transition-fast) ease, background-color var(--transition-fast) ease, color var(--transition-fast) ease;
}

.copyable-cell:hover .copy-chip,
.copy-chip:focus-visible {
  opacity: 1;
}

.copy-chip:hover {
  background-color: var(--surface-subtle);
  color: var(--text-main);
}

.copy-chip.copy-chip-done {
  opacity: 1;
  color: var(--success-color);
}

/* Modals */
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(15, 23, 42, 0.45);
  backdrop-filter: blur(4px);
  display: flex;
  align-items: center;
  justify-content: center;
  /* Keeps the dialog off the viewport edges at any zoom level, and gives the
     max-height below something to subtract from. */
  padding: 20px 16px;
  z-index: 100;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
}

.modal-overlay.open {
  opacity: 1;
  pointer-events: auto;
}

/* Stage 33: the container used to have no height limit at all. `body` is
   `overflow: hidden`, so a form taller than the viewport was centred by the
   overlay's flexbox and then clipped off BOTH ends - the first fields and the
   Save/Cancel footer were simply unreachable, with nothing to scroll. Now the
   container is a flex column that can never exceed the viewport, the header and
   footer stay pinned, and .modal-body takes the overflow (below). Sized in
   viewport units rather than at fixed breakpoints so browser zoom - 40%, 100%,
   120%, anything - is handled by the same rule: zoom changes the CSS-pixel
   viewport, and these bounds are recomputed from it. */
.modal-container {
  background-color: var(--panel-bg);
  border-radius: 8px;
  box-shadow: var(--shadow-lg);
  width: 100%;
  max-width: min(520px, 100%);
  display: flex;
  flex-direction: column;
  max-height: calc(100vh - 40px);
  max-height: calc(100dvh - 40px); /* dvh where supported: mobile URL bars */
  transform: translateY(20px);
  transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
  overflow: hidden;
}

.modal-overlay.open .modal-container {
  transform: translateY(0);
}

/* Most dialogs wrap body+footer in a <form> (see index.html), so the form -
   not .modal-body - is the container's flex child. Without this the form stays
   a plain block sized to its content, the body never gets the leftover height
   to scroll in, and the footer with the Save button is clipped away exactly as
   before. Written as `> form` so the handful of JS-built modals that put
   .modal-body directly in the container keep working unchanged. */
.modal-container > form {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

.modal-header {
  padding: 18px 22px;
  border-bottom: 1px solid var(--border-color);
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.modal-title {
  font-size: 18px;
  font-weight: 600;
  color: var(--text-main);
}

.modal-close {
  background: transparent;
  border: none;
  cursor: pointer;
  color: var(--text-muted);
  padding: 6px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background-color var(--transition-fast) ease, color var(--transition-fast) ease;
}

.modal-close:hover {
  background-color: var(--surface-subtle);
  color: var(--text-main);
}

.modal-body {
  padding: 22px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  /* The one part of the dialog that scrolls. min-height:0 is what actually
     lets a flex child shrink below its content height - without it the body
     refuses to shrink and the container overflows again. */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.modal-footer {
  padding: 16px 22px;
  background-color: var(--bg-color);
  border-top: 1px solid var(--border-color);
  display: flex;
  justify-content: flex-end;
  gap: 12px;
  flex: 0 0 auto;
  flex-wrap: wrap;
}

.modal-header {
  flex: 0 0 auto;
}

/* The generic document form (openDynamicModal in app.js - every master and
   document create/edit screen) is the one dialog with an unbounded field
   count: Item alone renders ~20. A single column made it taller than any
   screen. This lays it out on an intrinsic grid instead - one column when
   there's room for one, two or three when there's room for more - so the
   number of columns follows the available width rather than a breakpoint
   list. minmax(min(240px,100%),1fr) is the whole responsive story: 100%
   keeps a single column from overflowing a very narrow/heavily-zoomed
   viewport, 240px is the narrowest a labelled input stays usable at.

   The widening is opt-in per form (openDynamicModal sets .modal-container-wide
   above 4 fields): a 3-field master in a 920px dialog would be two-thirds empty
   row, and at 520px the 240px minimum yields one column on its own, so small
   masters keep exactly the dialog they have today. */
#dynamic-modal .modal-container.modal-container-wide {
  max-width: min(920px, 100%);
}

#dynamic-modal-body {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: 16px 20px;
  align-items: start;
}

/* Fields that are a composite editor rather than one input (the JSONTable /
   JSONMap line editors) get the full row - squeezing a multi-column table
   into a 240px grid track would defeat the point. */
#dynamic-modal-body .form-group-wide {
  grid-column: 1 / -1;
}

/* Form Controls */
.form-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.form-label {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-main);
}

.form-label .required {
  color: var(--danger-color);
  margin-left: 2px;
}

.form-input, .form-select, .form-textarea {
  font-family: var(--font-family);
  font-size: 14px;
  padding: 8px 12px;
  border-radius: 6px;
  border: 1px solid var(--border-color);
  background-color: var(--panel-bg);
  color: var(--text-main);
  outline: none;
  transition: border-color var(--transition-fast) ease, box-shadow var(--transition-fast) ease;
  width: 100%;
}

.form-input::placeholder, .form-textarea::placeholder {
  color: var(--text-placeholder);
}

.form-input:focus, .form-select:focus, .form-textarea:focus {
  border-color: var(--primary-color);
  box-shadow: 0 0 0 3px rgba(68, 84, 106, 0.15);
}

/* Server-issued values (Stage 30.6): document numbers come from the tenant's
   Prefix Configs series on save, so their fields read as informational rather
   than as an input someone is expected to fill in. Also covers the code field
   the generic record form already rendered read-only. */
.form-input[readonly] {
  background-color: var(--bg-color);
  color: var(--text-muted);
  cursor: default;
}

.form-input[readonly]:focus {
  border-color: var(--border-color);
  box-shadow: none;
}

.form-textarea {
  resize: vertical;
  min-height: 80px;
}

/* Master-data autosuggest (Stage 18.2) - a floating menu positioned under
   whichever <input> attachTypeahead() was called on. Fixed positioning +
   document.body append so it works unmodified under any form's DOM
   structure/overflow, matching how .modal-overlay already escapes its
   container. */
.typeahead-menu {
  position: fixed;
  z-index: 300;
  background-color: var(--panel-bg);
  border: 1px solid var(--border-color);
  border-radius: 6px;
  box-shadow: var(--shadow-lg);
  max-height: 220px;
  overflow-y: auto;
}

.typeahead-item {
  padding: 8px 12px;
  font-size: 13px;
  color: var(--text-main);
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.typeahead-item.active,
.typeahead-item:hover {
  background-color: var(--primary-color);
  color: var(--on-primary);
}

/* Global search suggestions (Stage 28.5) - the same typeahead menu, with a
   second muted line naming what the hit is ("Screen · Buying"). Built from
   the same tokens, so it themes itself along with everything else. */
.global-search-menu {
  max-height: 340px;
}

.global-search-item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  white-space: normal;
  padding: 8px 12px;
}

.global-search-label {
  font-size: 13px;
  font-weight: 500;
}

.global-search-meta {
  font-size: 11px;
  color: var(--text-muted);
}

/* Stays legible once the row is highlighted and the background flips to the
   primary colour. */
.global-search-item.active .global-search-meta,
.global-search-item:hover .global-search-meta {
  color: var(--on-primary);
  opacity: 0.75;
}

.global-search-empty {
  padding: 10px 12px;
  font-size: 12.5px;
  color: var(--text-muted);
}

/* Toggle Switch */
.toggle-group {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 4px 0;
}

.toggle-container {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.toggle-label {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-main);
}

.toggle-desc {
  font-size: 11px;
  color: var(--text-muted);
}

.switch {
  position: relative;
  display: inline-block;
  width: 44px;
  height: 24px;
  flex-shrink: 0;
}

.switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

.slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: var(--border-hover);
  transition: .3s;
  border-radius: 24px;
}

.slider:before {
  position: absolute;
  content: "";
  height: 18px;
  width: 18px;
  left: 3px;
  bottom: 3px;
  background-color: var(--on-primary);
  transition: .3s;
  border-radius: 50%;
  box-shadow: 0 1px 3px rgba(0,0,0,0.15);
}

input:checked + .slider {
  background-color: var(--primary-color);
}

input:focus + .slider {
  box-shadow: 0 0 1px var(--primary-color);
}

input:checked + .slider:before {
  transform: translateX(20px);
}

/* Utility Helpers */
.hidden {
  display: none !important;
}

.flex-row-gap {
  display: flex;
  align-items: center;
  gap: 8px;
}

.text-center {
  text-align: center;
}

.text-muted {
  color: var(--text-muted);
}

.py-8 {
  padding-top: 32px;
  padding-bottom: 32px;
}

/* Pagination */
.pagination {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 24px;
  border-top: 1px solid var(--border-color);
  font-size: 13px;
  color: var(--text-muted);
}

.pagination-buttons {
  display: flex;
  gap: 8px;
}

.pagination-btn {
  padding: 6px 12px;
  border-radius: 4px;
  border: 1px solid var(--border-color);
  background: var(--panel-bg);
  cursor: pointer;
  font-weight: 500;
  transition: background-color var(--transition-fast) ease, border-color var(--transition-fast) ease, opacity var(--transition-fast) ease;
}

.pagination-btn:hover {
  background-color: var(--bg-color);
}

.pagination-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Tab Active State for Activity Log */
.btn-outline.active {
  background-color: var(--primary-color);
  color: var(--on-primary);
  border-color: var(--primary-color);
}

/* System Exception Logs Badge Custom Colors */
.badge-danger {
  background-color: var(--danger-soft-bg);
  color: var(--danger-strong);
}

.badge-warning {
  background-color: var(--warning-soft-bg);
  color: var(--warning-strong);
}

/* Preformatted Stack Traces style rules */
pre {
  background: var(--surface-subtle);
  border: 1px solid var(--border-color);
  border-radius: 6px;
  padding: 12px;
  overflow-x: auto;
  font-family: Consolas, Monaco, "Andale Mono", monospace;
  white-space: pre-wrap;
  color: var(--text-main);
  text-align: left;
}

/* Custom Dialog Backdrop */
.custom-dialog-backdrop {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background-color: rgba(15, 23, 42, 0.6);
  backdrop-filter: blur(4px);
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 20px 16px;
  z-index: 999999;
  opacity: 1;
  transition: opacity 0.25s ease;
}

.custom-dialog-backdrop.hidden {
  display: none !important;
  opacity: 0;
}

/* Custom Dialog Box - same viewport-bounded structure as .modal-container
   above: a long showApiError() detail list clipped off-screen for the same
   reason a long form did. */
.custom-dialog-box {
  background-color: var(--panel-bg);
  border-radius: 8px;
  width: 100%;
  max-width: min(480px, 100%);
  display: flex;
  flex-direction: column;
  max-height: calc(100vh - 40px);
  max-height: calc(100dvh - 40px);
  box-shadow: var(--shadow-lg), 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
  border: 1px solid var(--border-color);
  animation: dialogFadeIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
  overflow: hidden;
}

@keyframes dialogFadeIn {
  from {
    transform: scale(0.92);
    opacity: 0;
  }
  to {
    transform: scale(1);
    opacity: 1;
  }
}

.custom-dialog-header {
  padding: 18px 24px;
  border-bottom: 1px solid var(--border-color);
  display: flex;
  justify-content: space-between;
  align-items: center;
  background-color: var(--bg-color);
  flex: 0 0 auto;
}

.custom-dialog-header h3 {
  font-size: 16px;
  font-weight: 600;
  color: var(--text-main);
  margin: 0;
}

.custom-dialog-close-x {
  background: none;
  border: none;
  font-size: 24px;
  font-weight: 400;
  color: var(--text-muted);
  cursor: pointer;
  line-height: 1;
  padding: 0;
  transition: color var(--transition-fast) ease;
}

.custom-dialog-close-x:hover {
  color: var(--danger-color);
}

.custom-dialog-body {
  padding: 24px;
  font-size: 14px;
  color: var(--text-main);
  line-height: 1.6;
  text-align: center;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Callers inject arbitrary markup into the dialog (showCustomAlert's extra
   slot, showApiError's detail lists, showCustomPrompt's input). Everything
   inherits .custom-dialog-body's readable foreground by default; these are
   the elements the browser would otherwise paint with a UA default colour
   that ignores the theme. Deliberately NOT a `*` rule - that would also
   flatten intentional colours like a .badge-danger dropped into a dialog. */
.custom-dialog-body p,
.custom-dialog-body li,
.custom-dialog-body td,
.custom-dialog-body th,
.custom-dialog-body h1,
.custom-dialog-body h2,
.custom-dialog-body h3,
.custom-dialog-body h4,
.custom-dialog-body label,
.custom-dialog-body strong,
.custom-dialog-body b,
.custom-dialog-body code {
  color: inherit;
}

.custom-dialog-body a {
  color: var(--primary-color);
}

.custom-dialog-body small,
.custom-dialog-body .text-muted {
  color: var(--text-muted);
}

.custom-dialog-footer {
  padding: 16px 24px;
  border-top: 1px solid var(--border-color);
  display: flex;
  justify-content: flex-end;
  gap: 12px;
  background-color: var(--bg-color);
  flex: 0 0 auto;
  flex-wrap: wrap;
}

/* Login Screen */
.login-screen {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background-color: var(--bg-color);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 500;
}

.login-card {
  background-color: var(--panel-bg);
  border: 1px solid var(--border-color);
  border-radius: 8px;
  box-shadow: var(--shadow-lg);
  width: 100%;
  max-width: 360px;
  padding: 32px 28px;
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.login-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 700;
  font-size: 18px;
  color: var(--primary-color);
  letter-spacing: -0.5px;
}

.login-subtitle {
  font-size: 13.5px;
  color: var(--text-muted);
  margin-top: -12px;
}

.login-card form {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* Sticker Printing (Stage 13.15) - #sticker-print-area is hidden on screen
   and only shown during window.print(), while everything else is hidden,
   so the printed page contains just the labels.

   31.1.9: the receipt and invoice sheets are listed here too. They were only
   ever shown/hidden by the @media print block below, so once a POS receipt
   had been printed its markup stayed rendered at the bottom of the live page
   until the next navigation - the areas are direct children of <body>, and
   nothing else was hiding them on screen. */
#sticker-print-area,
#receipt-print-area,
#invoice-print-area,
#bol-print-area {
  display: none;
}

.sticker-label {
  display: inline-block;
  width: 260px;
  padding: 10px;
  margin: 6px;
  border: 1px dashed #999;
  font-family: var(--font-family);
  page-break-inside: avoid;
}

.sticker-label .sticker-barcode {
  /* Stage 42.1.11: normally holds a real Code 128 <svg> (barcode_svg); the
     font/letter-spacing rules below only apply to the plain-text fallback,
     used on the rare label whose barcode value Code Set B can't encode. */
  font-family: monospace;
  font-size: 22px;
  font-weight: 700;
  letter-spacing: 2px;
  text-align: center;
  border-top: 1px solid #333;
  border-bottom: 1px solid #333;
  padding: 6px 0;
  margin: 6px 0;
}

.sticker-label .sticker-barcode svg {
  display: block;
  max-width: 100%;
  height: auto;
  margin: 0 auto;
}

.sticker-label .sticker-name {
  font-size: 13px;
  font-weight: 600;
}

.sticker-label .sticker-meta {
  font-size: 11px;
  color: #555;
}

/* Stage 52: a label that resolved a category StickerTemplate is sized to its
   own mm dimensions (overriding the fixed 260px default above) and becomes a
   positioning context for its absolutely-positioned .sticker-el children -
   the same mm coordinates the designer canvas and the printed ZPL agree on. */
.sticker-label.sticker-label-templated {
  display: inline-block;
  position: relative;
  width: auto;
  padding: 0;
  vertical-align: top;
}

.sticker-el {
  font-family: var(--font-family);
  line-height: 1.15;
  white-space: pre-wrap;
  word-break: break-word;
}

.sticker-el-barcode svg {
  width: 100%;
  height: 100%;
}

/* Marks the last copy of each category run (labels arrive pre-sorted by
   category) so a mixed-category batch tears apart as separate stacks. */
.sticker-label-group-end {
  page-break-after: always;
  break-after: page;
}

/* Stage 52 template designer canvas - a to-scale (4px/mm), draggable/
   resizable positioning surface. First pointer-drag UI in this app; kept
   deliberately plain (no library). */
.sticker-designer-el {
  position: absolute;
  box-sizing: border-box;
  border: 1px dashed #94a3b8;
  background: rgba(59, 130, 246, 0.06);
  cursor: move;
  overflow: hidden;
}

.sticker-designer-el.selected {
  border: 2px solid var(--primary-color, #2563eb);
  background: rgba(37, 99, 235, 0.1);
}

.sticker-designer-resize-handle {
  position: absolute;
  right: -4px;
  bottom: -4px;
  width: 10px;
  height: 10px;
  background: var(--primary-color, #2563eb);
  border-radius: 2px;
  cursor: nwse-resize;
}

@media print {
  body > *:not(#sticker-print-area):not(#receipt-print-area):not(#invoice-print-area):not(#bol-print-area) {
    display: none !important;
  }
  #sticker-print-area.printing,
  #receipt-print-area.printing,
  #invoice-print-area.printing,
  #bol-print-area.printing {
    display: block !important;
  }
}

.receipt {
  width: 280px;
  margin: 0 auto;
  font-family: 'Courier New', monospace;
  font-size: 12px;
  color: #000;
}

.receipt .receipt-header {
  text-align: center;
  margin-bottom: 8px;
}

.receipt .receipt-title {
  font-size: 14px;
  font-weight: 700;
}

.receipt hr {
  border: none;
  border-top: 1px dashed #000;
  margin: 6px 0;
}

.receipt .receipt-line {
  display: flex;
  justify-content: space-between;
  gap: 8px;
}

.receipt .receipt-total {
  font-weight: 700;
  font-size: 13px;
}

/* Invoice print sheet (Stage 31.1.9) - the browser fallback for the Sales
   Invoice "Print" button when QZ Tray is not available. Laid out for A4
   rather than a receipt roll, which is the one thing that differs from
   .receipt above. */
.invoice-sheet {
  width: 100%;
  max-width: 700px;
  margin: 0 auto;
  font-family: var(--font-family);
  font-size: 14px;
  color: #000;
}

.invoice-sheet .invoice-title {
  font-size: 26px;
  font-weight: 700;
}

.invoice-sheet .invoice-draft {
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 3px;
  color: #b91c1c;
}

.invoice-sheet hr {
  border: none;
  border-top: 2px solid #000;
  margin: 14px 0;
}

.invoice-sheet td {
  padding: 4px 0;
}

.invoice-sheet td.invoice-key {
  font-weight: 700;
  padding-right: 20px;
}

.invoice-sheet .invoice-total {
  margin-top: 24px;
  font-size: 22px;
  font-weight: 700;
}

.login-error {
  font-size: 13px;
  color: var(--danger-strong);
  background-color: var(--danger-soft-bg);
  border: 1px solid var(--danger-soft-border);
  border-radius: 4px;
  padding: 8px 12px;
}

/* View loading placeholder (Stage 32.2). Shown for the gap between clearing
   the old screen and the new one's data arriving - see renderView in app.js.
   Deliberately understated: an indeterminate bar plus a muted label, not a
   spinner that draws the eye, since on a fast connection it is visible for
   only a frame or two. */
.view-loading {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 24px;
  color: var(--text-muted);
  font-size: 13px;
}

.view-loading-bar {
  position: relative;
  height: 3px;
  border-radius: 999px;
  background: var(--surface-subtle);
  overflow: hidden;
}

.view-loading-bar::after {
  content: '';
  position: absolute;
  inset: 0 auto 0 0;
  width: 35%;
  border-radius: 999px;
  background: var(--primary-color);
  animation: view-loading-slide 1.1s var(--ease-out) infinite;
}

@keyframes view-loading-slide {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(340%); }
}

/* Respect a reduced-motion preference: keep the bar as a static hint rather
   than removing the feedback entirely, which would put us back to a screen
   that looks like nothing happened. */
@media (prefers-reduced-motion: reduce) {
  .view-loading-bar::after {
    animation: none;
    width: 100%;
    opacity: 0.35;
  }
}

/* MFA recovery codes (Stage 32.5) - the display-once list shown after
   enrollment and after a device change. Monospaced and generously spaced
   because these get read off the screen and written down by hand; the
   deliberate two-column grid keeps ten codes on screen without scrolling on a
   laptop, and collapses to one column on a phone. */
.recovery-code-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 6px 16px;
  margin: 0 0 12px;
  padding: 12px;
  background: var(--bg-color);
  border: 1px solid var(--border-color);
  border-radius: 8px;
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 13px;
  letter-spacing: 0.04em;
  white-space: normal;
  user-select: all;
}

/* Toast (Stage 23) - non-blocking, auto-dismissing notices; see
   showToast() in app.js. Stacks bottom-right, below the modal dialog's
   z-index so a blocking alert still wins if both are ever visible. */
#toast-container {
  position: fixed;
  bottom: 24px;
  right: 24px;
  z-index: 999998;
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: 360px;
}

.toast {
  background-color: var(--panel-bg);
  border-left: 4px solid var(--border-color);
  border-radius: 6px;
  box-shadow: var(--shadow-lg);
  padding: 12px 16px;
  cursor: pointer;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.2s ease, transform 0.2s ease;
}

.toast.toast-visible {
  opacity: 1;
  transform: translateY(0);
}

.toast-title {
  font-size: 13px;
  font-weight: 600;
  margin-bottom: 2px;
  color: var(--text-main);
}

.toast-message {
  font-size: 13px;
  color: var(--text-muted);
  line-height: 1.5;
}

.toast-danger { border-left-color: var(--danger-color); }
.toast-warning { border-left-color: var(--warning-color); }
.toast-success { border-left-color: var(--success-color); }
.toast-info { border-left-color: var(--primary-color); }

/* Page banner (Stage 23) - dismissible bar at the top of a screen
   container; see renderPageBanner() in app.js. */
.page-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 16px;
  border-radius: 6px;
  font-size: 13px;
  margin-bottom: 16px;
}

.page-banner-danger {
  color: var(--danger-strong);
  background-color: var(--danger-soft-bg);
  border: 1px solid var(--danger-soft-border);
}

.page-banner-warning {
  color: var(--warning-strong);
  background-color: var(--warning-soft-bg);
  border: 1px solid var(--warning-soft-border);
}

/* BLD-037: groups the message with its optional correlation reference so
   justify-content: space-between on .page-banner still pushes only the
   close button to the far side, not the reference along with it. */
.page-banner-body {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 10px;
  min-width: 0;
}

.page-banner-ref {
  font-size: 11px;
  font-weight: 600;
  opacity: 0.75;
  white-space: nowrap;
}

.page-banner-close {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 16px;
  line-height: 1;
  color: inherit;
  opacity: 0.7;
  padding: 0 4px;
}

.page-banner-close:hover {
  opacity: 1;
}

/* Empty-state guidance (Stage 30.5.1 / 30.5.2).
   One vocabulary for "there is nothing here yet, and here is the next step",
   used both under an empty picker and inside an empty table's placeholder
   row. Uses the existing colour tokens so it inherits light/dark for free. */
.empty-state-hint {
  margin-top: 6px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-muted);
}

.empty-state-link {
  color: var(--primary-color);
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}

.empty-state-link:hover,
.empty-state-link:focus-visible {
  color: var(--primary-hover);
  text-decoration: underline;
}

/* An empty typeahead menu's dead-end row is informational, not selectable -
   it must not pick up .typeahead-item's hover/active affordances. */
.typeahead-item-empty,
.typeahead-item-empty:hover,
.typeahead-item-empty.active {
  cursor: default;
  background: transparent;
  color: var(--text-muted);
  font-size: 12px;
}

/* Grouped typeahead results (Stage 30.5.8) - the heading a Location picker
   renders above each run of Stores / Warehouses / HO, and the footer note a
   browse-on-focus menu renders when it hit its page size. Neither is a
   .typeahead-item, deliberately: they are outside the arrow-key index, so
   keyboard navigation still maps 1:1 onto the result list. */
.typeahead-group {
  padding: 6px 12px 2px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
  cursor: default;
  white-space: nowrap;
}

.typeahead-group + .typeahead-item {
  padding-top: 4px;
}

.typeahead-note {
  padding: 6px 12px 8px;
  font-size: 11px;
  color: var(--text-muted);
  cursor: default;
  border-top: 1px solid var(--border-color);
  margin-top: 4px;
}

/* Setup menu grouping / filter / Advanced divider (Stage 30.5.4).
   The flyout is built at runtime from every Master doctype; these are the
   three non-navigable row types that structure it. */
.submenu-tools {
  padding: 8px 10px;
  position: sticky;
  top: 0;
  background-color: var(--panel-bg);
  border-bottom: 1px solid var(--border-color);
  /* inset shadow, not just the border, for the same reason as th's below -
     border-bottom alone leaves a hairline of the scrolling list showing
     through at fractional scroll offsets / non-integer zoom. */
  box-shadow: inset 0 -1px 0 var(--border-color);
  z-index: 1;
}

.submenu-filter {
  width: 100%;
  padding: 6px 8px;
  font-size: 13px;
  font-family: var(--font-family);
  color: var(--text-main);
  background-color: var(--panel-bg);
  border: 1px solid var(--border-color);
  border-radius: 6px;
}

.submenu-filter:focus {
  outline: none;
  border-color: var(--primary-color);
}

.submenu-group-label {
  padding: 6px 12px;
  margin: 4px 0 2px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
  background-color: var(--bg-color);
  border-radius: 5px;
}

.submenu-advanced-toggle {
  margin-top: 6px;
  border-top: 1px solid var(--border-color);
}

.submenu-advanced-toggle .submenu-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  font-weight: 600;
  color: var(--text-muted);
}

.submenu-advanced-caret {
  font-size: 11px;
  opacity: 0.8;
}

/* JSON line editor (Stage 30.5.3) - the add-line table that replaced
   hand-typed JSON on BOM, Routing, AppraisalCycle and the two report
   profile masters. Reuses the existing .btn/.form-input vocabulary; only
   the table's own density is new. */
.json-line-editor {
  border: 1px solid var(--border-color);
  border-radius: 6px;
  padding: 10px;
  background-color: var(--surface-subtle);
}

.json-line-table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 8px;
}

.json-line-table th {
  text-align: left;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
  padding: 4px 6px 6px;
}

.json-line-table td {
  padding: 3px 6px 3px 0;
  vertical-align: middle;
}

.json-line-table td:last-child {
  width: 1%;
  white-space: nowrap;
  padding-right: 0;
}

.json-line-table .form-input {
  padding: 6px 8px;
  font-size: 13px;
}

.json-line-empty {
  padding: 10px 6px;
  font-size: 12px;
  color: var(--text-muted);
}

.json-line-raw {
  width: 100%;
  margin-top: 8px;
  font-family: Consolas, Monaco, monospace;
  font-size: 12px;
}

/* Stage 30.5.7: what stands in for the create buttons when the current role
   has read access to a record type but not create access. */
.page-header-note {
  align-self: center;
  padding: 6px 10px;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
  background-color: var(--surface-subtle);
  border: 1px solid var(--border-color);
  border-radius: 6px;
  cursor: help;
}

/* Respect the OS "reduce motion" setting. Everything above is short enough to
   be comfortable, but motion sensitivity is an accessibility need, not a taste
   - and the flyout slide is the one piece here that moves rather than just
   fades. Durations are near-zero rather than `none` so transitionend/
   animationend listeners still fire. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ===========================================================================
   Purchase Order composer + printed PO (Stage 40.1)

   The PO screen went from four header inputs to a real line editor, so it
   needs a layout of its own. Everything below reuses the existing token
   vocabulary (--surface-subtle, --border-color, --text-muted, the semantic
   status colours) rather than introducing a second palette - so it inherits
   dark mode and the reduced-motion rule for free.
   =========================================================================== */

/* Two utilities the codebase already used in markup but never defined:
   .btn-sm appears on eleven existing buttons (the JSON line editor's own
   "+ Add Line" among them) and did nothing at all until now. */
.btn-sm {
  padding: 6px 12px;
  font-size: 12px;
}

.btn-ghost {
  background: transparent;
  border: 1px solid transparent;
  color: var(--text-muted);
}

.btn-ghost:hover {
  background-color: var(--surface-subtle);
  color: var(--text-main);
}

/* Right-align numeric cells. Money and quantities are read by comparing
   digit positions down a column, which only works if they share an edge. */
.num,
th.num,
td.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

input.form-input.num {
  text-align: right;
}

.po-composer {
  padding: 24px;
  margin-bottom: 24px;
}

.po-composer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 16px;
}

.po-composer-head h2 {
  font-size: 16px;
  font-weight: 700;
  margin: 0;
}

.po-header-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 14px;
  margin-bottom: 18px;
}

.po-header-grid .form-group {
  margin-bottom: 0;
}

.po-header-grid erp-typeahead,
.po-header-grid .form-input {
  width: 100%;
}

/* The derived-supply-type banner. Three states, because "we could not work it
   out" has to look different from both answers - a maker who cannot tell
   those apart will not go and fix the missing GSTIN. */
.po-supply-banner {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 14px;
  padding: 10px 14px;
  margin-bottom: 18px;
  border: 1px solid var(--border-color);
  border-radius: 8px;
  font-size: 13px;
  background-color: var(--surface-subtle);
  transition: background-color var(--transition-med) var(--ease-out),
              border-color var(--transition-med) var(--ease-out);
}

.po-supply-idle {
  color: var(--text-muted);
}

.po-supply-ok {
  background-color: var(--success-soft-bg);
  border-color: var(--success-color);
  color: var(--success-strong);
}

.po-supply-warn {
  background-color: var(--warning-soft-bg);
  border-color: var(--warning-soft-border);
  color: var(--warning-strong);
}

.po-override {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
}

.po-override input {
  width: auto;
  margin: 0;
}

.po-lines-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}

.po-lines-head h3 {
  font-size: 14px;
  font-weight: 700;
  margin: 0;
}

.po-optional {
  font-weight: 400;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.po-lines td {
  vertical-align: top;
  padding: 8px;
}

.po-lines .form-input {
  width: 100%;
  padding: 7px 9px;
}

/* Derived cells are visibly not-editable: same row, different surface, so it
   reads as "the system filled this in" without needing a legend. */
.po-derived {
  color: var(--text-muted);
  font-size: 13px;
  padding-top: 15px !important;
}

.po-line-total {
  font-weight: 600;
  color: var(--text-main);
}

.po-line-name {
  margin-top: 4px;
  font-size: 11px;
  color: var(--text-muted);
}

.po-line-error {
  margin-top: 4px;
  font-size: 11px;
  font-weight: 600;
  color: var(--danger-color);
}

.po-line-flagged td {
  background-color: var(--danger-soft-bg);
}

.po-line-remove {
  width: 26px;
  height: 26px;
  margin-top: 4px;
  padding: 0;
  font-size: 17px;
  line-height: 1;
  color: var(--text-muted);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  cursor: pointer;
  transition: color var(--transition-fast) var(--ease-out),
              background-color var(--transition-fast) var(--ease-out);
}

.po-line-remove:hover {
  color: var(--danger-color);
  background-color: var(--danger-soft-bg);
}

.po-footer {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 20px;
  margin-top: 18px;
}

.po-totals {
  min-width: 260px;
  font-size: 13px;
}

.po-total-row {
  display: flex;
  justify-content: space-between;
  gap: 24px;
  padding: 4px 0;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.po-total-grand {
  margin-top: 6px;
  padding-top: 8px;
  border-top: 1px solid var(--border-color);
  font-size: 16px;
  font-weight: 700;
  color: var(--text-main);
}

.po-total-mode,
.po-total-hint {
  margin-top: 8px;
  font-size: 11px;
  color: var(--text-muted);
}

.po-actions {
  display: flex;
  align-items: center;
  gap: 14px;
}

.po-actions .login-error {
  margin: 0;
  max-width: 420px;
}

.po-row-actions {
  white-space: nowrap;
}

.po-row-actions .action-btn + .action-btn {
  margin-left: 4px;
}

.po-sent-stamp {
  margin-top: 3px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--success-color);
}

.po-no-lines {
  font-size: 12px;
  color: var(--warning-color);
  cursor: help;
}

/* --- Printed PO ---------------------------------------------------------
   Extends .invoice-sheet (the existing A4 browser-print fallback) rather
   than defining a second sheet, so both documents share one page setup.
   There is deliberately no MRP column: the server strips it before the
   payload ever reaches this markup. */
.po-print .po-print-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
  margin: 10px 0 14px;
  font-size: 13px;
}

.po-print-parties {
  display: flex;
  gap: 24px;
  margin-bottom: 16px;
}

.po-print-party {
  flex: 1;
  padding: 10px 12px;
  border: 1px solid #999;
  border-radius: 4px;
  font-size: 12px;
  line-height: 1.5;
}

.po-print-party-title {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #555;
}

.po-print-party-name {
  font-size: 14px;
  font-weight: 700;
}

.po-print-lines {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
}

.po-print-lines th,
.po-print-lines td {
  padding: 6px 8px;
  border: 1px solid #999;
  text-align: left;
}

.po-print-lines th {
  background-color: #f0f0f0;
  font-weight: 700;
}

.po-print-itemname {
  font-size: 10px;
  color: #555;
}

.po-print-totals {
  width: 300px;
  margin-left: auto;
  margin-top: 10px;
  border-collapse: collapse;
  font-size: 13px;
}

.po-print-totals td {
  padding: 4px 8px;
}

.po-print-grand td {
  border-top: 2px solid #000;
  font-size: 15px;
  font-weight: 700;
}

.po-print-words {
  margin-top: 10px;
  font-size: 12px;
  font-style: italic;
}

.po-print-pos {
  margin-top: 4px;
  font-size: 12px;
}

.po-print-foot {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  margin-top: 34px;
  font-size: 11px;
}

.po-print-sign {
  padding-top: 6px;
  border-top: 1px solid #000;
  font-size: 12px;
}

/* Field-format guidance (Stage 40.2). Inline under the input, which is the
   DisplayStyle the message catalog already specifies for these codes. The
   note element is always present once decorated and simply empties when the
   value becomes valid, so the form does not jump as the message appears and
   disappears while typing. */
.field-format-note {
  min-height: 0;
  margin-top: 4px;
  font-size: 11px;
  line-height: 1.4;
  color: var(--text-muted);
}

.field-format-note.field-format-bad {
  color: var(--danger-color);
}

.field-format-invalid {
  border-color: var(--danger-color) !important;
}

.field-format-invalid:focus {
  box-shadow: 0 0 0 3px var(--danger-soft-bg) !important;
}

/* ===========================================================================
   Motion system (Stage 40.5)

   The complaint was precise: transitions "felt like a cheap flick". That is
   what a linear or ease-in-out curve at a middling duration reads as - the
   movement starts and stops abruptly and spends its time in the middle, which
   is the one part the eye does not need to see.

   Premium motion on Apple's platforms is the opposite shape: it leaves
   immediately, covers most of the distance in the first third, and settles.
   That is a decelerating curve with a slight overshoot, which is what
   --ease-out already encodes (cubic-bezier(0.22, 1, 0.36, 1)) and what the
   two curves below extend.

   Four rules everything here follows:

     1. Animate only `transform` and `opacity`. Both are composited on the GPU,
        so they never trigger layout or paint - which is the actual difference
        between 60fps and a stutter, far more than duration.
     2. Distances are small. 6-10px of travel reads as physical; 30px reads as
        a slide transition in a presentation.
     3. Interaction feedback is faster than layout movement. A press must
        answer inside ~100ms or it feels laggy; a panel appearing can take
        250ms and feel deliberate.
     4. Nothing moves that the user did not cause.

   The existing @media (prefers-reduced-motion: reduce) block at the end of
   this file already neutralises every duration here, so none of it fights an
   accessibility setting.
   =========================================================================== */

:root {
  /* Decelerate hard, settle soft. The workhorse for anything entering. */
  --ease-emphasized: cubic-bezier(0.16, 1, 0.3, 1);
  /* A gentle overshoot, for the one or two things that should feel springy
     (a value landing, a badge appearing) - never for large surfaces, where
     overshoot reads as wobble. */
  --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
  --duration-view: 0.22s;
}

/* --- View transitions ---------------------------------------------------
   renderView() clears #view-root and rebuilds it, which was an instant swap:
   the old screen vanished and the new one appeared fully formed, with a
   "Loading..." bar flashing between them on fast responses. That flash is the
   flick.

   Now the incoming view rises 8px and fades in over one frame budget's worth
   of time. Because it is transform+opacity only, the table underneath is
   never re-laid-out, so long lists do not judder as they arrive. */
#view-root > * {
  animation: view-enter var(--duration-view) var(--ease-emphasized) both;
}

@keyframes view-enter {
  from { opacity: 0; transform: translate3d(0, 8px, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

/* The loading bar is the other half of the flicker: on a fast response it
   appeared and vanished within ~80ms, which registers as a glitch rather than
   as feedback. Delaying its fade-in past the point where most responses have
   already returned means a quick screen simply appears, and only a genuinely
   slow one shows a loader. */
.view-loading {
  animation: loader-appear 0.2s var(--ease-out) 0.25s both;
}

@keyframes loader-appear {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* --- Interaction feedback ----------------------------------------------
   Buttons and rows answer the cursor inside the ~100ms window a human reads
   as instant. The press state scales down very slightly - 1.5% is below the
   threshold of "I saw it move" and above the threshold of "it responded". */
.btn,
.action-btn {
  transition: background-color var(--transition-fast) var(--ease-out),
              border-color var(--transition-fast) var(--ease-out),
              color var(--transition-fast) var(--ease-out),
              transform var(--transition-fast) var(--ease-out),
              box-shadow var(--transition-fast) var(--ease-out);
}

.btn:active:not(:disabled),
.action-btn:active:not(:disabled) {
  transform: scale(0.985);
}

/* Focus rings animate in rather than snapping, which is the difference
   between keyboard navigation feeling considered and feeling like a debugger
   outline. */
.form-input,
.form-select,
.form-textarea,
erp-typeahead {
  transition: border-color var(--transition-med) var(--ease-out),
              box-shadow var(--transition-med) var(--ease-out),
              background-color var(--transition-med) var(--ease-out);
}

/* Table rows: colour only. Moving or scaling a row inside a scrolling table
   is the single most common way a data-dense UI starts to feel cheap. */
tbody tr {
  transition: background-color var(--transition-fast) linear;
}

/* --- Surfaces ----------------------------------------------------------
   Modals and dialogs scale from 98% rather than sliding, which keeps the
   user's eye on the same point on screen. The backdrop fades slightly slower
   than the panel arrives, so the panel leads and the dimming follows. */
.modal-overlay {
  transition: opacity var(--transition-med) var(--ease-out);
}

.modal-overlay.active .modal-container,
.modal-overlay:not(.hidden) .modal-container {
  animation: modal-enter 0.24s var(--ease-emphasized) both;
}

@keyframes modal-enter {
  from { opacity: 0; transform: scale(0.98) translate3d(0, 6px, 0); }
  to   { opacity: 1; transform: scale(1) translate3d(0, 0, 0); }
}

/* Panels that appear inside a view (the PO composer's supply banner changing
   state, a totals block filling in) settle rather than snap. */
.po-supply-banner,
.po-totals {
  animation: settle 0.26s var(--ease-emphasized) both;
}

@keyframes settle {
  from { opacity: 0; transform: translate3d(0, 4px, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

/* --- Rendering quality --------------------------------------------------
   Grayscale antialiasing on dark backgrounds and subpixel on light is what
   makes text look native rather than washed out. Setting it globally to
   antialiased is the standard fix for the "slightly bolder than it should be"
   look that Chrome gives light text on dark panels. */
body {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

/* Money and quantity columns in the platform's mono face, with tabular
   figures, so a column of numbers lines up digit for digit. */
.po-lines .num,
.po-total-row,
.po-print-totals,
td.num {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* Long tables get their own paint layer so scrolling one does not repaint the
   sidebar and header with it. */
.table-wrapper {
  contain: paint;
}

/* The channel reference under an order's source (Stage 40.6). This pairing -
   which system the order came from, and its id over there - is what makes
   "did that Unicommerce order actually reach the ERP?" answerable on screen
   rather than in the database. */
.oms-channel-ref {
  margin-top: 2px;
  font-family: var(--font-numeric);
  font-size: 10px;
  color: var(--text-muted);
  word-break: break-all;
}

/* ===========================================================================
   Setup guidance (Stage 41)

   Two surfaces, deliberately different in weight:

   .setup-hint   - the line under a picker. Quiet by default and, when the
                   target list has records, only present while the field is
                   focused. It is guidance, not an alarm; a form with eight
                   pickers must not read as eight warnings.
   .setup-banner - the block at the top of a screen whose prerequisites are
                   missing. This one IS an alarm, because the user cannot use
                   the screen at all, so it takes the warning tint.

   Both use only semantic tokens, per the no-literal-colors rule above the
   dark-theme block, so they re-theme with everything else.
   =========================================================================== */
.setup-hint {
  margin-top: 6px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-muted);
  /* Collapsed until it has something to say, so an unfocused field with a
     populated target list takes no vertical space at all - the hint appears
     under the field rather than reserving a gap for itself everywhere. */
  display: none;
}

.setup-hint.visible {
  display: block;
}

.setup-hint-text {
  color: var(--text-muted);
}

.setup-hint-missing .setup-hint-text {
  color: var(--warning-strong);
  font-weight: 600;
}

/* The "you don't have access" variant. Muted rather than warning-coloured,
   and overriding .setup-hint-missing above deliberately: an amber, bold line
   reads as "you have something to fix", and this user has nothing to fix. It
   is an explanation of why the screen is empty and who to ask. */
.setup-hint-missing .setup-hint-blocked,
.setup-hint-blocked {
  color: var(--text-muted);
  font-weight: 400;
  font-style: italic;
}

/* The open-in-new-tab affordance next to every setup link. A separate <a> with
   target="_blank" rather than a JS window.open, so it behaves like a link:
   the browser's own context menu, middle-click and modifier-clicks all work,
   and a popup blocker never eats it. */
.setup-hint-newtab {
  display: inline-flex;
  align-items: center;
  margin-left: 4px;
  padding: 2px;
  border-radius: 3px;
  color: var(--primary-color);
  vertical-align: -2px;
  transition: background-color var(--transition-fast) ease, color var(--transition-fast) ease;
}

.setup-hint-newtab:hover,
.setup-hint-newtab:focus-visible {
  background-color: var(--primary-light);
  color: var(--primary-hover);
}

/* Stage 47.0.4/47.0.5: a persistent, non-dismissible strip so nobody
   mistakes a SIMULATED (external side effects off) instance for a real one -
   fixed to the viewport rather than the sidebar layout so it appears
   identically regardless of which view is on screen. body.env-banner-active
   (set by fetchAndRenderEnvironmentBanner in app.js) gets a JS-computed
   padding-top matching the banner's real height (not a hardcoded value) so
   the banner never overlaps the sidebar/topbar in either state below.

   BLD-037: the message grew a second clause (the 3PL/WMS single-owner-per-
   warehouse notice) after this was built single-line/ellipsis-only with the
   rest reachable only via the `title` hover tooltip - on a touch device
   (no hover) or a support screenshot, most of the text was simply
   unreadable. The .env-banner-text/.env-banner-toggle split below keeps the
   default compact (one line, ellipsis) but makes the full text a genuine
   tap/keyboard target, not a hover-only escape hatch. */
#environment-banner {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 9999;
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 8px;
  padding: 6px 12px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.02em;
  border-bottom: 1px solid var(--warning-soft-border);
  background-color: var(--warning-soft-bg);
  color: var(--warning-strong);
}

.env-banner-text {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

#environment-banner.env-banner-expanded {
  padding-top: 8px;
  padding-bottom: 8px;
}

#environment-banner.env-banner-expanded .env-banner-text {
  white-space: normal;
  overflow: visible;
  text-align: left;
  line-height: 1.5;
}

.env-banner-toggle {
  flex: 0 0 auto;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-weight: 700;
  text-decoration: underline;
  color: inherit;
  cursor: pointer;
}

.env-banner-toggle:focus-visible {
  outline: 2px solid var(--warning-strong);
  outline-offset: 2px;
}

.setup-banner {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 12px 14px;
  border: 1px solid var(--warning-soft-border);
  border-radius: 8px;
  background-color: var(--warning-soft-bg);
  color: var(--warning-strong);
  font-size: 13px;
  line-height: 1.55;
}

/* Nothing the viewer can act on - every missing prerequisite is one they lack
   permission to create. Drops to the neutral surface so it reads as
   information rather than as a task they are failing to do. */
.setup-banner-blocked {
  border-color: var(--border-color);
  background-color: var(--surface-subtle);
  color: var(--text-muted);
}

.setup-banner-body {
  flex: 1;
  min-width: 0;
}

.setup-banner-list {
  margin: 6px 0 0;
  padding-left: 18px;
}

.setup-banner-list li {
  margin-top: 3px;
}

/* Inside the banner the hint is always shown - the banner exists only when
   there is something to say, so the picker's focus-gating does not apply. */
.setup-banner-list .setup-hint-text {
  color: inherit;
  font-weight: 400;
}

/* The country phone rule shown under a phone field. Reuses .empty-state-hint's
   size/colour; this only carries the <code> styling for the "+" example. */
.phone-rule-hint code {
  padding: 0 3px;
  border-radius: 3px;
  background-color: var(--surface-subtle);
  font-family: var(--font-numeric);
  font-size: 11px;
}

/* ===========================================================================
   OMS Console (Stage 35.2)

   Layout only. Every colour, radius and spacing value below resolves to an
   existing token - no new palette, no new type scale. The console is a
   .table-panel with a filter rail, a bulk bar and a wide detail modal, all
   assembled from vocabulary that already existed.
   =========================================================================== */

/* .table-panel carries min-height:200px, which is right for a panel holding a
   table and wrong for one holding a single search field - it reserved a tall
   empty box above the order list until a search had been run. Modifier rather
   than a second panel style, so the border/shadow/radius stay shared. */
.table-panel.oms-compact-panel {
  min-height: 0;
}

.oms-search-row {
  display: flex;
  gap: 8px;
  align-items: center;
}

.oms-search-row .form-input {
  flex: 1;
}

.oms-console-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
  flex-wrap: wrap;
}

.oms-view-actions {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
}

/* The filter rail. Wraps rather than scrolls: seven facets on a narrow screen
   should stack into rows the operator can still read, not slide off the edge
   where a filter can be left set without being visible. */
.oms-facets {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: flex-end;
  padding-bottom: 16px;
  margin-bottom: 8px;
  border-bottom: 1px solid var(--border);
}

.oms-facet {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 11px;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.oms-facet .form-input {
  min-width: 150px;
  text-transform: none;
  letter-spacing: normal;
  font-size: 13px;
  color: var(--text);
}

/* .stat-card is a flex ROW (label beside value), which reads well for a
   two-item dashboard strip but not for a tile whose label is three words long.
   Stacked here, and only here, so no other screen's cards move. */
.oms-tile {
  cursor: pointer;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
}

.oms-tile-error {
  font-size: 11px;
  color: var(--text-muted);
}

.oms-hold-reason {
  margin-top: 2px;
  font-size: 11px;
  color: var(--text-muted);
}

.oms-expedite {
  margin-top: 4px;
  font-size: 10px;
}

.oms-pager {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  margin-top: 12px;
  font-size: 13px;
}

/* The order detail. Wider than the default modal because it carries nine
   sections of tabular data; capped at the viewport so it never overflows the
   page on a laptop, with the body scrolling instead. */
.oms-detail-container {
  max-width: min(1100px, 96vw);
  width: 100%;
}

.oms-detail-summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 16px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--border);
}

.oms-detail-address {
  padding: 16px 0;
  border-bottom: 1px solid var(--border);
}

.oms-action-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 16px 0;
  border-bottom: 1px solid var(--border);
}

.oms-detail-heading {
  margin: 20px 0 8px;
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.oms-detail-empty {
  margin: 0 0 8px;
  font-size: 13px;
}

.oms-split-pick {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-left: 8px;
  font-size: 11px;
  color: var(--text-muted);
}

/* ---------------------------------------------------------------------------
   Knowledge Center (Stage 39.4)

   Article HTML is generated by cmd/genkb with a fixed, small vocabulary, so
   these selectors cover it exhaustively rather than defensively. Every colour
   is an existing token, so the Knowledge Center follows the app's light/dark
   theme with no palette of its own.
   --------------------------------------------------------------------------- */

.kb-layout {
  display: grid;
  grid-template-columns: 260px minmax(0, 1fr);
  gap: 20px;
  align-items: start;
}

@media (max-width: 900px) {
  .kb-layout { grid-template-columns: minmax(0, 1fr); }
}

.kb-sidebar {
  position: sticky;
  top: 16px;
  background: var(--panel-bg);
  border: 1px solid var(--border-color);
  border-radius: 10px;
  padding: 16px;
  max-height: calc(100vh - 120px);
  overflow-y: auto;
}

.kb-search { position: relative; margin-bottom: 12px; }

.kb-search-results {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + 4px);
  z-index: 20;
  background: var(--panel-bg);
  border: 1px solid var(--border-color);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
  max-height: 320px;
  overflow-y: auto;
}

.kb-search-results a {
  display: block;
  padding: 8px 12px;
  border-bottom: 1px solid var(--border-color);
  color: var(--text-main);
  text-decoration: none;
}

.kb-search-results a:last-child { border-bottom: none; }
.kb-search-results a:hover { background: var(--primary-light); }
.kb-search-results a span { display: block; font-size: 12px; color: var(--text-muted); }
.kb-search-empty { padding: 10px 12px; font-size: 13px; color: var(--text-muted); }

.kb-nav-section { margin-bottom: 16px; }

.kb-nav-section h4 {
  margin: 0 0 6px;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.kb-nav-section ul { list-style: none; margin: 0; padding: 0; }

.kb-nav-section a {
  display: block;
  padding: 5px 8px;
  border-radius: 6px;
  font-size: 14px;
  color: var(--text-main);
  text-decoration: none;
}

.kb-nav-section a:hover { background: var(--primary-light); }
.kb-nav-section a.active { background: var(--primary-light); font-weight: 600; }

.kb-breadcrumb { font-size: 13px; color: var(--text-muted); margin-bottom: 12px; }
.kb-breadcrumb a { color: var(--primary-color); text-decoration: none; }
.kb-breadcrumb span { margin: 0 4px; }

.kb-toc {
  margin: 0 0 20px;
  padding: 10px 14px;
  background: var(--bg-color);
  border: 1px solid var(--border-color);
  border-radius: 8px;
  font-size: 14px;
}

.kb-toc summary { cursor: pointer; font-weight: 600; }
.kb-toc ul { margin: 8px 0 0; padding-left: 18px; }
.kb-toc a { color: var(--primary-color); text-decoration: none; }

.kb-body { line-height: 1.65; max-width: 78ch; }
.kb-body h1 { font-size: 24px; margin: 0 0 16px; }
.kb-body h2 { font-size: 19px; margin: 28px 0 10px; }
.kb-body h3 { font-size: 16px; margin: 22px 0 8px; }
.kb-body p { margin: 0 0 12px; }
.kb-body ul, .kb-body ol { margin: 0 0 12px; padding-left: 22px; }
.kb-body li { margin-bottom: 4px; }
.kb-body a { color: var(--primary-color); }
.kb-body code {
  background: var(--bg-color);
  border: 1px solid var(--border-color);
  border-radius: 4px;
  padding: 1px 5px;
  font-size: 13px;
}

.kb-body pre {
  /* Wide code must scroll inside its own box; the page itself must never
     scroll sideways. */
  overflow-x: auto;
  background: var(--bg-color);
  border: 1px solid var(--border-color);
  border-radius: 8px;
  padding: 12px 14px;
  margin: 0 0 14px;
}

.kb-body pre code { border: none; background: none; padding: 0; }

.kb-body table {
  width: 100%;
  border-collapse: collapse;
  margin: 0 0 16px;
  font-size: 14px;
}

.kb-body th, .kb-body td {
  border: 1px solid var(--border-color);
  padding: 7px 10px;
  text-align: left;
  vertical-align: top;
}

.kb-body th { background: var(--bg-color); font-weight: 600; }
.kb-body img { max-width: 100%; height: auto; border-radius: 8px; }

.kb-anchor {
  opacity: 0;
  margin-left: 6px;
  font-weight: 400;
  text-decoration: none;
  color: var(--text-muted);
}

.kb-body h2:hover .kb-anchor,
.kb-body h3:hover .kb-anchor { opacity: 1; }

/* Admonitions: internal/kb/markdown.go emits
   <aside class="kb-admonition kb-admonition-<kind>"> for the five GitHub kinds
   (note, tip, important, warning, caution). */
.kb-body .kb-admonition {
  margin: 0 0 16px;
  padding: 10px 14px;
  border-left: 3px solid var(--primary-color);
  background: var(--bg-color);
  border-radius: 0 8px 8px 0;
}

.kb-body .kb-admonition p { margin: 0 0 6px; }
.kb-body .kb-admonition p:last-child { margin-bottom: 0; }
.kb-body .kb-admonition-title { font-weight: 600; }
.kb-body .kb-admonition-warning { border-left-color: #dc2626; }
.kb-body .kb-admonition-important { border-left-color: #dc2626; }
.kb-body .kb-admonition-tip { border-left-color: #16a34a; }
.kb-body .kb-admonition-caution { border-left-color: #d97706; }

.kb-footer {
  margin-top: 32px;
  padding-top: 16px;
  border-top: 1px solid var(--border-color);
  font-size: 13px;
}

.kb-feedback {
  margin-top: 12px;
  display: flex;
  align-items: center;
  gap: 8px;
}

.kb-pager {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  margin-top: 8px;
}

.kb-pager a { color: var(--primary-color); text-decoration: none; }

.kb-drawer-container { max-width: 720px; width: 92vw; }
.kb-drawer-container .modal-body { max-height: 65vh; overflow-y: auto; }

/* --- RF / mobile task shell (Stage 47.6.2/47.6.3/47.6.4) ------------------
   Not the desktop layout at a narrow width. A single column that reflows, no
   sidebar, controls sized for a gloved thumb, and every outcome carried by
   text and shape as well as colour - a warehouse aisle is loud, bright and
   read at arm's length, and an operator wearing gloves cannot hit a 28px
   button.

   Every colour below is a token, per the rule stated above the dark-theme
   block: these screens must be legible in both themes, and an RF device on a
   loading dock is exactly where a hardcoded light background blinds someone
   at night. */

/* `.app-header`, not `.top-bar` - the element in index.html is
   <header class="app-header">, and an earlier draft of this rule named a class
   that does not exist in this markup, so the RF shell was still rendering the
   desktop search bar and industry selector above the task. */
body.rf-shell .sidebar,
body.rf-shell .app-header,
body.rf-shell .nav-toggle,
body.rf-shell .nav-scrim,
body.rf-shell .page-header { display: none !important; }

body.rf-shell .main-content,
body.rf-shell #view-root {
  margin: 0 !important;
  padding: 0 !important;
  max-width: 100% !important;
}

.rf-wrap {
  max-width: 560px;
  margin: 0 auto;
  padding: 16px 14px 96px;
  /* The status line is fixed to the bottom, so the body needs room under it. */
}

.rf-header { margin-bottom: 18px; position: relative; padding-right: 76px; }

.rf-help-btn {
  position: absolute;
  top: 0;
  right: 0;
  min-width: 60px;
  min-height: 44px;
  padding: 8px 12px;
  border: 2px solid var(--border-color);
  border-radius: 8px;
  background: var(--panel-bg);
  color: var(--text-color);
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}

.rf-help-btn:focus-visible {
  outline: 3px solid var(--primary-color);
  outline-offset: 2px;
}

.rf-title {
  font-size: 24px;
  font-weight: 700;
  line-height: 1.2;
  color: var(--text-color);
}

.rf-instruction {
  margin-top: 6px;
  font-size: 16px;
  line-height: 1.45;
  color: var(--text-muted);
}

/* One task per row, full width, tall enough to hit without looking. */
.rf-task-btn {
  display: block;
  width: 100%;
  min-height: 72px;
  text-align: left;
  padding: 14px 16px;
  margin-bottom: 12px;
  border: 2px solid var(--border-color);
  border-radius: 10px;
  background: var(--panel-bg);
  color: var(--text-color);
  cursor: pointer;
}

.rf-task-btn:focus-visible,
.rf-input:focus-visible,
.rf-primary:focus-visible,
.rf-secondary:focus-visible {
  /* A visible focus ring that survives on both themes - an RF operator
     tabbing with a ring scanner has nothing else to go on. */
  outline: 3px solid var(--primary-color);
  outline-offset: 2px;
}

.rf-task-label { display: block; font-size: 18px; font-weight: 600; }
.rf-task-hint { display: block; margin-top: 4px; font-size: 14px; color: var(--text-muted); }

.rf-field { display: block; margin-bottom: 14px; }

.rf-label {
  display: block;
  font-size: 15px;
  font-weight: 600;
  margin-bottom: 6px;
  color: var(--text-color);
}

.rf-input {
  width: 100%;
  min-height: 52px;
  font-size: 20px;
  /* Codes are read back character by character when something goes wrong, so
     a monospace face with unambiguous zero/O and one/l matters more here than
     it does anywhere else in the product. */
  font-family: ui-monospace, "Cascadia Mono", "Consolas", monospace;
  padding: 10px 12px;
  border: 2px solid var(--border-color);
  border-radius: 8px;
  background: var(--input-bg, var(--panel-bg));
  color: var(--text-color);
}

.rf-primary,
.rf-secondary {
  display: block;
  width: 100%;
  min-height: 56px;
  font-size: 18px;
  font-weight: 600;
  border-radius: 10px;
  margin-top: 12px;
  cursor: pointer;
}

.rf-primary {
  background: var(--primary-color);
  color: var(--on-primary);
  border: none;
}

.rf-secondary {
  background: transparent;
  color: var(--text-color);
  border: 2px solid var(--border-color);
}

/* The status line is pinned so feedback is in the same place every time -
   an operator learns one place to look, rather than hunting the page. */
.rf-status {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  padding: 14px 16px;
  font-size: 17px;
  font-weight: 600;
  line-height: 1.4;
  text-align: center;
  border-top: 4px solid transparent;
  background: var(--panel-bg);
  color: var(--text-color);
}

.rf-status:empty { display: none; }

/* Colour is one of four signals, never the only one: the text says what
   happened, the border weight and position differ, and the tone/vibration
   pattern differs per outcome (see setRFStatus). */
.rf-status-ok {
  border-top-color: var(--success-color);
  background: var(--success-soft-bg);
  color: var(--success-strong);
}

.rf-status-duplicate {
  border-top-color: var(--warning-color);
  background: var(--warning-soft-bg);
  color: var(--warning-strong);
}

.rf-status-error {
  border-top-color: var(--danger-color);
  background: var(--danger-soft-bg);
  color: var(--danger-strong);
}

.rf-status-offline {
  border-top-color: var(--text-muted);
  background: var(--bg-color);
  color: var(--text-color);
}

/* The three outcomes 47.6.4 names that are neither a plain success nor a plain
   error. Each differs from its neighbours in border WEIGHT as well as hue, so
   the four channels stay four even for an operator who cannot distinguish the
   colours at all. */
.rf-status-wrong_item {
  border-top-width: 8px;
  border-top-style: double;
  border-top-color: var(--danger-color);
  background: var(--danger-soft-bg);
  color: var(--danger-strong);
}

.rf-status-owner_mismatch {
  border-top-width: 8px;
  border-top-style: dashed;
  border-top-color: var(--danger-color);
  background: var(--danger-soft-bg);
  color: var(--danger-strong);
}

.rf-status-hold {
  border-top-width: 8px;
  border-top-style: solid;
  border-top-color: var(--warning-color);
  background: var(--warning-soft-bg);
  color: var(--warning-strong);
}

/* Landscape on a short device: the header shrinks rather than pushing the
   first scan field off screen, which is what "no hidden overflow at supported
   portrait/landscape sizes" means in practice. */
@media (max-height: 480px) {
  .rf-wrap { padding-top: 8px; padding-bottom: 80px; }
  .rf-title { font-size: 19px; }
  .rf-instruction { font-size: 14px; }
  .rf-task-btn { min-height: 60px; }
}

/* ===========================================================================
   Stage 47.6.3 / 47.6.4 - operator-width responsiveness and WCAG 2.2 AA
   ===========================================================================

   Written as ONE layer rather than six screen patches, deliberately. The
   item names POS, RF receiving, putaway, wave/pick, sort/stage/load and
   return - but every one of those is built from the same vocabulary
   (.table-panel, <table>, .form-input, .btn, .action-btn), so fixing the
   vocabulary fixes all six AND every screen added later. Patching six
   screens would fix six screens.

   What was actually wrong at 390px, measured against the markup rather than
   assumed:

     1. .sidebar is a hard 270px with no collapsed state anywhere in this
        stylesheet. On a 390px device it took 69% of the viewport and left
        120px for the application - which is what "a desktop page squeezed
        into 390px" means concretely (audit A-06/A-39).
     2. `table { width: 100% }` with .table-panel `overflow: visible` means a
        table wider than its container pushes the PAGE sideways. Columns then
        sit off-screen with no scrollbar attached to anything an operator can
        reach - the "hidden horizontal overflow" the item names.
     3. Focus rings existed on ~8 components out of the whole application, so
        keyboard and scanner-driven navigation was invisible on the rest
        (WCAG 2.2 SC 2.4.7).

   The desktop rendering is deliberately untouched: every rule below either
   sits inside a max-width media query, or is a focus/target-size rule that
   adds to what is there. In particular .table-panel keeps `overflow: visible`
   at desktop width, because the sticky-<th> behaviour documented at the
   `table` rule above depends on the panel NOT being a scroll ancestor. The
   scroll container is introduced only below the breakpoint, where sticky
   headers matter far less than being able to reach column six.
   -------------------------------------------------------------------------- */

/* --- WCAG 2.2 SC 2.4.7 Focus Visible, applied once, globally -------------- */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 3px solid var(--primary-color);
  outline-offset: 2px;
  /* SC 2.4.11 Focus Not Obscured (Minimum): lift the focused control above
     sticky headers and toolbars so the ring is never hidden behind them. */
  position: relative;
  z-index: 1;
}

/* --- WCAG 2.2 SC 2.5.8 Target Size (Minimum), 24x24 CSS px ---------------
   .action-btn is the row-action control used across every table screen and
   was 6px of padding around a 16px icon on some rows, which lands under the
   minimum. Stated as min-width/height rather than more padding so it cannot
   be undone by a tighter padding override later. */
.action-btn,
.btn,
.icon-btn {
  min-width: 24px;
  min-height: 24px;
}

/* --- The navigation drawer ------------------------------------------------
   Hidden above the breakpoint; the sidebar is an ordinary column there and
   nothing below this comment applies. */
.nav-toggle { display: none; }

.nav-scrim {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(15, 23, 42, 0.5);
  z-index: 39; /* directly under .sidebar's own z-index: 40 */
}

@media (max-width: 820px) {
  .nav-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* 44px, not the 24px AA minimum: this is the control every operator hits
       first, on a device, often wearing gloves. */
    width: 44px;
    height: 44px;
    margin-right: 8px;
    border: 1px solid var(--border-color);
    border-radius: 8px;
    background: var(--panel-bg);
    color: var(--text-main);
    cursor: pointer;
    flex-shrink: 0;
  }

  /* Off-canvas: the sidebar leaves the layout flow entirely, so the content
     column gets the whole viewport instead of 120px of it. */
  .sidebar {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    height: 100%;
    max-width: 86vw;
    transform: translateX(-100%);
    transition: transform var(--transition-speed) ease;
    box-shadow: 4px 0 24px rgba(15, 23, 42, 0.18);
  }

  .app-container.nav-open .sidebar { transform: translateX(0); }
  .app-container.nav-open .nav-scrim { display: block; }

  /* prefers-reduced-motion is honoured by the existing blocks above; the
     drawer simply appears rather than sliding for those users. */

  .main-content { width: 100%; }

  .app-header {
    flex-wrap: wrap;
    gap: 8px;
    row-gap: 8px;
  }

  .header-search { flex: 1 1 180px; min-width: 0; }

  /* --- (2) wide tables scroll inside their own panel, never the page ------
     overflow-x on the panel, with -webkit-overflow-scrolling for momentum on
     the Android WebViews these devices run. min-width: 0 is what actually
     lets a flex child shrink below its content width; without it the panel
     grows to the table and the page scrolls sideways regardless. */
  .table-panel {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    min-width: 0;
    min-height: 0;
  }

  /* The table may exceed the panel - that is the point - but it must not be
     forced to 100% of a container it is wider than, or the columns crush
     into unreadable slivers instead of scrolling. */
  .table-panel > table { min-width: max-content; }

  /* --- form rows reflow instead of overflowing --------------------------
     Most screens lay their filter/action rows out as an inline
     `display:flex; gap:12px` with max-width'd children. Without wrap those
     rows are the second source of sideways scrolling after tables. */
  .table-panel [style*="display: flex"],
  .table-panel [style*="display:flex"],
  .page-header {
    flex-wrap: wrap;
  }

  .form-group { min-width: 0; }
  .form-input, .form-select, .form-textarea { max-width: 100%; }

  /* Touch targets on the controls an operator actually drives on a device. */
  .btn, .form-input, .form-select { min-height: 44px; }
}

/* --- WCAG 2.2 SC 1.4.10 Reflow: usable at 320px with no 2D scrolling ------
   320 CSS px is the criterion's own threshold (400% zoom of a 1280px
   viewport). Below this the page must still not scroll horizontally. */
@media (max-width: 400px) {
  .rf-wrap,
  .table-panel { padding-left: 10px; padding-right: 10px; }

  .app-header { padding-left: 10px; padding-right: 10px; }

  /* Anything that hard-codes a pixel width in an inline style - and there are
     a number of those across the older screens - is capped rather than
     hunted down one by one. A max-width cannot break a narrower element and
     cannot be forgotten on the next screen someone writes. */
  .table-panel [style*="width:"],
  .table-panel [style*="width :"] { max-width: 100% !important; }
}

/* Landscape on a short device: the same rule the RF shell uses, extended to
   the ordinary screens, so a header does not consume the whole viewport. */
@media (max-height: 480px) and (max-width: 900px) {
  .app-header { position: static; }
  .page-header { margin-bottom: 8px; }
}

/* Release identity in the sidebar footer, under the profile chip.

   The line itself is the bare version number and nothing else. It is a real
   <button> (it opens the popup below) styled back down to plain text, so it
   keeps keyboard focus and touch activation without looking like a control.

   10px rather than the 11px of the role line above, so it recedes a step
   further into the chrome. The colour deliberately stays on --text-muted and
   is NOT lightened: #64748b on white is 4.76:1, already close to the 4.5:1
   floor WCAG AA sets for body text at any size, so one step lighter would
   fail. "Quieter" is bought with size here, never with contrast. */
.app-version {
  position: relative;
  margin: 8px 8px 0;
}

.app-version-line {
  display: block;
  padding: 0;
  border: none;
  background: none;
  font-family: var(--font-family);
  font-size: 10px;
  line-height: 1.4;
  letter-spacing: 0.01em;
  color: var(--text-muted);
  cursor: default;
  text-align: left;
}

.app-version-line:hover,
.app-version-line:focus-visible {
  color: var(--text-main);
}

/* The popup. Deliberately tiny: two numeric lines, no labels, no heading, no
   actions. It opens upward because this sits at the very bottom of the
   sidebar, and is positioned relative to .app-version so it travels with the
   line rather than needing measured coordinates. */
.app-version-pop {
  position: absolute;
  bottom: 100%;
  left: 0;
  margin-bottom: 6px;
  z-index: 120;
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 6px 9px;
  border: 1px solid var(--border-color);
  border-radius: 6px;
  background: var(--panel-bg);
  box-shadow: var(--shadow-md);
  font-family: var(--font-family);
  font-size: 11px;
  line-height: 1.35;
  white-space: nowrap;
}

.app-version-pop-num {
  color: var(--text-main);
  font-weight: 600;
}

.app-version-pop-date {
  color: var(--text-muted);
}

/* Stage 57 (user QA round, 2026-10-04) --------------------------------------
   Row actions stay in view on wide lists. A master with ~20 fields (Offer,
   Item) pushed Edit/Delete into a last column the user had to scroll right to
   find - reported as "I don't see edit in offer management". The cell sticks
   to the right edge and stays opaque so the row scrolls underneath it. */
td.row-actions-cell {
  position: sticky;
  right: 0;
  z-index: 2;
  background-color: var(--panel-bg);
  box-shadow: inset 1px 0 0 var(--border-color);
}
th.row-actions-cell {
  right: 0;
  z-index: 17;
  box-shadow: inset 1px 0 0 var(--border-color), inset 0 -1px 0 var(--border-color);
}

/* Shared date picker (57.6): the date input itself. The picker and the inline
   quick-create dialog style themselves from view-pickers.js. The native input keeps its value and typing;
   only the browser's own picker is swapped for this one. */
input[type="date"][data-date-picker="1"] {
  cursor: pointer;
  padding-right: 34px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cline x1='16' y1='2' x2='16' y2='6'/%3E%3Cline x1='8' y1='2' x2='8' y2='6'/%3E%3Cline x1='3' y1='10' x2='21' y2='10'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 16px 16px;
}
input[type="date"][data-date-picker="1"]::-webkit-calendar-picker-indicator { display: none; }

