/* ============================================================================
   Blitz V2 — blitz.css · the ONE stylesheet (S0 scope)
   ----------------------------------------------------------------------------
   Reorganized from the old app's 2,108-line monolith stylesheet + the 53-line
   V2 shim that patched it. EVERY value is verbatim from those two files —
   this is a reorganization, not a redesign. The old app's look is the spec;
   its file structure is the anti-spec.

   Carried: only what the current consumers use —
     shell.js (topbar/sidebar/nav), motion.js (.am-*), hero-scene.js
     (#hero-scene), login.html, settings.html, user_management.html.
   Dropped: all page-specific sections for the 34 unbuilt pages (stat cards,
   machine tiles, progress bars, tabs, hero-band, org chart, workflow, kpi,
   chart containers, loading-overlay, view-toggle, status dots, …). They come
   back stage by stage from the old repo's stylesheet (the reference oracle).

   ONE approved deviation from the old app: body { margin-left } is scoped to
   body.with-sidebar (shell.js adds the class at mount), so the sidebar-less
   login page is truly centered instead of shoved sideways (old-app bug).
   Everything else is pixel-identical.

   Sections:  1 TOKENS · 2 BASE · 3 SHELL · 4 COMPONENTS · 5 MOTION
   ========================================================================== */


/* ============================================================================
   1. TOKENS
   ----------------------------------------------------------------------------
   Each custom property is defined ONCE per theme, directly with its resolved
   value. The old file kept two parallel names for most colors (--app- brand
   tokens aliased into the legacy --bg- / --btn- / --nav- names); the carried
   rules
   consume the legacy names (plus --app-focus-ring / --app-ember, which they
   use directly), so those are the names kept. Values identical to the old
   resolved chain. Dark overrides list only properties that actually change.
   ========================================================================== */
:root {
  /* Surfaces (the "Factory Wakes Up" warm-light world) */
  --bg-primary: #FAF8F4;        /* warm cream page background */
  --bg-secondary: #FFFFFF;      /* card / container background */
  --bg-tertiary: #F3EFE8;       /* surface / table-header tint */

  /* Text */
  --text-primary: #1A1A1A;      /* headings / strong text */
  --text-secondary: #333;       /* body text */
  --text-muted: #5A5A54;        /* secondary / muted text */

  /* Borders */
  --border-color: #E7E1D6;      /* borders / dividers */
  --border-light: #d9d2c5;      /* input borders / scrollbar thumb */

  /* Sidebar (always-dark, both themes) */
  --nav-bg: #1f1812;
  --nav-text: #e7e1d6;
  --nav-text-hover: #fff;       /* same in both themes */

  /* Buttons */
  --btn-primary: #E0531F;       /* ember accent (CTAs, active) */
  --btn-primary-hover: #C2461A; /* ember accent, hover */

  /* THE FILL WITH WORDS ON IT — teal, defined ONCE.
     The user picked #1c92b0, but white on it measures 3.6:1 — under the 4.5:1 that a 14px label
     has to meet — so the fill is the same hue one step deeper. Filled buttons only: the ember
     accent stays for rules, focus rings, the active nav bar and the row tints, none of which
     carry text.
     Deliberately NOT redefined for dark: white on this measures 5.1:1 whatever is behind it, and a
     second value would be a second thing to keep accessible. */
  --btn-fill: #17788F;          /* white on this: 5.1:1  */
  --btn-fill-hover: #12627A;    /* white on this: 6.9:1  */
  --btn-secondary: #4a443c;     /* same in both themes */
  --btn-secondary-hover: #2a251f;
  --btn-danger: #dc2626;
  --btn-danger-hover: #b91c1c;

  /* Shadows */
  --shadow: rgba(26,20,14,0.06);
  --shadow-lg: rgba(26,20,14,0.10);

  /* Accents consumed directly by carried rules */
  --app-focus-ring: rgba(224,83,31,0.22); /* ember focus glow */
  --app-ember: #FF6B2C;                   /* brighter ember (moon icon) */

  /* Layout */
  --sidebar-width: 240px;

  /* Typography */
  --font-display: "Inter Tight", "Inter", system-ui, sans-serif;

  /* Motion (see section 5; phone override lives there) */
  --am-ease: cubic-bezier(.22,.61,.36,1);
  --am-dur: 480ms;
  --am-shift: 12px;
}

/* Collapsed icon-strip sidebar (desktop, toggled by shell.js / Ctrl+B) */
html[data-sidebar="collapsed"] {
  --sidebar-width: 56px;
}

/* Night world — warm charcoal surfaces + brighter ember accent */
[data-theme="dark"] {
  --bg-primary: #17130E;
  --bg-secondary: #211B14;
  --bg-tertiary: #2a2118;

  --text-primary: #FAF8F4;
  --text-secondary: #ece6da;
  --text-muted: #a89f93;

  --border-color: rgba(250,248,244,0.12);
  --border-light: rgba(250,248,244,0.20);

  --nav-bg: #0e0b07;
  --nav-text: #cabfb0;

  --btn-primary: #FF6B2C;
  --btn-primary-hover: #e0531f;
  --btn-secondary-hover: #5c554b;
  --btn-danger: #ef4444;
  --btn-danger-hover: #dc2626;

  --shadow: rgba(0,0,0,0.35);
  --shadow-lg: rgba(0,0,0,0.5);

  --app-focus-ring: rgba(255,107,44,0.30);
}


/* ============================================================================
   2. BASE
   ----------------------------------------------------------------------------
   Box model, theme cross-fade, scrollbars, body, headings, table numerals.
   The ember focus ring has no global rule (nor did the old app): it is applied
   per-control via --app-focus-ring on inputs (section 4), the sidebar search
   (section 3), and the login inputs (section 4 · LOGIN).
   ========================================================================== */
* { box-sizing: border-box; }

/* Smooth theme transitions on all elements: duration is 0s until a theme
   attribute exists, then color-ish properties cross-fade at 0.3s. */
*, *::before, *::after {
  transition-property: background-color, border-color, color, fill, stroke, box-shadow;
  transition-duration: 0s;
  transition-timing-function: ease;
}
[data-theme] *, [data-theme] *::before, [data-theme] *::after {
  transition-duration: 0.3s;
}

/* Custom scrollbars (WebKit) */
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
::-webkit-scrollbar-track {
  background: var(--bg-primary);
  border-radius: 4px;
}
::-webkit-scrollbar-thumb {
  background: var(--border-light);
  border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
  background: var(--text-muted);
}

/* Firefox scrollbar */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--border-light) var(--bg-primary);
}

/* Body — Inter 16px. NOTE: no margin-left here; the sidebar offset is scoped
   to body.with-sidebar in section 3 (the one approved deviation). The
   margin-left transition still lives here so expand/collapse animates. */
body {
  margin: 0;
  font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
               Roboto, Ubuntu, Arial, sans-serif;
  font-feature-settings: "cv11", "ss01";
  background: var(--bg-primary);
  color: var(--text-secondary);
  line-height: 1.6;
  font-size: 16px;
  transition: background 0.3s, color 0.3s, margin-left 0.25s ease;
}

/* Headings — Inter Tight, tightened tracking, h2 carries the divider rule */
h1, h2, h3 { margin-top: 0; color: var(--text-primary); letter-spacing: -0.015em; font-family: var(--font-display); }
h1 { font-size: 1.75rem; font-weight: 700; }
h2 { font-size: 1.4rem; font-weight: 700; padding-bottom: 8px; margin-bottom: 16px; border-bottom: 1px solid var(--border-color); }
h3 { font-size: 1.1rem; font-weight: 600; }

@media (max-width: 600px) {
  h2 { font-size: 20px; }
  h3 { font-size: 16px; }
}

/* Table numerals align in columns. (The old file also listed
   .stat-card .stat-value and .kpi-value here — dropped with their pages.) */
td { font-variant-numeric: tabular-nums; }

/* The hidden attribute must beat every display rule below (a.button/button.button are
   inline-block, which otherwise overrides the UA's hidden default and shows hidden buttons). */
[hidden] { display: none !important; }


/* ============================================================================
   3. SHELL
   ----------------------------------------------------------------------------
   Injected by shell.js on every page except login: mobile topbar + hamburger,
   overlay, always-dark sidebar (brand, theme toggle, collapse toggle, search,
   grouped nav links), desktop icon-strip collapse, mobile drawer.
   .nav-link-logout is a behavior hook only (no rule here or in the old app).
   ========================================================================== */

/* THE approved deviation: the old app put margin-left on every <body>, which
   also shoved the sidebar-less login page sideways. shell.js now tags the
   body it mounts into with .with-sidebar; only that body gets the offset. */
body.with-sidebar { margin-left: var(--sidebar-width); }

/* ---- Mobile top bar ---- */
.topbar {
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 52px;
  background: var(--nav-bg);
  align-items: center;
  padding: 0 12px;
  z-index: 90;
  gap: 12px;
}

.topbar-title {
  font-weight: 700;
  color: #fff;
  font-size: 16px;
  flex: 1;
}

/* Hamburger button (three bars -> X when .open) */
.hamburger {
  background: none;
  border: none;
  cursor: pointer;
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.hamburger span {
  display: block;
  width: 22px;
  height: 2px;
  background: #fff;
  border-radius: 2px;
  transition: transform 0.3s, opacity 0.3s;
}
.hamburger.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.hamburger.open span:nth-child(2) { opacity: 0; }
.hamburger.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* ---- Sidebar overlay (mobile drawer backdrop) ---- */
.sidebar-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.5);
  z-index: 95;
}
.sidebar-overlay.open { display: block; }

/* ---- Sidebar ---- */
.sidebar {
  position: fixed;
  top: 0;
  left: 0;
  width: var(--sidebar-width);
  height: 100vh;
  background: var(--nav-bg);
  display: flex;
  flex-direction: column;
  z-index: 100;
  overflow-y: auto;
  overflow-x: hidden;
  transition: transform 0.3s ease, width 0.25s ease;
}

.sidebar-header {
  padding: 16px 18px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-bottom: 1px solid rgba(255,255,255,0.1);
  flex-shrink: 0;
  gap: 6px;
}

.sidebar-brand {
  font-weight: 700;
  font-size: 16px;
  color: #fff;
}

.sidebar-header-actions {
  display: flex;
  align-items: center;
  gap: 4px;
}

/* ---- Theme toggle (sun / moon) — used in both topbar and sidebar ---- */
.theme-toggle {
  background: rgba(255,255,255,0.1);
  border: none;
  border-radius: 50%;
  width: 36px;
  height: 36px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 18px;
  transition: background 0.2s;
}
.theme-toggle:hover { background: rgba(255,255,255,0.2); }

.theme-icon-light { display: inline; color: #fbbf24; }
.theme-icon-dark { display: none; color: var(--app-ember); }
[data-theme="dark"] .theme-icon-light { display: none; }
[data-theme="dark"] .theme-icon-dark { display: inline; }

/* ---- Collapse toggle button (desktop only) ---- */
.collapse-toggle {
  background: transparent;
  border: 1px solid rgba(255,255,255,0.15);
  color: rgba(255,255,255,0.7);
  width: 28px;
  height: 28px;
  border-radius: 6px;
  cursor: pointer;
  font-size: 16px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.collapse-toggle:hover {
  background: rgba(255,255,255,0.08);
  color: #fff;
  border-color: rgba(255,255,255,0.3);
}
.collapse-icon-expanded { display: inline; }
.collapse-icon-collapsed { display: none; }
html[data-sidebar="collapsed"] .collapse-icon-expanded { display: none; }
html[data-sidebar="collapsed"] .collapse-icon-collapsed { display: inline; }

/* ---- Sidebar search ("/" focuses; filters nav links) ---- */
.sidebar-search {
  padding: 10px 14px 6px;
  flex-shrink: 0;
}
.sidebar-search input {
  width: 100%;
  background: rgba(255,255,255,0.08);
  border: 1px solid rgba(255,255,255,0.1);
  color: #fff;
  font-size: 13px;
  padding: 6px 10px;
  border-radius: 6px;
  outline: none;
  transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
  font-family: inherit;
}
.sidebar-search input::placeholder { color: rgba(255,255,255,0.4); }
.sidebar-search input:focus {
  border-color: var(--btn-primary);
  background: rgba(255,255,255,0.12);
  box-shadow: 0 0 0 2px var(--app-focus-ring);
}

/* ---- Nav ---- */
.sidebar-nav {
  flex: 1;
  padding: 8px 0;
  overflow-y: auto;
  scrollbar-color: rgba(255,255,255,0.15) transparent;
}
.sidebar-nav::-webkit-scrollbar-track { background: transparent; }
.sidebar-nav::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 4px; }
.sidebar-nav::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.25); }

.nav-group { margin-bottom: 4px; }

.nav-group-title {
  padding: 14px 18px 6px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  /* 0.5, NOT the old app's 0.4. At 0.4 these section headings land on #797471 over the sidebar's
     #1f1812 — 3.79:1, under the 4.5:1 that 11px text has to meet, and measurably unreadable for
     anyone with reduced contrast sensitivity. 0.5 measures 5.2:1 and is the smallest step that
     clears it. This is the one value in the carried-over nav that fails an accessibility scan, and
     it failed it on every page in the app, not on one. */
  color: rgba(255,255,255,0.5);
}

.nav-link {
  display: block;
  padding: 9px 18px 9px 24px;
  color: var(--nav-text);
  text-decoration: none;
  font-weight: 500;
  font-size: 13.5px;
  border-left: 3px solid transparent;
  transition: background 0.15s, color 0.15s, border-color 0.15s, padding-left 0.15s;
}
.nav-link:hover {
  background: rgba(255,255,255,0.06);
  color: var(--nav-text-hover);
  padding-left: 28px;              /* the hover slide */
}
.nav-link.active {
  background: rgba(255,255,255,0.1);
  color: #fff;
  border-left-color: var(--btn-primary);  /* the active ember bar */
  font-weight: 700;
}

/* ---- Collapsed sidebar (icon-strip mode, desktop only) ----
   Links shrink to their 2-letter mnemonic from data-letter. */
html[data-sidebar="collapsed"] .sidebar-brand,
html[data-sidebar="collapsed"] .sidebar-search,
html[data-sidebar="collapsed"] .nav-group-title,
html[data-sidebar="collapsed"] .desktop-theme-toggle {
  display: none;
}
html[data-sidebar="collapsed"] .sidebar-header {
  padding: 12px 6px;
  justify-content: center;
}
html[data-sidebar="collapsed"] .sidebar-header-actions { gap: 0; }
html[data-sidebar="collapsed"] .nav-link {
  font-size: 0;
  text-align: center;
  padding: 10px 0;
  border-left: 3px solid transparent;
}
html[data-sidebar="collapsed"] .nav-link::before {
  content: attr(data-letter);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.5px;
  display: inline-block;
}
html[data-sidebar="collapsed"] .nav-link:hover { padding-left: 0; }
html[data-sidebar="collapsed"] .nav-link.active { background: rgba(255,255,255,0.12); }

/* ---- Mobile drawer breakpoint ---- */
@media (max-width: 768px) {
  /* Deviation-consistent: offset + topbar clearance only under the shell */
  body.with-sidebar { margin-left: 0; padding-top: 52px; }

  .sidebar { top: 52px; height: calc(100vh - 52px); width: 240px; transform: translateX(-100%); }
  .sidebar.open { transform: translateX(0); }

  .sidebar-header { display: none; }
  .topbar { display: flex; }
  .desktop-theme-toggle,
  .desktop-collapse-toggle { display: none; }
  .sidebar-overlay.open { top: 52px; }

  /* Mobile ignores collapse state — hamburger controls visibility */
  html[data-sidebar="collapsed"] .sidebar-brand,
  html[data-sidebar="collapsed"] .sidebar-search,
  html[data-sidebar="collapsed"] .nav-group-title { display: block; }
  html[data-sidebar="collapsed"] .nav-link { font-size: 13.5px; text-align: left; padding: 9px 18px 9px 24px; }
  html[data-sidebar="collapsed"] .nav-link::before { content: none; }
}


/* ============================================================================
   4. COMPONENTS
   ----------------------------------------------------------------------------
   Container · cards · buttons · forms · tables · badges · alerts · toast ·
   spinner · modal · user cards + permission grid · footer · login · hero.
   ========================================================================== */

/* ---- Page container ---- */
.container {
  margin: 20px;
  padding: 22px 26px;
  background: var(--bg-secondary);
  border-radius: 14px;
  box-shadow: 0 1px 3px var(--shadow), 0 8px 24px var(--shadow-lg);
  transition: background 0.3s, box-shadow 0.3s;
}

/* ---- Cards ---- */
.card {
  flex: 1;
  min-width: 240px;
  padding: 16px 20px;
  border: 1px solid var(--border-color);
  border-radius: 12px;
  background: var(--bg-secondary);
  transition: background 0.3s, border-color 0.3s, transform 0.2s, box-shadow 0.2s;
}
.card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px var(--shadow-lg);
  border-color: var(--border-light);
}

/* ---- Buttons ---- */
a.button, button.button {
  display: inline-block;
  padding: 9px 16px;
  background: var(--btn-fill);
  color: #fff;
  border-radius: 8px;
  text-decoration: none;
  font-size: 14px;
  font-weight: 600;
  border: none;
  cursor: pointer;
  box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.08);
  transition: background 0.2s, transform 0.15s, box-shadow 0.15s;
}
a.button:hover, button.button:hover {
  background: var(--btn-fill-hover);
  transform: translateY(-1px);
  box-shadow: 0 4px 8px rgba(0,0,0,0.15), 0 2px 4px rgba(0,0,0,0.1);
}
a.button:active, button.button:active {
  transform: translateY(0);
  box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}

a.button.secondary, button.button.secondary { background: var(--btn-secondary); }
a.button.secondary:hover, button.button.secondary:hover { background: var(--btn-secondary-hover); }

a.button.danger, button.button.danger { background: var(--btn-danger); }
a.button.danger:hover, button.button.danger:hover { background: var(--btn-danger-hover); }

/* Small action buttons + their row (user cards) — from the V2 shim */
.button.btn-sm { padding: 4px 10px; font-size: 0.85em; }
.btn-group { display: flex; gap: 8px; flex-wrap: wrap; }

/* ---- Forms ----
   `.field` USED TO BE A BARE HOOK, unstyled here and in the old app, and that is why labels and
   inputs did not line up: with no rule, both are inline-level and simply flow as text, so where a
   label lands depends on how much room the browser happens to have. The same form read "Name
   [box]   HSN Code [box]" at one zoom level and put HSN Code on its own line at another.

   Five pages had each written their own near-identical fix (customers, factory_config,
   item_history, daily_performance, tally). The pages that had not — item types, sales, orders,
   tasks, machine item, machine settings — got the raw flow. So the rule lives HERE, once, and the
   copies are gone: a label above its control, the control filling the column.

   `min-width: 0` matters more than it looks: a grid or flex item will otherwise refuse to shrink
   below its content, which is how a long select pushes a form wider than its card. */
.field { margin-bottom: 12px; min-width: 0; }
.form-grid .field { margin-bottom: 0; }
.field > label {
  display: block;
  margin-bottom: 5px;
  font-size: 0.85em;
}
.field > input:not([type="checkbox"]):not([type="radio"]),
.field > select,
.field > textarea {
  width: 100%;
  min-width: 0;
  box-sizing: border-box;
}
/* A checkbox and its words belong on one line, not stacked. */
.field > label.check-label { display: flex; align-items: center; gap: 6px; font-size: 1em; }

/* The filter bar is deliberately a ROW of small controls, so its labels stay beside nothing and
   above everything — small, tight, and never as tall as a form label. */
.filter-bar .field > label { font-size: 0.78em; margin-bottom: 3px; }

label {
  font-weight: 700;
  color: var(--text-primary);
}

input[type="text"], input[type="email"], input[type="tel"], input[type="password"], input[type="search"], input[type="url"], input[type="date"], input[type="datetime-local"], input[type="time"], input[type="month"], input[type="week"], input[type="number"], select, textarea {
  padding: 9px 12px;
  border: 1px solid var(--border-light);
  border-radius: 8px;
  font-size: 14px;
  min-width: 180px;
  background: var(--bg-secondary);
  color: var(--text-secondary);
  transition: background 0.2s, border-color 0.2s, color 0.2s, box-shadow 0.2s;
}

input[type="text"]:hover, input[type="email"]:hover, input[type="tel"]:hover, input[type="password"]:hover, input[type="search"]:hover, input[type="url"]:hover, input[type="date"]:hover, input[type="datetime-local"]:hover, input[type="time"]:hover, input[type="month"]:hover, input[type="week"]:hover, input[type="number"]:hover, select:hover, textarea:hover {
  border-color: var(--text-muted);
}

/* The ember focus ring */
input[type="text"]:focus, input[type="email"]:focus, input[type="tel"]:focus, input[type="password"]:focus, input[type="search"]:focus, input[type="url"]:focus, input[type="date"]:focus, input[type="datetime-local"]:focus, input[type="time"]:focus, input[type="month"]:focus, input[type="week"]:focus, input[type="number"]:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--btn-primary);
  box-shadow: 0 0 0 3px var(--app-focus-ring);
}

input[type="checkbox"] {
  transform: scale(1.15);
  accent-color: var(--btn-primary);
}

/* Help text under fields */
.help {
  font-size: 12px;
  color: var(--text-muted);
  margin-top: 6px;
}

/* Field grid (create/edit user modal) */
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; }

/* ---- Tables — sticky uppercase headers, zebra rows, ember hover wash ---- */
table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 12px;
}

th, td {
  padding: 11px 14px;
  border-bottom: 1px solid var(--border-color);
  text-align: left;
  vertical-align: top;
}

th {
  background: var(--bg-tertiary);
  font-weight: 700;
  font-size: 0.8em;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  border-bottom: 2px solid var(--border-color);
  position: sticky;
  top: 0;
  z-index: 2;
}

tr:nth-child(even) { background: color-mix(in srgb, var(--bg-tertiary) 40%, transparent); }
tr:hover { background: color-mix(in srgb, var(--btn-primary) 6%, var(--bg-secondary)); }

/* Responsive table — stacked card layout for tablets & phones.
   Tables opt out with class="no-mobile-stack" (horizontal scroll instead);
   cells label themselves via data-label. This is the shared mechanism that
   keeps every table usable on mobile. */
@media (max-width: 768px) {
  table:not(.no-mobile-stack),
  table:not(.no-mobile-stack) thead,
  table:not(.no-mobile-stack) tbody,
  table:not(.no-mobile-stack) th,
  table:not(.no-mobile-stack) td,
  table:not(.no-mobile-stack) tr {
    display: block;
  }

  table:not(.no-mobile-stack) thead tr {
    position: absolute;
    top: -9999px;
    left: -9999px;
  }

  /* The caption is the one part of a table that does NOT get display:block above, and left alone
     it keeps display:table-caption — sizing itself against a table box that no longer exists.
     The sentence then comes down the page as a 40px-wide column of single words, one per line,
     which is unreadable and pushes every card down with it. As a block it is the width of the
     card again. */
  table:not(.no-mobile-stack) caption {
    display: block;
  }

  table:not(.no-mobile-stack) tr {
    margin-bottom: 12px;
    border: 1px solid var(--border-color);
    border-radius: 10px;
    padding: 14px 16px;
    background: var(--bg-secondary);
    box-shadow: 0 1px 2px rgba(0,0,0,0.04);
  }

  table:not(.no-mobile-stack) td {
    border: none;
    position: relative;
    padding: 6px 0 6px 45%;
    text-align: right;
    word-break: break-word;
    font-variant-numeric: tabular-nums;
    min-height: 0;
  }

  table:not(.no-mobile-stack) td::before {
    content: attr(data-label);
    position: absolute;
    left: 0;
    width: 42%;
    padding-right: 10px;
    white-space: nowrap;
    text-align: left;
    font-weight: 600;
    font-size: 13px;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.02em;
    /* The label is the column's name, not the cell's data: it never inherits a monospace GSTIN or an
       italic "not a Blitz material" from the value it sits beside. */
    font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    font-style: normal;
  }

  /* Empty data-label = no prefix slot, value gets full width */
  table:not(.no-mobile-stack) td[data-label=""] {
    padding-left: 0;
    text-align: left;
  }

  /* Action cells: horizontal pill of compact buttons */
  table:not(.no-mobile-stack) td[data-label="Actions"] {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    padding-left: 0;
    margin-top: 4px;
  }
  table:not(.no-mobile-stack) td[data-label="Actions"]::before { display: none; }
  table:not(.no-mobile-stack) td[data-label="Actions"] .button {
    width: auto;
    flex: 1 1 auto;
    padding: 7px 12px;
    text-align: center;
  }
}

/* ---- Badges — only the pairs the current pages use ---- */
.badge {
  padding: 4px 12px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 700;
  display: inline-block;
  letter-spacing: 0.02em;
}

.badge-on  { background: #dcfce7; color: #166534; border: 1px solid #86efac; }
.badge-off { background: #fee2e2; color: #991b1b; border: 1px solid #fca5a5; }
[data-theme="dark"] .badge-on  { background: #166534; color: #dcfce7; border-color: #22c55e; }
[data-theme="dark"] .badge-off { background: #991b1b; color: #fee2e2; border-color: #ef4444; }

/* Admin badge (accent background, white text) — from the V2 shim.
   The same white-on-ember pair as a filled button, so it takes the same readable fill. */
.badge-admin { background: var(--btn-fill); color: #fff; }

/* ---- Alerts ---- */
.alert {
  padding: 12px 16px;
  border-radius: 8px;
  margin-bottom: 16px;
  font-weight: 500;
}
.alert-success { background: #dcfce7; color: #166534; border: 1px solid #86efac; }
.alert-error   { background: #fee2e2; color: #991b1b; border: 1px solid #fca5a5; }
[data-theme="dark"] .alert-success { background: #166534; color: #dcfce7; border-color: #22c55e; }
[data-theme="dark"] .alert-error   { background: #991b1b; color: #fee2e2; border-color: #ef4444; }

/* Warning bar (also used standalone as a stock-alert bar) — its own padding/
   font-size intentionally override .alert's when combined, as in the old file */
.alert-warning {
  background: #fef3c7; color: #92400e; border: 1px solid #f59e0b;
  border-radius: 8px; padding: 10px 16px; margin-bottom: 16px; font-size: 14px;
}
[data-theme="dark"] .alert-warning { background: #78350f; border-color: #f59e0b; color: #fef3c7; }

/* ---- Toast (fixed bottom-right, JS toggles .show) ---- */
.org-toast { position: fixed; bottom: 24px; right: 24px; padding: 12px 20px; border-radius: 8px; font-size: 0.85rem; font-weight: 600; color: #fff; z-index: 1000; opacity: 0; transform: translateY(10px); transition: opacity 0.3s, transform 0.3s; pointer-events: none; }
.org-toast.show { opacity: 1; transform: translateY(0); }
.org-toast.success { background: #16a34a; }
.org-toast.error { background: #dc2626; }

/* ---- Loading spinner (three pulsing ember dots) ---- */
#loading-indicator { display: none; text-align: center; padding: 60px 20px; color: var(--text-muted); }
#loading-indicator.active { display: flex; flex-direction: column; align-items: center; justify-content: center; }
#loading-indicator .spinner { display: flex; gap: 8px; margin-bottom: 20px; }
#loading-indicator .spinner-dot {
  width: 12px; height: 12px; border-radius: 50%;
  background: var(--btn-primary);
  animation: dotPulse 1.4s ease-in-out infinite;
  box-shadow: 0 0 8px color-mix(in srgb, var(--btn-primary) 40%, transparent);
}
#loading-indicator .spinner-dot:nth-child(2) { animation-delay: 0.16s; }
#loading-indicator .spinner-dot:nth-child(3) { animation-delay: 0.32s; }
#loading-indicator .loading-msg { font-size: 0.95em; color: var(--text-muted); font-weight: 500; }

@keyframes dotPulse {
  0%, 80%, 100% { transform: scale(0.4); opacity: 0.3; }
  40% { transform: scale(1); opacity: 1; }
}

/* ---- Modal (JS toggles .active) ---- */
.assign-modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center; }
.assign-modal.active { display: flex; }
/* WHAT WENT WRONG, AND THE SECOND THING THAT WENT WRONG FIXING IT.
   The dialog scrolls as one block, and a long list inside it pushed the buttons below the fold:
   measured on the categories modal against real data, 277 rows meant 15,000px of content in a
   648px box, with "Close" 14,352px down. The first fix made the box a flex column and DROPPED
   `overflow-y: auto` — which stopped the pushing by CLIPPING everything past 90vh instead, so the
   editor at the bottom became unreachable altogether. Worse, and caught only because a spec
   clicked a button that was suddenly not actionable.
   So: the box still scrolls exactly as it always did, the ✕ is STICKY inside that scroll so it
   never leaves, and any list within is capped so it scrolls itself rather than the dialog. */
.assign-modal-content {
  background: var(--bg-primary);
  border-radius: 8px;
  /* The right padding is wider on purpose: it is the gutter the sticky ✕ rides in. Because the ✕
     is sticky it travels down the whole scroll, so if it sat over the content column it would sit
     on top of a different control at every scroll position — measured on the assembled-products
     form, where scrolling to "Add line" parked it on the Customer select.
     AND THE GUTTER IS GIVEN BACK IN max-width. box-sizing is border-box globally (line ~127), so
     without that the extra 28px would come straight out of the content column and every modal in
     the app would quietly narrow. The lane is reserved OUTSIDE the content, not carved out of it. */
  padding: 24px 52px 24px 24px;
  max-width: 528px;   /* 500 + the 28px gutter */
  width: 90%;
  max-height: 90vh;
  overflow-y: auto;
  position: relative;
}

/* A list inside a modal is capped and scrolls in place, so it can never be the thing that pushes
   the controls away. */
.assign-modal-content .table-scroll { max-height: 38vh; overflow-y: auto; overflow-x: auto; }

/* The always-there close control, injected by ui.js into every modal so a ninth cannot be built
   without one. Sticky rather than absolute: absolute scrolls away with the content, sticky stays
   at the top of the scrolling box wherever the reader is in it. */
.modal-x {
  position: sticky;
  top: -8px;
  float: right;
  margin: -12px -42px 0 0;
  z-index: 2;
  border: 0;
  background: var(--bg-primary);
  color: var(--text-muted);
  font-size: 1.5rem;
  line-height: 1;
  padding: 0.1rem 0.4rem;
  cursor: pointer;
  border-radius: 6px;
}
.modal-x:hover { background: var(--bg-tertiary); color: var(--text-primary); }
.modal-x:focus-visible { outline: 2px solid var(--btn-primary); outline-offset: 2px; }

.assign-modal-content.modal-wide { max-width: 928px; }   /* 900 + the 28px gutter */

/* ---- User cards + permission grid (user_management) — from the V2 shim ----
   .card supplies bg / border / radius / padding; .cb-view / .cb-edit are
   unstyled JS hooks on the grid checkboxes. */
.user-card { margin-bottom: 12px; }
.user-card-header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; }
.user-card-header h3 { margin: 0; font-size: 1.1em; }
.user-badges { display: flex; gap: 6px; flex-wrap: wrap; }

/* Select-all row above the permission grid */
.select-all-row { display: flex; gap: 16px; align-items: center; padding: 8px 10px; background: var(--bg-tertiary); border-radius: 6px; margin-bottom: 8px; }
.select-all-row label { font-weight: 600; font-size: 0.9em; cursor: pointer; display: flex; align-items: center; gap: 6px; }

/* A single-tick grid row (the Operator permission): one checkbox spanning View and Edit, labelled */
.perm-single { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }

/* Grid scroll wrapper (sticky <th> comes free from the global table rules) */
.perm-scroll { max-height: 60vh; overflow-y: auto; }

/* Section-header rows (<td colspan=3>), styled to read like .nav-group-title */
tr.perm-section-header td {
  background: var(--bg-tertiary);
  color: var(--text-muted);
  font-weight: 700;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  border-top: 2px solid var(--border-color);
}

/* ---- Footer ---- */
.footer {
  text-align: center;
  font-size: 13px;
  color: var(--text-muted);
  margin-top: 24px;
}

/* ---- Login ----
   With the body offset scoped away (section 3), the card is truly centered.
   z-index: 1 lifts the card stack above the fixed hero background. */
.login-container { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 1rem; position: relative; z-index: 1; }
.login-box { background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 8px; padding: 2rem; width: 100%; max-width: 360px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
.login-header { text-align: center; margin-bottom: 1.5rem; }
.login-header h1 { margin: 0 0 0.25rem 0; font-size: 1.5rem; color: var(--text-primary); }
.login-header p { margin: 0; color: var(--text-muted); font-size: 0.9rem; }
.login-form .field { margin-bottom: 1rem; }
.login-form label { display: block; margin-bottom: 0.25rem; font-weight: 500; color: var(--text-primary); }
.login-form input[type="text"], .login-form input[type="password"] { width: 100%; padding: 0.625rem 0.75rem; border: 1px solid var(--border-color); border-radius: 4px; background: var(--bg-primary); color: var(--text-secondary); font-size: 1rem; box-sizing: border-box; }
.login-form input:focus { outline: none; border-color: var(--btn-primary); box-shadow: 0 0 0 2px var(--app-focus-ring); }
.login-form .button { width: 100%; padding: 0.75rem; font-size: 1rem; }
.login-error { background: #fef2f2; border: 1px solid #fecaca; color: #dc2626; padding: 0.75rem; border-radius: 4px; margin-bottom: 1rem; font-size: 0.9rem; }
[data-theme="dark"] .login-error { background: #450a0a; border-color: #7f1d1d; color: #fca5a5; }
.login-footer { text-align: center; margin-top: 1.5rem; font-size: 0.8rem; color: var(--text-muted); }

/* ---- Three.js scene host (login full-bleed background) ----
   The CSS poster gradient is the guaranteed-good fallback; the WebGL canvas
   (when hero-scene.js mounts it) fades in on top and the page is complete
   without it. */
#hero-scene {
  position: relative;
  overflow: hidden;
  background:
    radial-gradient(120% 150% at 80% 15%, rgba(255,107,44,0.30), transparent 55%),
    radial-gradient(80% 120% at 15% 90%, rgba(74,222,128,0.10), transparent 60%),
    linear-gradient(180deg, #17130E 0%, #2a1d12 58%, #1c1610 100%);
}
#hero-scene canvas {
  position: absolute; inset: 0;
  width: 100% !important; height: 100% !important;
  display: block; opacity: 0; transition: opacity 0.9s ease;
}
#hero-scene.three-on canvas { opacity: 1; }

/* Login full-bleed background + lifted card so it reads on the dark scene.
   The old app wrote this as bare .hero-login-bg, which loses the position
   battle to #hero-scene (id beats class) — so its login backdrop never
   actually rendered. Fixed here by qualifying the selector; the S10
   dashboard hero-band (#hero-scene without this class) is unaffected. */
#hero-scene.hero-login-bg { position: fixed; inset: 0; z-index: 0; }
.hero-login-bg ~ .login-container .login-box { box-shadow: 0 24px 60px rgba(0,0,0,0.45); }

@media (prefers-reduced-motion: reduce) { #hero-scene canvas { transition: none; } }

/* ---- Components · responsive (tablet ≤ 900px) ---- */
@media (max-width: 900px) {
  .container { margin: 12px; padding: 16px; }
  .card { min-width: 200px; }
  table { font-size: 14px; }
  th, td { padding: 8px 10px; }
}

/* ---- Components · responsive (phone ≤ 600px) ---- */
@media (max-width: 600px) {
  .container { margin: 8px; margin-top: 8px; padding: 12px; border-radius: 8px; }
  .card { min-width: 100%; }
  input[type="text"], input[type="email"], input[type="tel"], input[type="password"], input[type="search"], input[type="url"], input[type="date"], input[type="datetime-local"], input[type="time"], input[type="month"], input[type="week"], input[type="number"], select {
    min-width: 100%;
    width: 100%;
  }
  a.button, button.button { width: 100%; text-align: center; padding: 10px 12px; }
}


/* ---- Components · S2 shared kit (consumed via assets/ui.js) ----
   Generic, page-agnostic pieces only; a page's own layout rules live in
   assets/pages/<page>.css. Sort direction is carried by aria-sort. */
.toolbar { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
.toolbar h2 { margin: 0; }
.toolbar-actions { display: flex; gap: 8px; flex-wrap: wrap; }

.filter-bar { display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; padding: 12px; background: var(--bg-tertiary); border-radius: 8px; margin-bottom: 16px; }
.filter-bar .field { margin: 0; min-width: 140px; flex: 0 1 auto; }
.filter-bar .field label { font-size: 0.78em; }
.filter-bar .filter-actions { display: flex; gap: 8px; }

/* TABS — one definition, shared.
   These lived in sales_reports.css, so the second page that wanted tabs (assembly production) would
   have copied them and the app would have had two. That is the hodgepodge plan §11 names by
   example: table-sort JS pasted four times, fmtHMS nine copies. Port the face, not the mess. */
/* WRAP, because a tab strip that does not is a page that scrolls sideways. Six status tabs at
   phone width pushed the last two past the viewport and took the WHOLE DOCUMENT with them — every
   section on the page then sat in a 390px column inside a 598px canvas, and the horizontal scroll
   was the app's, not the strip's. Nothing asserted it and nothing could: every tab was present,
   visible and clickable, and the suite was entirely green. It was found by looking at the page at
   the width a person holds. Wrapping costs a second row on a narrow screen and nothing at all on a
   wide one, where `flex-wrap` never fires. */
.tabs {
  display: flex; flex-wrap: wrap; gap: 4px;
  border-bottom: 1px solid var(--rule, #d5d9de); margin-bottom: 16px;
}
.tab { background: none; border: none; border-bottom: 2px solid transparent; padding: 10px 16px;
       font: inherit; color: var(--muted, #5c636e); cursor: pointer; }
.tab.active { color: var(--ink, #191c21); border-bottom-color: var(--accent, #9a5200); font-weight: 600; }
.tab:focus-visible { outline: 2px solid var(--accent, #9a5200); outline-offset: -2px; }

.table-scroll { overflow-x: auto; }

/* THE ARROW IS A PICTURE, AND THE `/ ""` SAYS SO. Generated content counts towards an element's
   ACCESSIBLE NAME, so without the empty alternative text every sortable column announced itself as
   "Purchase frequency ↕" — the arrow read out loud after every header, and the header no longer
   findable by its own name. `content: <string> / <alt>` is the CSS Content L3 alternative-text
   form: the glyph is still drawn, and it is drawn for the eye only. ui.js:118-161 owns what the
   arrow MEANS; this owns how it is seen. */
th[data-sort] { cursor: pointer; user-select: none; white-space: nowrap; }
th[data-sort]::after { content: "↕" / ""; margin-left: 6px; opacity: 0.35; font-size: 0.85em; }
th[data-sort][aria-sort="ascending"]::after { content: "↑" / ""; opacity: 1; }
th[data-sort][aria-sort="descending"]::after { content: "↓" / ""; opacity: 1; }
th[data-sort]:hover { color: var(--text-primary); }
/* ui.js makes every sortable header focusable, so it needs the ring every other operable thing in
   this app has — an element that takes focus and shows nothing for it is worse than one that
   cannot be reached, because a keyboard user cannot tell where they are. */
th[data-sort]:focus-visible { outline: 2px solid var(--btn-primary); outline-offset: -2px; }

/* Incomplete-data flag (blank recipe values etc.) — never a guess, always a flag */
.badge-warn { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
[data-theme="dark"] .badge-warn { background: #451a03; color: #fbbf24; border-color: #92400e; }

/* ============================================================================
   5. MOTION — reveal-on-enter layer driven by motion.js (window.AppMotion)
   ----------------------------------------------------------------------------
   .am-pre is applied by JS ONLY, never in markup, so no-JS / motion-off users
   are never left with invisible content. [data-am-loader] on the spinner is a
   markup hook with no rule (here or in the old app).
   ========================================================================== */

/* Entrance reveal: hidden pre-state (JS-applied) -> animated in-state.
   Uses transform + opacity only; the global [data-theme] * rule transitions
   COLOR props only, so there is no conflict. */
.am-pre {
  opacity: 0;
  transform: translateY(var(--am-shift));
  transition: none;
  will-change: opacity, transform;
}
.am-in {
  opacity: 1;
  transform: none;
  transition: opacity var(--am-dur) var(--am-ease),
              transform var(--am-dur) var(--am-ease);
}
/* Clean up the GPU hint once settled */
.am-in:not(.am-pre) { will-change: auto; }

/* Animated theme-toggle icon nudge (augments toggleTheme()) */
.theme-toggle.am-spin { animation: amThemeSpin 0.5s var(--am-ease); }
@keyframes amThemeSpin {
  0%   { transform: rotate(0deg)   scale(1); }
  50%  { transform: rotate(180deg) scale(1.18); }
  100% { transform: rotate(360deg) scale(1); }
}

/* Sidebar nav entrance (enhanceNav) */
.nav-link.am-nav-pre { opacity: 0; transform: translateX(-8px); }
.nav-link.am-nav-in {
  opacity: 1; transform: none;
  transition: opacity 0.32s var(--am-ease), transform 0.32s var(--am-ease);
}

/* Gentler + faster on phones; entirely off for reduced-motion */
@media (max-width: 768px) {
  :root { --am-shift: 8px; --am-dur: 380ms; }
}
@media (prefers-reduced-motion: reduce) {
  .am-pre, .am-in,
  .nav-link.am-nav-pre, .nav-link.am-nav-in {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
    animation: none !important;
  }
  .theme-toggle.am-spin { animation: none !important; }
}
