/* Liposoll Manufacturing Hub — restyled onto the Capsoil design system.
   Loaded after /style-kit/styles.css. Legacy variable names are kept as
   aliases onto the new tokens so every existing class/inline style below
   picks up the brand look without touching any JS. */
:root {
  --bg: var(--ink-900);
  --bg-2: var(--ink-800);
  --panel: var(--ink-600);
  --panel-2: var(--ink-700);
  --border: var(--border-default);
  --text: var(--text-1);
  --text-dim: var(--text-3);
  --text-faint: var(--text-4);
  --accent: var(--c-blue);
  --accent-2: var(--c-cyan);
  /* No --danger alias: the kit already names it --danger, and re-declaring it
     as `--danger: var(--danger)` made it reference itself. A self-referential
     custom property is invalid at computed-value time, so it did not fall back
     to the kit's #E5484D — it resolved to nothing, and every `color:
     var(--danger)` in the app inherited its parent's colour instead. Red was
     silently absent everywhere: destructive buttons, conflict alerts,
     negative stock adjustments. */
  --ok: var(--success);
  --warn: var(--warning);

  /* Production-line accent colors are functional data encoding (used to
     visually distinguish the 5 pipeline stages across dashboard, calendar,
     Gantt, and capacity views) rather than brand chrome — left as-is. */
  --line-nanoemulsion: #3b82f6;
  --line-freezeDry: #06b6d4;
  --line-powderProcessing: #f59e0b;
  --line-blending: #ec4899;
  --line-qaHold: #a855f7;
  --line-packaging: #22c55e;

  --radius: var(--radius-md);
  /* Elevation, not a flat drop shadow. The glass system's guardrail is that a
     single box-shadow reads as a sticker; every raised surface gets a stack. */
  --shadow: var(--elev-1);

  /* ---------- Glass material ------------------------------------------------
     Transcribed from capsoil_style_kit/GLASS_DESIGN_SYSTEM.md §1. They live here
     rather than in public/style-kit/ so that vendored copy stays byte-identical
     to capsoil_style_kit/ and remains diffable against future kit drops.

     A raised surface is never a flat fill: it is a translucent ink fill, a white
     gradient sheen stacked on top of it, and a one-pixel lit rim (baked into the
     --elev-* stacks below). All three, always. */
  --glass-fill-card:   rgba(38, 35, 64, 0.50);
  --glass-fill-panel:  rgba(30, 27, 52, 0.88);
  --glass-fill-chrome: rgba(16, 14, 30, 0.72);
  --glass-fill-float:  rgba(38, 35, 64, 0.62);

  --glass-sheen:        linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.015) 50%);
  --glass-sheen-strong: linear-gradient(180deg, rgba(255,255,255,0.075), rgba(255,255,255,0.015) 38%);
  --glass-specular:     radial-gradient(120% 100% at 22% -10%, rgba(255,255,255,0.09), transparent 62%);

  --glass-blur:        blur(18px) saturate(1.2);
  --glass-blur-panel:  blur(30px) saturate(1.3);
  --glass-blur-chrome: blur(24px) saturate(1.4);
  --glass-border:       rgba(255,255,255,0.10);
  --glass-border-hover: rgba(255,255,255,0.16);

  /* Contact shadow (weight) + wide negative-spread shadows (distance) + lit top
     rim. The inset rim is what makes the edge catch light. */
  --elev-1: 0 1px 1px rgba(0,0,0,0.35), 0 14px 32px -16px rgba(0,0,0,0.60),
            inset 0 1px 0 rgba(255,255,255,0.10);
  --elev-2: 0 1px 1px rgba(0,0,0,0.50), 0 8px 20px -8px rgba(0,0,0,0.55),
            0 24px 56px -20px rgba(0,0,0,0.70), inset 0 1px 0 rgba(255,255,255,0.11);
  --elev-2-hover: 0 1px 1px rgba(0,0,0,0.50), 0 18px 36px -10px rgba(0,0,0,0.60),
            0 48px 96px -24px rgba(0,0,0,0.80), inset 0 1px 0 rgba(255,255,255,0.16);
  --elev-3: 0 2px 4px rgba(0,0,0,0.45), 0 30px 70px -18px rgba(0,0,0,0.72),
            0 80px 150px -50px rgba(0,0,0,0.90), inset 0 1px 0 rgba(255,255,255,0.16);
  --elev-float: 0 2px 8px rgba(0,0,0,0.40), inset 0 1px 0 rgba(255,255,255,0.10);
}

* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  font-family: var(--font-body);
  background: var(--bg);
  color: var(--text);
  font-size: var(--text-sm);
  line-height: var(--lh-normal);
}

/* Atmospheric depth, never flat black. The wash sits behind everything and is
   inert to the pointer; content is lifted onto its own layer above it. */
#app { display: flex; min-height: 100vh; position: relative; isolation: isolate; }
#app::before {
  content: "";
  position: fixed; inset: -20%;
  background: var(--gradient-aurora);
  opacity: 0.55;
  pointer-events: none;
  z-index: -1;
}

#login-screen { min-height: 100vh; display: flex; align-items: center; justify-content: center; }
#login-screen .panel { width: 340px; }

/* Sidebar */
.sidebar {
  width: 232px;
  flex-shrink: 0;
  background: var(--glass-fill-chrome);
  backdrop-filter: var(--glass-blur-chrome);
  -webkit-backdrop-filter: var(--glass-blur-chrome);
  border-right: 1px solid rgba(255,255,255,0.07);
  display: flex;
  flex-direction: column;
  padding: 18px 14px;
  position: sticky;
  top: 0;
  height: 100vh;
  /* The chrome is a painted surface, so nothing may render outside it. Fourteen
     nav items plus the brand, the location switcher and two footers are taller
     than a short viewport, and with the default `visible` the overflow spilled
     past the background and sat directly on the page's aurora. */
  overflow: hidden;
}
/* The brand block holds one thing now: the wordmark. container-type is
   load-bearing rather than decorative — the fluid size below is in cqw, which
   only resolves inside an element declaring containment. Without it the clamp
   silently falls back to its minimum and the mark just looks small. */
.brand {
  display: flex; justify-content: center; align-items: center;
  padding: 10px 4px 22px;
  container-type: inline-size;
}

/* ---- The Soll wordmark -----------------------------------------------------
   Transcribed from soll-logo-export/soll-logo.css, the portable export of the
   mark as it is drawn in the Order Request app — so both products render the
   same brand rather than two near-misses.

   It is live text, not artwork: "So" upright, "ll" slanted, glowing. There is
   no SVG or PNG, deliberately — a raster would drift the moment either app's
   type scale moved.

   The slant is a browser-synthesised oblique, not a real italic: JUST Sans
   ships Regular and ExBold, both upright, with no italic cut. That is what the
   mark is, so this reproduces it faithfully, but the exact angle is the
   rendering engine's choice and can differ slightly across platforms.

   JUST Sans ExBold is already declared and already vendored under
   public/style-kit/assets/fonts, so nothing new is copied in — the face is
   commercially licensed and duplicating the binaries would be a licensing
   question rather than a technical one. */
:root {
  /* The two hues the glow trades between, and the still colour used when
     motion is reduced (their midpoint). RGB triplets so the alpha can vary. */
  --soll-glow-blue: 47, 127, 224;
  --soll-glow-purple: 142, 60, 196;
  --soll-glow-still: 91, 63, 214;
  --soll-ease: cubic-bezier(0.65, 0, 0.35, 1);
}

.soll-wordmark {
  font-family: var(--font-display), "JUST Sans", "Inter Display", system-ui, sans-serif;
  font-weight: 800;
  font-size: 2.6rem;
  line-height: 1;
  /* Tightened because the slant opens a gap after the upright pair that
     default tracking leaves looking like two words. */
  letter-spacing: -0.03em;
  color: #ffffff;
  white-space: nowrap;
  /* The glow is on the TEXT, not behind it — text-shadow follows the
     letterforms including the slant, where a box-shadow or a blurred div
     would halo a rectangle the mark does not fill. */
  animation: soll-pulse 5s var(--soll-ease) infinite;
  will-change: text-shadow;
}

.soll-wordmark-slant { font-style: italic; letter-spacing: 0.01em; }

/* Blue and purple counter-move rather than one glow brightening, so the mark
   reads as lit from a source that is moving. Both are present in every frame:
   fading one to nothing flattens the letterforms at the crossover. */
@keyframes soll-pulse {
  0%, 100% {
    text-shadow:
      0 0 14px rgba(var(--soll-glow-blue), 0.65),
      0 0 34px rgba(var(--soll-glow-blue), 0.42),
      0 0 64px rgba(var(--soll-glow-purple), 0.16);
  }
  50% {
    text-shadow:
      0 0 14px rgba(var(--soll-glow-purple), 0.68),
      0 0 34px rgba(var(--soll-glow-purple), 0.46),
      0 0 64px rgba(var(--soll-glow-blue), 0.18);
  }
}

/* Fills the sidebar rail and keeps filling it if the rail changes width.
   Requires `container-type: inline-size` on .brand — see the note there. */
.soll-wordmark--fluid { font-size: clamp(2.2rem, 46cqw, 4.4rem); }

/* Reduced motion keeps the glow — it is the brand — and holds it still, at the
   midpoint of the two hues rather than at either end. The global reduced-motion
   override elsewhere only shortens animations; this sets the resting look. */
@media (prefers-reduced-motion: reduce) {
  .soll-wordmark {
    animation: none;
    text-shadow:
      0 0 14px rgba(var(--soll-glow-still), 0.62),
      0 0 34px rgba(var(--soll-glow-still), 0.40),
      0 0 64px rgba(var(--soll-glow-still), 0.16);
  }
}

#nav {
  display: flex; flex-direction: column; gap: 2px;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: rgba(255,255,255,0.16) transparent;
  /* A hairline between the scrolling list and the pinned footers. Without it a
     clipped final row reads as a rendering fault rather than as "there is more
     below"; with it the cut lands on a deliberate edge. */
  padding-bottom: 8px;
  margin-bottom: 8px;
  border-bottom: 1px solid var(--border-subtle);
}
#nav::-webkit-scrollbar { width: 6px; }
#nav::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.16); border-radius: 3px; }
#nav::-webkit-scrollbar-track { background: transparent; }
.nav-item {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px; border-radius: var(--radius-sm); cursor: pointer;
  color: var(--text-dim); text-decoration: none; font-weight: var(--fw-medium);
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.nav-item:hover { background: var(--surface-glass); color: var(--text); }
.nav-item.active { background: var(--accent); color: #fff; }
.nav-item .ico { width: 18px; text-align: center; }

.sidebar-footer { display: flex; align-items: center; gap: 8px; font-size: var(--text-xs); color: var(--text-faint); padding: 8px; }
.dot { width: 9px; height: 9px; border-radius: var(--radius-circle); background: var(--text-faint); }
.dot.ok { background: var(--ok); }
.dot.bad { background: var(--danger); }

/* Content */
.content { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.topbar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 20px 28px; border-bottom: 1px solid rgba(255,255,255,0.07);
  background: var(--glass-fill-chrome);
  backdrop-filter: var(--glass-blur-chrome);
  -webkit-backdrop-filter: var(--glass-blur-chrome);
  position: sticky; top: 0; z-index: var(--z-sticky);
}
.topbar h1 {
  margin: 0; font-size: var(--text-2xl); font-family: var(--font-display);
  /* Optical tracking: 24px titles sit at -0.028em (glass system §2). */
  font-weight: var(--fw-black); letter-spacing: -0.028em; color: var(--text-1);
}
.view { padding: 24px 28px; }

/* Buttons */
.btn {
  border: 1px solid var(--border-default); background: var(--glass-fill-float); color: var(--text);
  padding: 9px 14px; border-radius: 11px; cursor: pointer;
  box-shadow: var(--elev-float);
  font-size: var(--text-sm); font-weight: var(--fw-semibold); font-family: var(--font-body);
  letter-spacing: -0.006em;
  display: inline-flex; align-items: center; gap: 7px;
  /* A label broken across two lines reads as a squeezed layout rather than a
     control. Buttons here are short enough to stay on one line; a narrow
     container should make the button overflow its card, which is visible and
     fixable, rather than quietly restyle the button. */
  white-space: nowrap;
  transition: border-color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
.btn:hover { background: var(--surface-raised); border-color: var(--c-cyan); }
.btn:active { transform: scale(0.985); }
.btn.primary {
  /* The system's primary brand blue, flat. The brand gradient ran cyan to
     purple across the fill, which on a small control read as two different
     buttons wide and put white text over its own lightest stop at the left
     edge. Kept for large surfaces (stat figures, capacity bars) where the
     sweep has room to be read as one thing. */
  background: var(--accent); border-color: transparent; color: #fff;
  /* The gradient carried its own light. A flat fill has to state the lit top
     edge explicitly or it reads as a sticker rather than a raised surface. */
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
}
.btn.primary:hover {
  /* Glow and lift rather than a lighter fill: white on this blue is already
     near the contrast floor, and brightening on hover would drop it below. */
  box-shadow: var(--glow-blue), inset 0 1px 0 rgba(255, 255, 255, 0.22);
  transform: translateY(-1px);
}
.btn.primary:active { background: var(--accent-press); transform: scale(0.985); box-shadow: none; }
.btn.danger { color: var(--danger); }
.btn.danger:hover { background: var(--danger-bg); border-color: var(--danger); }
.btn.small { padding: 5px 10px; font-size: var(--text-xs); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; }
/* An icon inside a button is decoration, never the click target.
   Without this, clicking the glyph makes event.target the <svg>, and the
   ~50 handlers that guard a double-submit with `e.target.disabled = true`
   set an expando on an SVGElement instead — silently doing nothing, so the
   second click fires a second request. Invisible until someone
   double-clicks Save on a slow connection and gets two records. */
.btn > svg, .btn > * { pointer-events: none; }

/* Cards / panels */
.panel {
  background: var(--glass-sheen), var(--glass-fill-panel);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  border-radius: 20px;
  padding: 18px;
  box-shadow: var(--elev-1);
}
.grid { display: grid; gap: 16px; }
.section-title {
  font-family: var(--font-body); font-size: var(--text-xs); font-weight: var(--fw-semibold);
  text-transform: uppercase; letter-spacing: var(--ls-eyebrow); color: var(--text-3); margin: 0 0 12px;
}

/* Forms */
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
.field label { font-size: var(--text-xs); color: var(--text-dim); font-weight: var(--fw-semibold); }
.field .hint { font-size: var(--text-xs); color: var(--text-faint); }
/* The one place that stays opaque: glass behind editable text hurts legibility. */
input, select, textarea {
  background: var(--bg); border: 1px solid var(--border-default); border-radius: var(--radius-sm);
  color: var(--text); padding: 9px 11px; font-size: var(--text-sm); font-family: var(--font-body); width: 100%;
  transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--c-cyan); box-shadow: var(--glow-focus); }
/* Disabled fields are read-only display, not muted inputs — drop the box
   chrome entirely so the value reads as plain text at full contrast. */
input:disabled, select:disabled {
  background: transparent; border-color: transparent; padding-left: 0; padding-right: 0;
  color: var(--text-1); opacity: 1; cursor: default;
}
textarea { resize: vertical; min-height: 70px; }
/* The browser's default date-picker icon is dark and nearly invisible on our
   dark inputs — invert it so it reads clearly, and brighten further on hover. */
input[type="date"]::-webkit-calendar-picker-indicator {
  filter: invert(1) brightness(1.3);
  cursor: pointer;
  border-radius: var(--radius-xs);
  padding: 2px;
}
input[type="date"]::-webkit-calendar-picker-indicator:hover { filter: invert(1) brightness(1.6); }
.row { display: flex; gap: 14px; flex-wrap: wrap; }
.row > .field { flex: 1; min-width: 160px; }

/* Tables */
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--border-subtle); }
th { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: var(--ls-wide); color: var(--text-dim); }
tr:hover td { background: var(--surface-glass); }

/* Products table: the Product column needs real room for a name + code
   (unlike the narrow numeric columns beside it), so it's given a fixed
   layout with each column's width pinned explicitly rather than the
   browser auto-sizing every column off its narrowest content. */
.products-table { table-layout: fixed; }
.products-table th:first-child, .products-table td:first-child { width: 46%; }
.products-table th:not(:first-child):not(:last-child), .products-table td:not(:first-child):not(:last-child) { width: 12%; }
.products-table th:last-child, .products-table td:last-child { width: 18%; }

/* A product's name + code, shown as a small self-contained card so a long
   name/code wraps and stays fully readable instead of clipping in a
   fixed-width single-line field. */
.product-card { padding: 8px 10px; border-radius: var(--radius-sm); background: var(--surface-glass); }
.product-card-name { font-size: var(--text-base); font-weight: var(--fw-semibold); overflow-wrap: break-word; }
.product-card-code { font-size: var(--text-xs); color: var(--text-2); margin-top: 4px; overflow-wrap: break-word; }

/* Badges & chips */
.badge { display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; border-radius: var(--radius-pill); font-size: var(--text-xs); font-weight: var(--fw-semibold); letter-spacing: -0.006em; white-space: nowrap; }
/* Semantic badge states, from the kit's desaturated palette rather than the
   Tailwind-family literals these were inlined with. */
.badge.running      { background: var(--success-bg); color: var(--success); }
.badge.issue        { background: var(--danger-bg);  color: var(--danger); }
.badge.qa-held      { background: rgba(122,64,200,0.18); color: #c4a5f0; }
.badge.qa-released  { background: var(--success-bg); color: var(--success); }
.badge.waiting      { background: var(--warning-bg); color: var(--warning); }
.badge.location     { background: rgba(var(--loc-rgb, 148,163,184), 0.18); color: rgb(var(--loc-rgb, 148,163,184)); }
.badge.open { background: rgba(255,255,255,0.10); color: var(--text-2); }
.badge.in_progress { background: var(--warning-bg); color: var(--warn); }
.badge.completed { background: var(--success-bg); color: var(--ok); }
.line-chip { display: inline-flex; align-items: center; gap: 6px; font-size: var(--text-xs); color: var(--text-dim); }
.line-dot { width: 9px; height: 9px; border-radius: var(--radius-xs); }

/* Dashboard columns */
.board { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; align-items: start; }
.board-col {
  background: var(--glass-sheen), var(--glass-fill-panel);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  border-radius: 20px;                 /* tables & summary cards, per the radius scale */
  box-shadow: var(--elev-1);
  padding: 12px;
}
.board-col h3 { margin: 4px 6px 12px; font-size: var(--text-sm); font-family: var(--font-body); font-weight: var(--fw-semibold); letter-spacing: normal; display: flex; justify-content: space-between; }
.board-col h3 .count { color: var(--text-faint); }
.wo-card {
  position: relative;
  background: var(--glass-sheen), var(--glass-fill-card);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  border-radius: 22px;                 /* position cards, per the radius scale */
  box-shadow: var(--elev-1);
  padding: 13px 14px;
  margin-bottom: 10px;
  cursor: pointer;
  /* The shadow spreads slower than the lift, so the card reads as leaving the
     surface rather than being scaled (glass system §4.3). */
  transition: transform 220ms var(--ease-out),
              box-shadow 320ms var(--ease-out),
              border-color 260ms var(--ease-out);
  will-change: transform;
}
/* Location enters as a header wash, never a left stripe (§6). It also has to be
   distinguishable from the line colours below, which are drawn from the very
   same six hexes — a wash and a hard-edged bar read as different vocabularies. */
.wo-card::before {
  content: "";
  position: absolute; inset: 0 0 auto 0; height: 62px;
  border-radius: 22px 22px 0 0;
  background: linear-gradient(180deg, rgba(var(--loc-rgb, 148,163,184), 0.14), transparent 78%);
  pointer-events: none;
}
.wo-card > * { position: relative; }
.wo-card:hover  { transform: translateY(-3px); border-color: var(--glass-border-hover); box-shadow: var(--elev-2-hover); }
.wo-card:active { transform: scale(0.985); box-shadow: var(--elev-1); }
.wo-card-flagged { border-color: rgba(229,72,77,0.45); }
.wo-card-selected { border-color: var(--c-cyan); box-shadow: var(--elev-1), 0 0 0 1px rgba(63,176,232,0.45); }
.wo-card .lot { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--accent-2); letter-spacing: -0.006em; }
.wo-card .prod { font-weight: var(--fw-semibold); margin: 3px 0; letter-spacing: -0.022em; color: var(--text-1); }
.wo-card .meta { font-size: var(--text-xs); color: var(--text-dim); letter-spacing: -0.006em; }
/* Badges wrap instead of crushing each other — a released-and-waiting order
   carries four at once. */
.wo-badges { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; justify-content: flex-end; }

.wo-lines { display: flex; gap: 4px; margin-top: 10px; }
.wo-lines .seg { position: relative; overflow: hidden; height: 6px; border-radius: var(--radius-xs); flex: 1; }
.wo-lines .seg.is-done { opacity: 0.4; }   /* finished stages recede */

/* Stat tiles */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; margin-bottom: 20px; }
.stat {
  background: var(--glass-sheen), var(--glass-fill-card);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  border-radius: 18px;                 /* stat tiles, per the radius scale */
  box-shadow: var(--elev-1);
  padding: 16px;
  transition: transform 220ms var(--ease-out), box-shadow 320ms var(--ease-out),
              border-color 260ms var(--ease-out);
}
.stat:hover { transform: translateY(-2px); border-color: var(--glass-border-hover); box-shadow: var(--elev-2); }
.stat .n {
  font-family: var(--font-display); font-weight: var(--fw-black); font-size: var(--text-3xl); line-height: var(--lh-tight);
  /* Oversized gradient figures take the tightest tracking in the system. */
  letter-spacing: -0.035em;
  font-variant-numeric: tabular-nums;
  /* Widened so each tile samples a *different* slice of the brand gradient —
     at the default size all six render nearly the same colour. The per-tile
     offset is set inline from JS (--gi). */
  background: var(--gradient-brand);
  background-size: 320% 100%;
  background-position: calc(var(--gi, 0) * 20%) 0;
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.stat .l { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: var(--ls-eyebrow); color: var(--text-3); margin-top: 6px; }

/* Calendar */
.cal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; flex-wrap: wrap; gap: 10px; }

/* A month is a real calendar: seven columns, or the week alignment that makes
   it readable is gone.
   minmax(0, 1fr) rather than 1fr — a bare `1fr` is `minmax(auto, 1fr)`, and an
   `auto` minimum is the column's max-content width. The chips inside are
   `white-space: nowrap`, so every column was being forced out to the width of
   its longest lot label and the whole page scrolled sideways. Zero as the
   minimum lets the columns shrink and hands the overflow to the chip's own
   text-overflow, which is where it belonged. */
.cal-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 6px; }
.cal-dow { font-size: var(--text-xs); color: var(--text-dim); text-transform: uppercase; letter-spacing: var(--ls-wide); text-align: center; padding: 4px 0; }
.cal-cell {
  background: var(--glass-sheen), var(--glass-fill-card);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-md);
  box-shadow: var(--elev-1);
  min-height: 96px;
  padding: 8px;
  min-width: 0;                        /* so a long chip cannot widen the track */
  transition: transform 220ms var(--ease-out), box-shadow 320ms var(--ease-out),
              border-color 260ms var(--ease-out);
}
.cal-cell:hover { transform: translateY(-2px); border-color: var(--glass-border-hover); box-shadow: var(--elev-2); }
.cal-cell.other { opacity: 0.4; }
.cal-cell.today { border-color: var(--c-cyan); box-shadow: var(--elev-1), 0 0 0 1px rgba(63,176,232,0.35); }
.cal-cell.blocked {
  background: repeating-linear-gradient(135deg, var(--bg-2), var(--bg-2) 8px, rgba(229,72,77,0.08) 8px, rgba(229,72,77,0.08) 16px);
  border-color: rgba(229,72,77,0.35);
}
/* A week is only seven days, so it does not need — and at this width cannot
   carry — a rigid seven-across grid. It becomes a wrapping card deck, the same
   auto-fit shape the Line Status board uses, so it reflows instead of forcing
   the page sideways. Each card names its own weekday, which is what lets the
   shared header row go. */
.cal-grid.week { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; }
.cal-grid.week .cal-dow { display: none; }
.cal-grid.week .cal-cell { min-height: 190px; border-radius: 18px; padding: 12px; }
.cal-cell.week .cal-date { font-size: var(--text-sm); color: var(--text-1); font-weight: var(--fw-semibold); }
.cal-date { font-size: var(--text-xs); color: var(--text-dim); margin-bottom: 4px; }
.cal-blocked-label { font-size: 10px; text-transform: uppercase; letter-spacing: var(--ls-eyebrow); color: #fca5a5; margin-bottom: 4px; }
/* A day's work reads as a stack of small cards rather than one-line chips:
   same material as the Gantt tiles and the dashboard cards, so an order looks
   like the same object wherever it appears. */
.cal-chip {
  max-width: 100%;
  margin-bottom: 5px;
  padding: 5px 8px;
  border-radius: 9px;
  color: var(--ink-950);
  cursor: pointer;
  overflow: hidden;
  background-color: rgb(var(--chip-rgb));
  background-image: var(--glass-sheen-strong);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.28),
    inset 0 0 0 1px rgba(0,0,0,0.10),
    0 1px 2px rgba(0,0,0,0.28),
    0 6px 14px -10px rgba(0,0,0,0.6);
  transition: transform 180ms var(--ease-out), box-shadow 240ms var(--ease-out), filter 180ms var(--ease-out);
}
.cal-chip:hover {
  transform: translateY(-2px);
  filter: brightness(1.06);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.34),
    inset 0 0 0 1px rgba(0,0,0,0.12),
    0 2px 4px rgba(0,0,0,0.30),
    0 12px 24px -10px rgba(0,0,0,0.66),
    0 0 0 2px rgba(var(--chip-rgb), 0.34);
}
.cal-chip:active { transform: translateY(0) scale(0.98); }
.cc-top { display: flex; align-items: baseline; gap: 5px; min-width: 0; }
.cc-lot { font-size: 11px; font-weight: var(--fw-bold); letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cc-kg { margin-left: auto; flex-shrink: 0; font-size: 9.5px; font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; color: rgba(0,0,0,0.74); }
.cc-sub { font-size: 9.5px; font-weight: var(--fw-medium); line-height: 1.25; color: rgba(0,0,0,0.64); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Month cells are ~85px wide, where the lot id and the kg cannot share a line
   — and the id truncates from the right, which is precisely where the part
   that distinguishes one round from another lives ("260904-R1" became "26…").
   So in a narrow card the lot gets the first line to itself, kg drops beneath
   it, and the product name goes: it is identical across every round of an
   order and is in the tooltip either way. Keyed to the card's own width, since
   a month cell is narrow regardless of how wide the window is. */
.cal-chip { container-type: inline-size; }
@container (max-width: 118px) {
  .cc-top { flex-wrap: wrap; row-gap: 0; }
  .cc-kg { margin-left: 0; }
  .cc-sub { display: none; }
}

/* Gantt */
.gantt { overflow-x: auto; border: 1px solid var(--border-subtle); border-radius: var(--radius-md); }
/* An explicit min-width, NOT width:max-content. Under max-content the `1fr`
   day columns below resolve to each tile's own max-content width, so one long
   product name stretched its whole day column and only two days fit on screen.
   Sized from the day count instead, the columns get a definite width to divide
   up: the chart fills the pane when a month fits and scrolls when it doesn't,
   and either way every day column is the same width. The sticky lane labels
   below are what keep that scroll usable. */
.gantt-inner { min-width: calc(150px + (var(--days) * var(--day-w))); }
/* The one template every layer shares — axis, gridlines and tiles. Defined
   once so a tile can never land in a different column than its date label. */
.gantt-axis .ticks,
.gantt-track,
.gantt-daygrid { display: grid; grid-template-columns: repeat(var(--days), minmax(var(--day-w), 1fr)); }
.gantt-axis { display: flex; border-bottom: 1px solid var(--border-subtle); position: sticky; top: 0; background: var(--bg-2); }
/* Pinned so the line a card belongs to stays readable once the chart is
   scrolled sideways — which now happens on any viewport under the inner's
   720px, and a card whose lane you can't see is missing half its meaning.
   Needs an opaque background and a z-index above the bars to scroll over
   them cleanly. */
.gantt-axis .lane-label { width: 150px; flex-shrink: 0; padding: 8px 12px; font-size: var(--text-xs); color: var(--text-dim); position: sticky; left: 0; z-index: 7; background: var(--bg-2); }
.gantt-axis .ticks { flex: 1; }
.gantt-tick {
  font-size: 10px; color: var(--text-faint);
  padding: 7px 4px; border-left: 1px solid var(--border-subtle);
  display: flex; flex-direction: column; align-items: center; gap: 1px;
}
.gt-dow { text-transform: uppercase; letter-spacing: var(--ls-eyebrow); font-size: 9px; }
.gt-day { font-size: 12px; font-weight: var(--fw-semibold); color: var(--text-2); font-variant-numeric: tabular-nums; }
.gantt-tick.weekend { background: rgba(255,255,255,0.022); }
.gantt-tick.today .gt-day { color: var(--c-blue); }
.gantt-tick.today .gt-dow { color: var(--c-blue); }
.gantt-lane { display: flex; border-bottom: 1px solid var(--border-subtle); }
.gantt-lane .lane-label {
  width: 150px; flex-shrink: 0; padding: 12px;
  display: flex; align-items: center; gap: 8px;
  font-size: var(--text-xs); font-weight: var(--fw-semibold);
  position: sticky; left: 0; z-index: 6;
  background: var(--bg);
  border-right: 1px solid var(--border-subtle);
}
.gantt-track {
  flex: 1; position: relative;
  grid-auto-rows: 54px;
  row-gap: 8px;
  padding: 8px 0;
  min-height: 70px;
  align-content: start;
}
/* Day separators drawn as their own grid on the same template. A background
   gradient can't do this: the column width is `1fr`, so it isn't known until
   after layout. */
.gantt-daygrid { position: absolute; inset: 0; pointer-events: none; }
.gantt-daycell { border-left: 1px solid var(--border-subtle); }
.gantt-daycell.weekend { background: rgba(255,255,255,0.022); }
.gantt-daycell.today { background: rgba(59,130,246,0.07); box-shadow: inset 1px 0 0 rgba(59,130,246,0.45); }

/* A work-order tile is a card, not a ribbon.
   ---------------------------------------------------------------------------
   A tile spans whole day columns — `grid-column: <start day> / span <days>`
   from gantt.js — so it begins at the left edge of the day it starts on and
   ends at the right edge of the day it ends on, never past either. Duration
   inside a day is not drawn, deliberately: sub-day proportionality is what
   used to render a 12-minute nanoemulsion pass as a 4px sliver with its label
   stripped off. A day column is the unit of truth here. */
.gantt-bar {
  position: relative;
  /* 3px of side margin keeps two tiles on adjacent days from touching while
     leaving the tile inside the day columns it spans. */
  margin: 0 3px;
  min-width: 0;
  box-sizing: border-box;
  border-radius: 12px;
  padding: 7px 10px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  overflow: hidden;
  cursor: pointer;
  color: var(--ink-950);
  /* Saturated line colour carries the identity; the sheen and elevation stack
     keep it from reading as a flat sticker (glass system §3 guardrail). */
  background-color: rgb(var(--bar-rgb));
  background-image: var(--glass-sheen-strong);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.30),
    inset 0 0 0 1px rgba(0,0,0,0.10),
    0 1px 2px rgba(0,0,0,0.30),
    0 10px 22px -14px rgba(0,0,0,0.65);
  transition: transform 180ms var(--ease-out), box-shadow 260ms var(--ease-out), filter 180ms var(--ease-out);
}
.gantt-bar:hover {
  transform: translateY(-2px);
  filter: brightness(1.06);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.36),
    inset 0 0 0 1px rgba(0,0,0,0.12),
    0 2px 4px rgba(0,0,0,0.32),
    0 18px 34px -14px rgba(0,0,0,0.70),
    0 0 0 3px rgba(var(--bar-rgb), 0.28);
  z-index: 4;
}
.gantt-bar:active { transform: translateY(0) scale(0.99); }

.gb-top { display: flex; align-items: center; gap: 6px; min-width: 0; }
.gb-lot {
  font-size: 12.5px;
  font-weight: var(--fw-bold);
  letter-spacing: -0.012em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Pushed to the trailing edge and never compressed — kg is the number people
   scan a schedule for, so it holds its space while the name gives way. */
.gb-kg {
  margin-left: auto;
  flex-shrink: 0;
  font-size: 10.5px;
  font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
  padding: 1px 6px;
  border-radius: 999px;
  background: rgba(0,0,0,0.16);
  color: rgba(0,0,0,0.78);
}
.gb-sub {
  font-size: 11px;
  font-weight: var(--fw-medium);
  line-height: 1.25;
  color: rgba(0,0,0,0.66);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.gb-state { flex-shrink: 0; display: inline-flex; align-items: center; }
.gb-state svg { display: block; }

/* Keyed to the TILE's width, not the viewport's — a single-day tile is narrow
   whatever the screen size, so a media query would answer the wrong question.
   As room runs out the product name goes first (it repeats across every tile
   of an order and is in the tooltip); kg drops to its own line rather than
   being dropped, since it is the number people scan a schedule for. */
.gantt-bar { container-type: inline-size; }
@container (max-width: 168px) {
  .gb-sub { display: none; }
  .gb-top { flex-wrap: wrap; row-gap: 0; }
  .gb-kg { margin-left: 0; }
  /* Tighter gutters and type so an 88px day column still fits a whole lot id
     — truncating it would hide the round number, the only part that differs. */
  .gantt-bar { padding: 5px 7px; }
  .gb-lot { font-size: 11.5px; }
  .gb-kg { font-size: 10px; }
}
/* A single-day tile in an 88px column: shave the gutters and the type so a
   full lot id still fits, since the round suffix is the whole point of it. */
@container (max-width: 104px) {
  .gantt-bar { padding: 5px 6px; }
  .gb-lot { font-size: 10.5px; letter-spacing: -0.02em; }
  .gb-kg { font-size: 9.5px; padding: 0 4px; }
}
@container (max-width: 58px) {
  .gb-kg { display: none; }
}

/* Running: the same breathing halo the dashboard cards use, so "live" reads
   the same way on every screen. */
.gantt-bar.is-running { animation: gantt-bar-live 2600ms var(--ease-in-out) infinite alternate; }
@keyframes gantt-bar-live {
  from { box-shadow: inset 0 1px 0 rgba(255,255,255,0.30), inset 0 0 0 1px rgba(0,0,0,0.10), 0 1px 2px rgba(0,0,0,0.30), 0 0 0 2px rgba(255,255,255,0.16); }
  to   { box-shadow: inset 0 1px 0 rgba(255,255,255,0.40), inset 0 0 0 1px rgba(0,0,0,0.10), 0 1px 2px rgba(0,0,0,0.30), 0 0 0 5px rgba(255,255,255,0.30); }
}
/* Finished stages recede rather than disappear — still placed, no longer
   competing for attention with what is still to run. */
.gantt-bar.is-done { filter: saturate(0.55) brightness(0.82); }
.gantt-bar.is-done:hover { filter: saturate(0.8) brightness(0.95); }

/* Trimmed at a window edge: square off the cut side so a tile that continues
   past this page of the chart doesn't read as one that ends here. */
.gantt-bar.is-cut-start { border-top-left-radius: 3px; border-bottom-left-radius: 3px; margin-left: 0; }
.gantt-bar.is-cut-end { border-top-right-radius: 3px; border-bottom-right-radius: 3px; margin-right: 0; }

@media (max-width: 720px) {
  .gantt-bar { border-radius: 10px; padding: 6px 8px; }
  .gantt-track { grid-auto-rows: 48px; }
  .gb-lot { font-size: 11.5px; }
  .gb-sub { font-size: 10px; }
}

/* Capacity monitor */
.cap-chart-wrap { position: relative; height: 160px; margin-top: 6px; }
.cap-chart { position: absolute; inset: 0; display: flex; align-items: flex-end; gap: 2px; }
.cap-bar-wrap { flex: 1; height: 100%; display: flex; align-items: flex-end; min-width: 3px; }
.cap-bar { width: 100%; border-radius: var(--radius-xs) var(--radius-xs) 0 0; transition: height var(--dur) var(--ease-out); }
.cap-bar-wrap:hover .cap-bar { filter: brightness(1.15); }
.cap-capline { position: absolute; left: 0; right: 0; border-top: 2px dashed var(--warn); z-index: 2; pointer-events: none; }
.cap-axis { display: flex; justify-content: space-between; font-size: var(--text-xs); color: var(--text-faint); margin-top: 6px; }

/* Modal */
#modal-root:empty { display: none; }
/* The scrim *desaturates* what's behind it while the panel *saturates* — that
   opposition is what sells the modal as a physical pane in front of the page. */
.modal-overlay { position: fixed; inset: 0; background: rgba(7,6,13,0.62); backdrop-filter: blur(28px) saturate(0.85); -webkit-backdrop-filter: blur(28px) saturate(0.85); display: grid; place-items: center; z-index: var(--z-modal); padding: 20px; perspective: 1800px; animation: modal-scrim-in 200ms var(--ease-out) both; }
/* Flip-open.
   ---------------------------------------------------------------------------
   Hinged on the left edge rather than spun about its centre, so it reads as a
   card being opened rather than a card tumbling. The rotation lands slightly
   before the opacity does, which is what keeps the half-open frames from
   looking like a glitch.

   `perspective` lives on .modal-overlay, not in the transform: an ancestor
   with a transform or filter becomes the backdrop root for its descendants,
   and putting it on .modal itself made the panel's own backdrop-filter sample
   nothing and go flat. Kept separate, both survive.

   Every dialog in the app is built by openModal in util.js, so this covers
   work orders opened from the dashboard, calendar tiles, and Gantt tiles
   alike — there is no per-page flip code. */
@keyframes modal-flip-in {
  from { opacity: 0; transform: rotateY(-72deg) scale(0.94); }
  55%  { opacity: 1; }
  to   { opacity: 1; transform: rotateY(0deg) scale(1); }
}
@keyframes modal-scrim-in { from { opacity: 0; } to { opacity: 1; } }

/* Not an entrance: a stacked dialog, a restored parent, or a panel reopened
   mid-flow. Set by openModal — see its comment for why each case matters. */
.modal.no-flip { animation: none; }

.modal {
  transform-origin: left center;
  animation: modal-flip-in 460ms cubic-bezier(0.22, 1, 0.36, 1) both;
  will-change: transform, opacity;
  background: var(--glass-sheen-strong), var(--glass-fill-panel);
  backdrop-filter: var(--glass-blur-panel);
  -webkit-backdrop-filter: var(--glass-blur-panel);
  border: 1px solid rgba(255,255,255,0.12);
  border-radius: 24px;
  width: 100%; max-width: 560px; max-height: 90vh; overflow-y: auto;
  box-shadow: var(--elev-3);
}
.modal-header { display: flex; justify-content: space-between; align-items: center; padding: 18px 20px; border-bottom: 1px solid var(--border-subtle); }
.modal-header h2 { margin: 0; font-size: var(--text-lg); font-family: var(--font-display); font-weight: var(--fw-black); }
.modal-body { padding: 20px; }
.modal-footer { padding: 16px 20px; border-top: 1px solid var(--border-subtle); display: flex; justify-content: flex-end; gap: 10px; }
.close-x { background: none; border: none; color: var(--text-dim); font-size: 22px; cursor: pointer; transition: color var(--dur-fast) var(--ease-out); }
.close-x:hover { color: var(--text-1); }

/* Chat widget */
.chat-launcher {
  position: fixed; bottom: 24px; right: 24px; z-index: var(--z-overlay);
  width: 52px; height: 52px; border-radius: 50%; border: none;
  background: var(--accent); color: #fff; font-size: 20px; cursor: pointer;
  box-shadow: var(--shadow-lg); display: grid; place-items: center;
  transition: transform var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
.chat-launcher:hover { transform: scale(1.06); box-shadow: var(--glow-blue); }
.chat-panel {
  position: fixed; bottom: 24px; right: 24px; z-index: var(--z-overlay);
  width: 380px; max-width: calc(100vw - 32px);
  height: 560px; max-height: calc(100vh - 48px);
  background: var(--surface-card); border: 1px solid var(--border-default);
  border-radius: var(--radius-xl); box-shadow: var(--shadow-xl);
  display: flex; flex-direction: column; overflow: hidden;
}
.chat-panel[hidden] { display: none; }
.chat-panel-header {
  display: flex; justify-content: space-between; align-items: center;
  padding: 14px 16px; border-bottom: 1px solid var(--border-subtle);
  font-family: var(--font-display); font-weight: var(--fw-black); font-size: var(--text-sm);
}
.chat-thread { flex: 1; overflow-y: auto; padding: 14px 16px; }
.chat-input-row { display: flex; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--border-subtle); }
.chat-input-row textarea { flex: 1; resize: none; min-height: 0; }
.chat-bubble { margin-bottom: 12px; }
.chat-bubble.user { text-align: right; }
.chat-bubble .msg {
  display: inline-block; max-width: 88%; text-align: left; white-space: pre-wrap;
  padding: 9px 12px; border-radius: var(--radius-md); font-size: var(--text-sm);
}
.chat-bubble.user .msg { background: var(--accent); color: #fff; }
.chat-bubble.assistant .msg { background: var(--bg-2); border: 1px solid var(--border-subtle); }
.chat-action-card {
  margin-top: 8px; padding: 10px 12px; border-radius: var(--radius-sm);
  border: 1px solid var(--border-default); background: var(--bg-2); font-size: var(--text-sm);
}

/* Toast */
#toast-root { position: fixed; bottom: 20px; right: 20px; display: flex; flex-direction: column; gap: 8px; z-index: var(--z-toast); }
.toast { background: var(--surface-raised); border: 1px solid var(--border-default); border-left: 3px solid var(--accent); border-radius: var(--radius-sm); padding: 12px 16px; box-shadow: var(--shadow-lg); font-size: var(--text-sm); max-width: 340px; }
.toast.ok { border-left-color: var(--ok); }
.toast.bad { border-left-color: var(--danger); }

/* Misc */
.empty { text-align: center; color: var(--text-faint); padding: 40px 20px; }
.muted { color: var(--text-dim); }
.mono { font-family: var(--font-mono); }
.right { text-align: right; }
.spread { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
.mb-16 { margin-bottom: 16px; }
.checkline { display: flex; align-items: center; gap: 8px; padding: 8px 10px; background: var(--bg); border: 1px solid var(--border-default); border-radius: var(--radius-sm); margin-bottom: 6px; }
.checkline input { width: auto; }
.schedule-preview { background: var(--bg-2); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); padding: 14px; }
/* flex-wrap is load-bearing: a running line carries two more controls than an
   idle one (Stop and Complete, versus just Start), and without wrapping the
   row had nowhere to put them — Issue was pushed past the panel's right edge
   and clipped. */
.preview-stage { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--border-subtle); flex-wrap: wrap; }
.preview-stage:last-child { border-bottom: none; }
/* min-width: 0 is the other half. A flex item defaults to min-width: auto and
   refuses to shrink below its content, so a long stage label kept its full
   width and shoved the buttons out rather than letting them wrap. */
.preview-stage .lab { flex: 1 1 auto; min-width: 0; }
.preview-stage .dur { color: var(--text-dim); font-size: var(--text-xs); white-space: nowrap; }

/* The controls travel together. Grouped rather than left as loose siblings so
   they wrap onto a second line as one block — individually they would break
   mid-group and leave one button stranded under the label. margin-left: auto
   keeps them right-aligned while they still share the first line. */
.preview-stage .stage-actions {
  display: flex; align-items: center; gap: 6px;
  flex-wrap: wrap; margin-left: auto;
}

/* Alerts (warnings / conflicts) */
.alert { border-radius: var(--radius-sm); padding: 10px 12px; margin-top: 10px; font-size: var(--text-xs); border: 1px solid var(--border-subtle); }
.alert-title { font-weight: var(--fw-bold); margin-bottom: 4px; }
.alert-line { line-height: var(--lh-normal); }
.alert-conflict { background: var(--danger-bg); border-color: rgba(229,72,77,0.45); color: #fca5a5; }
.alert-warn { background: var(--warning-bg); border-color: rgba(232,163,61,0.4); color: #fcd34d; }
.alert-info { background: var(--info-bg); border-color: rgba(63,176,232,0.4); color: #bae0f7; }

@media (max-width: 900px) {
  .stats { grid-template-columns: repeat(2, 1fr); }
  .board { grid-template-columns: 1fr; }
}

/* Narrow screens — phones and the small mounted tablets the shop floor
   actually uses. The sidebar is a fixed 232px column with no breakpoint, so
   below this width it took a third of a phone screen and left card text
   wrapping one word per line. It becomes a normal block at the top of the
   page instead, with the nav scrolling sideways as a row of chips. Kiosk and
   My Work Orders are the two pages genuinely meant for floor hardware, which
   is why this is a layout change rather than a nicety. */
@media (max-width: 760px) {
  #app { flex-direction: column; }

  .sidebar {
    width: 100%;
    height: auto;
    position: static;
    flex-shrink: 1;
    padding: 12px 14px;
    border-right: none;
    border-bottom: 1px solid var(--border-subtle);
  }
  /* On a phone the sidebar is a horizontal strip, so the mark sits at its
     left rather than centred, and takes a fixed size — the fluid clamp is
     sized for a tall rail and would swell to fill the full page width here. */
  .brand { justify-content: flex-start; padding: 0 0 10px; }
  .soll-wordmark--fluid { font-size: 1.9rem; }

  #nav {
    flex-direction: row;
    overflow-x: auto;
    gap: 6px;
    padding-bottom: 4px;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  #nav::-webkit-scrollbar { display: none; }
  .nav-item { white-space: nowrap; flex-shrink: 0; }
  /* The desktop rules above turn the nav into a vertical scroller with a
     divider; here it is a horizontal chip strip inside a sidebar that grows to
     fit, so neither applies. */
  #nav { overflow-y: visible; border-bottom: none; margin-bottom: 0; }
  .sidebar { overflow: visible; }

  /* The footer blocks (location switcher, connection, user) sit under the nav
     rather than pinned to the bottom of a full-height column. */
  .sidebar-footer { margin-top: 8px; }

  .topbar { padding: 14px 16px; flex-wrap: wrap; gap: 10px; }
  .topbar h1 { font-size: var(--text-xl); }
  .view { padding: 16px; }

  .stats { grid-template-columns: 1fr 1fr; gap: 10px; }

  /* A modal at this width should use the screen, not float in the middle of
     it — the work-order detail view is long and every gutter pixel counts. */
  .modal-overlay { padding: 0; place-items: stretch; }
  .modal { max-width: none; max-height: 100vh; border-radius: 0; border: none; }
  .modal-footer { flex-wrap: wrap; }
}

/* Very narrow — a single column everywhere, including the paired stat grid. */
@media (max-width: 460px) {
  .stats { grid-template-columns: 1fr; }
}

/* TV Mode — full-viewport display for Line Status / Stats, meant to be
   toggled on and left running on a shop-floor or office TV/monitor (see
   util.js's enterTvView). Reuses the existing .panel/.badge/.schedule-preview
   classes scaled up via descendant selectors below, rather than duplicating
   them, so TV mode automatically stays visually in sync with the normal
   page. clamp() keeps text readable across very different screen sizes
   (a 1080p monitor vs. a big 4K wall TV) without separate breakpoints. */
.tv-view {
  position: fixed; inset: 0; z-index: 500;
  background: var(--bg);
  overflow-y: auto;
  padding: 48px 64px;
}
/* Top centre: on a wall-mounted screen the corner is the hardest place to
   find and to reach, and this is the one control the view has. Kept at a
   readable opacity for the same reason — it was at 0.4, which on a TV across
   a room is effectively invisible. */
.tv-exit-btn {
  position: fixed; top: 16px; left: 50%; transform: translateX(-50%); z-index: 501;
  display: inline-flex; align-items: center; gap: 8px;
  background: rgba(255,255,255,0.10);
  backdrop-filter: var(--glass-blur-chrome);
  -webkit-backdrop-filter: var(--glass-blur-chrome);
  border: 1px solid var(--glass-border);
  color: var(--text-2);
  padding: 11px 22px;
  border-radius: var(--radius-pill);
  font-size: 15px;
  font-weight: var(--fw-semibold);
  cursor: pointer;
  opacity: 0.75;
  box-shadow: var(--elev-float);
  transition: opacity .2s var(--ease-out), background .2s var(--ease-out);
}
.tv-exit-btn:hover { opacity: 1; background: rgba(255,255,255,0.16); }

/* Extra top padding clears the now-centred exit button. */
.tv-header { display: flex; align-items: center; justify-content: space-between; margin: 34px 0 30px; flex-wrap: wrap; gap: 12px; }
.tv-header h1 { font-size: clamp(28px, 3vw, 44px); font-weight: var(--fw-black); margin: 0; }
/* The clock is half the point of a wall display — it says how current what
   you're looking at is. Full brightness, tabular figures so the digits don't
   jitter every second, and the date above it. */
.tv-clock { text-align: right; line-height: 1.05; }
.tv-clock-time {
  display: block;
  font-size: clamp(30px, 3.4vw, 52px);
  font-weight: var(--fw-black);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
  color: var(--text-1);
}
.tv-clock-date {
  display: block;
  font-size: clamp(13px, 1.2vw, 18px);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  letter-spacing: var(--ls-eyebrow);
  color: var(--text-3);
  margin-top: 4px;
}

.tv-location-group { margin-bottom: 44px; padding-left: 20px; }
.tv-location-title { font-size: clamp(20px, 2vw, 28px); font-weight: var(--fw-bold); margin-bottom: 18px; }

.tv-view .section-title { font-size: clamp(16px, 1.4vw, 20px); }
.tv-view .panel { padding: 28px; border-radius: var(--radius-xl); }
.tv-view .badge { font-size: clamp(13px, 1vw, 16px); padding: 6px 14px; }
.tv-view .schedule-preview { padding: 20px; font-size: clamp(14px, 1.1vw, 17px); }

/* Stand-alone TV stat/bar-chart pieces for the Stats page's TV view — no
   existing normal-page equivalent to scale up, unlike Line Status above. */
.tv-stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 24px; margin-bottom: 40px; }
/* The four current-period totals. Fixed at four across on a wide screen so the
   day/week/month/year reading order survives — auto-fit would reflow them into
   two rows of two at some widths, which reads as two unrelated pairs. */
.tv-stats-grid-4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 1100px) { .tv-stats-grid-4 { grid-template-columns: repeat(2, 1fr); } }
.tv-stat-card { background: var(--panel-2); border: 1px solid var(--border); border-radius: var(--radius-xl); padding: 32px; text-align: center; }
.tv-stat-n { font-size: clamp(36px, 5vw, 64px); font-weight: var(--fw-black); }
.tv-stat-l { font-size: clamp(13px, 1.1vw, 17px); color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.08em; margin-top: 10px; }
.tv-bar-row { display: flex; align-items: center; gap: 18px; margin-bottom: 16px; }
.tv-bar-label { width: 170px; flex-shrink: 0; font-size: clamp(14px, 1.2vw, 18px); color: var(--text-dim); text-align: right; }
.tv-bar-track { flex: 1; height: 30px; background: var(--bg-2); border-radius: var(--radius-sm); overflow: hidden; }
.tv-bar-fill { height: 100%; background: var(--gradient-brand); border-radius: var(--radius-sm); }
.tv-bar-value { width: 120px; flex-shrink: 0; font-size: clamp(14px, 1.2vw, 18px); font-weight: var(--fw-bold); }

/* Line Status's TV View: a real TV has no way to scroll, so this is a
   no-scroll layout (see util.js's enterTvView `fit` option) — a fixed
   viewport-height flex column whose grid(s) are sized in JS (see
   lineStatus.js's _computeGridDims) to exactly fill whatever's left after
   the header, so every line's equipment is always on screen at once.
   Each .tv-tile is a CSS containment context (`container-type: size`) so
   its own text scales via cqh/cqw units to whatever size the computed grid
   actually gives it, from a handful of large tiles down to several dozen
   small ones. */
.tv-view-fit { display: flex; flex-direction: column; overflow: hidden; padding: 20px 28px; }
.tv-view-fit .tv-view-content { flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
.tv-ls-body { flex: 1; display: flex; flex-direction: column; gap: 10px; min-height: 0; overflow: hidden; }
.tv-ls-location-section { display: flex; flex-direction: column; overflow: hidden; min-height: 0; }
.tv-ls-location-title { flex: 0 0 auto; font-weight: var(--fw-bold); font-size: 15px; padding: 2px 0 6px; }
.tv-ls-grid { flex: 1; display: grid; gap: 8px; min-height: 0; overflow: hidden; }

.tv-tile {
  /* Padding here MUST be a plain unit, not cqh/cqw — this element is
     itself the query container (container-type: size below), and a
     container can't size its own padding from its own query dimensions
     (circular); browsers fall back to a much larger value, which crushed
     tiles down to a couple of usable pixels before this was fixed. */
  container-type: size;
  border-radius: var(--radius-md);
  background: var(--panel-2);
  border: 1px solid var(--border);
  padding: 6px 8px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  min-width: 0;
  min-height: 0;
  cursor: pointer;
}
.tv-tile-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 6px; flex: 0 0 auto; min-width: 0; }
.tv-tile-title { font-size: 8cqh; font-weight: var(--fw-bold); overflow: hidden; white-space: normal; word-break: break-word; min-width: 0; line-height: 1.15; }
.tv-tile-title .tv-tile-line { color: var(--text-dim); font-weight: var(--fw-regular); }
.tv-tile-badge { font-size: 8cqh; flex-shrink: 0; }
.tv-tile-badge-issue { color: #fca5a5; }
.tv-tile-badge-running { color: #4ade80; }
.tv-tile-badge-scheduled { color: #fcd34d; }
.tv-tile-badge-idle { color: var(--text-faint); }
.tv-tile-body { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 0.8cqh; overflow: hidden; min-height: 0; }
.tv-tile-lot { font-size: 12cqh; font-weight: var(--fw-black); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tv-tile-elapsed { font-size: 9cqh; font-family: var(--font-mono); font-weight: var(--fw-bold); }
.tv-tile-personnel { font-size: 7cqh; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tv-tile-more { font-size: 6.5cqh; color: var(--text-faint); }
.tv-tile-idle { font-size: 8cqh; color: var(--text-faint); }
.tv-tile-issue-lot { font-size: 9.5cqh; font-weight: var(--fw-black); color: #fca5a5; }
.tv-tile-issue-desc {
  font-size: 7.5cqh; color: #fca5a5; overflow: hidden;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.tv-tile-resolve {
  align-self: flex-start; margin-top: 0.6cqh;
  font-size: 7cqh; padding: 0.8cqh 1.6cqw;
  border-radius: var(--radius-sm); border: none; cursor: pointer;
  background: var(--danger); color: #fff;
}


/* Kiosk — the shop-floor terminal.
   Sized with clamp() rather than fixed px because the same page runs on a wall
   monitor, a mounted tablet, and someone's phone. These were inline styles at
   fixed sizes, which a media query cannot override and which pushed the
   controls off-screen on anything narrow. */
.kiosk-lot {
  font-size: clamp(20px, 5vw, 32px);
  font-weight: var(--fw-bold, 700);
  overflow-wrap: anywhere;
}
.kiosk-elapsed {
  font-size: clamp(34px, 12vw, 56px);
  font-weight: 800;
  font-family: var(--font-mono);
  margin: 16px 0;
  font-variant-numeric: tabular-nums;
}
.kiosk-alert { font-size: clamp(15px, 3.5vw, 20px); padding: clamp(14px, 4vw, 24px); text-align: center; }
.kiosk-alert-title { font-size: clamp(18px, 4.5vw, 26px); }
.kiosk-empty { text-align: center; padding: clamp(20px, 6vw, 40px); font-size: clamp(15px, 3.5vw, 20px); }
.kiosk-btn {
  font-size: clamp(15px, 3.6vw, 22px);
  padding: clamp(12px, 3vw, 18px) clamp(16px, 5vw, 32px);
}
@media (max-width: 760px) {
  /* One action per row beats four cramped ones the thumb keeps missing. */
  .kiosk-btn { width: 100%; }
}

/* ============================================================================
   Motion
   ----------------------------------------------------------------------------
   The rule from the glass system: motion either communicates state (something
   moved, opened, or is flowing) or it does not ship. Everything below reports a
   real production fact — a line is running, an order moved, a figure changed.

   prefers-reduced-motion is already handled globally by a `*` !important
   override in style-kit/tokens/base.css, so nothing here needs its own guard.
   ========================================================================== */

/* --- A work order with a line running ------------------------------------- */
/* The card carries the running line's colour as an "R, G, B" triplet set inline
   from JS (--line-rgb). A triplet plus rgba() rather than color-mix() keeps this
   working below Chrome 111 / Safari 16.2 with no fallback ladder.

   Breathing, not flashing: the halo grows and settles over 2.6s, the same tempo
   as the stage pulse below, so a card and its live segment feel like one object. */
.wo-card.is-running {
  border-color: rgba(var(--line-rgb), 0.38);
  animation: card-breathe 2600ms var(--ease-in-out) infinite alternate;
}
@keyframes card-breathe {
  from {
    box-shadow: var(--elev-1),
                0 0 0 1px rgba(var(--line-rgb), 0.16),
                0 8px 28px -12px rgba(var(--line-rgb), 0.20);
  }
  to {
    box-shadow: var(--elev-1),
                0 0 0 1px rgba(var(--line-rgb), 0.40),
                0 16px 44px -8px rgba(var(--line-rgb), 0.46);
  }
}
/* Hover still lifts a running card (transform and border-color aren't animated,
   so they apply normally); the halo keeps breathing rather than being replaced,
   because "this is live" outranks "you are pointing at it".

   Pausing the animation on hover was tried and removed: with the entrance
   composed onto the same element, pausing could strand a card mid-fade at
   opacity < 1 if the pointer landed on it during the first 420ms. */

/* --- The live stage, inside the card's line strip -------------------------- */
/* The CSS analogue of the glass system's traveling edge pulse (§4.1): a bright
   dash running along the thing it describes. Same 2.6s, same linear timing, same
   justification — direction is the information. It says *which* stage is live,
   which the card halo alone cannot when an order runs several at once. */
.wo-lines .seg.is-running::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.92) 50%, transparent 100%);
  transform: translateX(-100%);
  animation: seg-travel 2600ms linear infinite;
}
@keyframes seg-travel {
  to { transform: translateX(100%); }
}

/* --- Board entrance -------------------------------------------------------- */
/* One-shot, on navigation only. Gated in JS so it does not replay every time a
   selection checkbox rebuilds the board. */
.board.is-entering .wo-card {
  opacity: 0;
  animation: card-enter var(--dur-slow) var(--ease-out) forwards;
  animation-delay: calc(var(--i, 0) * 26ms);
}
/* A card can be entering AND live at once. `animation` is a shorthand, so the
   more specific rule would otherwise drop the halo entirely — they have to be
   listed together. They touch different properties (opacity/transform vs
   box-shadow), so order is irrelevant here. */
.board.is-entering .wo-card.is-running {
  animation: card-enter var(--dur-slow) var(--ease-out) forwards,
             card-breathe 2600ms var(--ease-in-out) infinite alternate;
  animation-delay: calc(var(--i, 0) * 26ms), 0ms;
}
@keyframes card-enter {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

/* --- A card the last reflow actually moved -------------------------------- */
/* Reflow silently rewrites dates across the board; this is the only signal that
   says which orders it touched. One pass, then it is gone. */
.wo-card.did-move {
  animation: card-flash 900ms var(--ease-out) 1;
}
/* Both animate box-shadow, so the flash is listed *second* to win for its 900ms;
   when it ends the halo — which has no fill mode — takes the property back. */
.wo-card.did-move.is-running {
  animation: card-breathe 2600ms var(--ease-in-out) infinite alternate,
             card-flash 900ms var(--ease-out) 1;
}
@keyframes card-flash {
  0%   { box-shadow: var(--elev-1), 0 0 0 0 rgba(63,176,232,0.55); border-color: var(--c-cyan); }
  60%  { box-shadow: var(--elev-1), 0 0 0 6px rgba(63,176,232,0.00); border-color: var(--c-cyan); }
  100% { box-shadow: var(--elev-1), 0 0 0 0 rgba(63,176,232,0.00); }
}

/* The entrance starts a card at opacity 0 and relies on the animation's
   `forwards` fill to bring it back. Under the global reduced-motion override
   that still resolves correctly (duration 0.001ms, fill applies) — but the
   failure mode if it ever did not is an invisible board, which is severe
   enough to be worth one defensive line. */
@media (prefers-reduced-motion: reduce) {
  .board.is-entering .wo-card { opacity: 1; }
}


/* Calendar on a narrow screen.
   A month grid has to stay seven across — that alignment is the whole point of
   a month — but at ~44px per column the chip labels truncate to "2…", which is
   noise rather than information. Below this width they become slim colour bars
   instead: the line colour and the number of them still say which lines run
   that day and how busy it is, which is what the month view is scanned for.
   The full labels are one tap away in the day detail. */
@media (max-width: 700px) {
  .cal-grid:not(.week) .cal-cell { min-height: 74px; padding: 5px 4px; }
  .cal-grid:not(.week) .cal-chip {
    height: 5px;
    padding: 0;
    margin-bottom: 3px;
    border-radius: 2px;
    font-size: 0;                      /* the label is unreadable at this width */
    overflow: hidden;
  }
  .cal-dow { font-size: 10px; letter-spacing: 0.02em; }
  .cal-date { font-size: 11px; }

  /* The week cards go one per row here, so the desktop min-height would be
     seven screens of mostly-empty card. Let them size to their contents. */
  .cal-grid.week .cal-cell { min-height: 0; padding: 10px 12px; }
  .cal-grid.week .cal-date { font-size: var(--text-sm); }
}
