/* 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;
  --warning-strong: #d97706;
  --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);
  --on-primary: #ffffff;
  --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);
    --on-primary: #ffffff;
    --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);
  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);
}

.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);
}

/* 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;
}

/* 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);
}

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 {
  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 {
  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-name {
  font-size: 13px;
  font-weight: 600;
}

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

@media print {
  body > *:not(#sticker-print-area):not(#receipt-print-area):not(#invoice-print-area) {
    display: none !important;
  }
  #sticker-print-area.printing,
  #receipt-print-area.printing,
  #invoice-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);
}

.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);
  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: 10px 12px 4px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.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);
}

.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;
}

.setup-banner-close {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 4px;
  border: none;
  border-radius: 4px;
  background: transparent;
  color: inherit;
  cursor: pointer;
  opacity: 0.6;
  transition: opacity var(--transition-fast) ease;
}

.setup-banner-close:hover,
.setup-banner-close:focus-visible {
  opacity: 1;
}

/* 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-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; }
