/* ============================================================================
   DCM Industries ERP - design system
   Restored to the IMS visual language (2026-07-30). Light mode only.

   Faithful to ims.dcmindustries.co.in/assets/css/ims.css, with three deliberate
   departures, all recorded in docs/PROJECT_MEMORY.md:
     1. IMS repeated the same inline styles on every page (buttons, table heads,
        role badges). Those exact values are named classes here - same pixels,
        far less markup, one place to change.
     2. IMS stopped at 991/767/575. This adds 1200 and tightens
        414/375, because every screen must work at six widths (R5).
     3. Every table stays a table at every width and scrolls inside .table-wrap;
        the phone card layout was removed 2026-08-02. The PAGE never scrolls
        sideways (R9).

   Breakpoints:  <576 phone · 576-767 large phone · 768-991 tablet
                 992-1199 small laptop · ≥1200 desktop
   ========================================================================== */

:root {
  /* Brand - identical to IMS */
  --ink-900:#1e1b4b; --ink-700:#312e81; --ink-600:#3730a3;
  --accent:#4f46e5;  --accent-dark:#4338ca; --accent-soft:#a5b4fc;
  --accent-tint:#ede9fe; --accent-wash:#f0f4ff; --purple:#7c3aed;
  --violet-ink:#5b21b6;

  /* Surface */
  --page-bg:#f1f5f9; --card-bg:#fff; --card-border:#e2e8f0;
  --line:#f1f5f9; --field-border:#cbd5e1;

  /* Text */
  --body:#1e293b; --muted:#64748b; --faint:#94a3b8;

  /* Status */
  --ok:#16a34a; --ok-bg:#dcfce7; --ok-ink:#15803d;
  --warn:#ea580c; --warn-bg:#fff7ed; --warn-ink:#c2410c;
  --danger:#dc2626; --danger-bg:#fef2f2; --danger-ink:#b91c1c;
  --info-bg:#e0f2fe; --info-ink:#0369a1;

  --radius:16px; --radius-sm:10px; --radius-field:8px;
  --sidebar-w:240px;
  --shadow-card:0 1px 4px rgba(0,0,0,.04);
  --shadow-pop:0 4px 16px rgba(0,0,0,.1);
}

/* ==========================================================================
   0. Base
   ========================================================================== */
body {
  font-family: 'Segoe UI', system-ui, sans-serif;
  background: var(--page-bg);
  color: var(--body);
  margin: 0;
  overflow-x: hidden;                 /* the page body never scrolls sideways */
}

/* ⚠ DO NOT PUT `overflow-x: hidden` OR `clip` ON `html`. It was tried on
   2026-08-25 to stop a phone panning sideways, and it BROKE VERTICAL SCROLLING
   ON EVERY PAGE - measured: a 1520px document in a 667px viewport with
   `window.scrollY` stuck at 0 and `scrollingElement.scrollTop` refusing to move.

   The mechanism: `body` already carries `overflow-x: hidden`, which is what the
   viewport's scrolling is propagated FROM while the root stays fully `visible`.
   Give the root an overflow of its own and that propagation stops - the root
   becomes the scroll container, and Chrome resolves its `overflow-y: visible`
   against the clipped x axis as clipped too. The page then scrolls in neither
   direction, on every screen in the app, and nothing looks broken enough to
   investigate.

   The horizontal pan is dealt with where it actually starts - see
   `overscroll-behavior-x` on .table-wrap. */

html { -webkit-text-size-adjust: 100%; }
button, .btn, [onclick] { touch-action: manipulation; -webkit-tap-highlight-color: transparent; }
.min-w-0 { min-width: 0; }

/* ==========================================================================
   1. Sidebar - overlays the page at EVERY width; never reserves space
   ========================================================================== */
.sidebar {
  width: var(--sidebar-w);
  height: 100vh;
  height: 100dvh;                     /* accounts for mobile browser chrome */
  position: fixed; top: 0; left: 0; z-index: 300;
  background: linear-gradient(180deg, #1e1b4b 0%, #312e81 60%, #3730a3 100%);
  display: flex; flex-direction: column;
  transform: translateX(-100%); transition: transform .25s ease;
  -webkit-overflow-scrolling: touch;
}
.sidebar.open { transform: translateX(0); }

.sidebar-overlay {
  display: none; position: fixed; inset: 0; z-index: 299;
  background: rgba(0,0,0,.35); cursor: pointer;
}
.sidebar-overlay.show { display: block; }

.sidebar-brand {
  display: flex; align-items: center; gap: .6rem;
  padding: 1.5rem 1.25rem 1rem;
  border-bottom: 1px solid rgba(255,255,255,.08);
}
.logo-box {
  width: 40px; height: 40px;
  border-radius: 5px; padding: 5px;
  background: #fff; object-fit: cover; flex-shrink: 0;
  box-shadow: 0 0 24px rgba(79,70,229,.25);
}
.brand-name { color: #fff; font-weight: 700; font-size: .9rem; margin: 0; line-height: 1.2; }
.brand-sub  { color: rgba(255,255,255,.45); font-size: .72rem; }

.nav-section {
  padding: 1rem 0 0; flex: 1; overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: rgba(255,255,255,.22) transparent;
  -webkit-overflow-scrolling: touch;
}
.nav-section:hover { scrollbar-color: rgba(255,255,255,.35) transparent; }
.nav-section::-webkit-scrollbar { width: 5px; }
.nav-section::-webkit-scrollbar-track { background: transparent; }
.nav-section::-webkit-scrollbar-thumb { background: rgba(255,255,255,.22); border-radius: 3px; }
.nav-section::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,.35); }

.nav-label {
  font-size: .65rem; letter-spacing: 1.2px;
  color: rgba(255,255,255,.35); font-weight: 600;
  padding: .5rem 1.25rem .25rem; text-transform: uppercase;
}
.nav-item {
  display: flex; align-items: center; gap: .6rem;
  padding: .55rem 1.25rem;
  color: rgba(255,255,255,.65); text-decoration: none;
  font-size: .875rem; font-weight: 500;
  border-left: 3px solid transparent; transition: all .15s;
}
.nav-item:hover { color: #fff; background: rgba(255,255,255,.07); }
.nav-item.active { color: #fff; background: rgba(255,255,255,.12); border-left-color: var(--accent-soft); }
.nav-item i { font-size: 1rem; width: 20px; text-align: center; flex-shrink: 0; }
.nav-item span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Collapsible sub-menu */
.nav-group-hd {
  display: flex; align-items: center; gap: .6rem;
  padding: .55rem 1.25rem;
  color: rgba(255,255,255,.65); font-size: .875rem; font-weight: 500;
  border-left: 3px solid transparent; cursor: pointer;
  transition: all .15s; user-select: none;
}
.nav-group-hd:hover { color: #fff; background: rgba(255,255,255,.07); }
.nav-group-hd i:first-child { font-size: 1rem; width: 20px; text-align: center; }
.nav-group-arr { margin-left: auto; font-size: .7rem; transition: transform .2s; opacity: .5; }
.nav-group.open .nav-group-arr { transform: rotate(180deg); opacity: 1; }
.nav-group-body { display: none; background: rgba(0,0,0,.15); }
.nav-group.open .nav-group-body { display: block; }
.nav-child a {
  display: flex; align-items: center; gap: .6rem;
  padding: .48rem 1.25rem .48rem 2.35rem;
  color: rgba(255,255,255,.55); text-decoration: none;
  font-size: .82rem; font-weight: 500;
  border-left: 3px solid transparent; transition: all .15s;
}
.nav-child a:hover { color: #fff; background: rgba(255,255,255,.06); }
.nav-child a.active { color: #fff; background: rgba(255,255,255,.10); border-left-color: var(--accent-soft); }
.nav-child a i { font-size: .9rem; width: 18px; text-align: center; }

.sidebar-footer { padding: 1rem 1.25rem; border-top: 1px solid rgba(255,255,255,.08); }
.user-pill {
  display: flex; align-items: center; gap: .6rem;
  padding: .5rem; background: rgba(255,255,255,.06); border-radius: 10px;
}
.user-avatar {
  width: 32px; height: 32px;
  background: linear-gradient(135deg, var(--accent), var(--purple));
  border-radius: 8px;
  display: flex; align-items: center; justify-content: center;
  color: #fff; font-size: .75rem; font-weight: 700; flex-shrink: 0;
}
.user-pill .u-name { color: #fff; font-size: .8rem; font-weight: 600; line-height: 1.25; }
.user-pill .u-role { color: rgba(255,255,255,.45); font-size: .7rem; }
.signout-btn {
  display: block; width: 100%; margin-top: .5rem; text-align: center;
  background: rgba(255,255,255,.1); color: rgba(255,255,255,.8);
  border: 1px solid rgba(255,255,255,.15); border-radius: 8px;
  font-size: .82rem; font-weight: 600; padding: .28rem .5rem; cursor: pointer;
  text-decoration: none;
}
.signout-btn:hover { background: rgba(255,255,255,.16); color: #fff; }

/* ==========================================================================
   2. Layout, topbar & page body
   ========================================================================== */
/* Overlay at EVERY width - the sidebar slides over the page and never reserves
   space, so the content width is identical whether the menu is open or shut.
   Pinning it on desktop would reflow every table and chart on each toggle. */
.main { margin-left: 0; min-height: 100vh; }
/* The sticky offset for a column that has to sit clear of the sticky topbar -
   the calculator rail is the only user (app.css §15d). Measured off the shipped
   shell: the topbar is 65px from 1200px up. It is a token rather than a literal
   so the two cannot drift apart in different files. */
:root { --calc-rail-top: calc(65px + 1rem); }

.topbar {
  background: #fff; border-bottom: 1px solid var(--card-border);
  padding: .75rem 1.5rem;
  display: flex; align-items: center; justify-content: space-between; gap: .6rem;
  position: sticky; top: 0; z-index: 50;
}
.topbar h5, .page-title {
  margin: 0; font-size: 1.05rem; font-weight: 700; color: var(--ink-900); line-height: 1.25;
}
.page-sub { margin: 0; font-size: .75rem; color: var(--muted); }
.page-body { padding: 1rem; }

/* ⚠ THE MASTER PICKER IS GONE (2026-08-31). `.master-pick`, `.master-pick-btn`,
   the `.mo-tick` rules and the whole `.topbar:has(.master-pick)` media block
   went with it - the master is chosen from a rail on the screen itself now
   (`.mnav-*`, section 15f), because the masters are grouped and a dropdown
   cannot show a heading.

   ⚠ THAT MEDIA BLOCK IS WHY MASTERS WAS THE ONE SCREEN WITH A TWO-ROW TOPBAR ON
   A PHONE: it gave the right-hand cluster `width: 100%`, which wrapped the row
   whatever the left half was doing. With it gone that bar is one row like every
   other screen's. Do not reintroduce a `:has()` topbar rule for a control that
   is no longer in the topbar.

   Bulk Upload is all that is left up there. It acts on whichever master the rail
   has selected, which is exactly why it must not sit inside the control that
   selects one. Its label drops below 768px so the icon still fits beside the
   page title. */
.master-menu { min-width: 210px; padding: .3rem; }
.master-menu .dropdown-item {
  display: flex; align-items: center; gap: .55rem;
  border-radius: 8px; font-size: .85rem; padding: .45rem .55rem;
}
.master-menu .dropdown-item i:first-child { font-size: .95rem; color: var(--muted); width: 1.1em; }
.master-menu .mo-label { flex: 1 1 auto; }
.master-bulk-btn { display: inline-flex; align-items: center; gap: .4rem; white-space: nowrap; }
.master-bulk-btn .mp-caret { font-size: .7rem; opacity: .75; }
.master-bulk-btn[aria-expanded="true"] .mp-caret { transform: rotate(180deg); }
/* "Show On Dashboard" beside it - the modal footer's status switch, in a light
   pill the height of the Bulk Upload button. */
.master-dash {
  display: inline-flex; align-items: center; gap: .4rem; white-space: nowrap;
  padding: .2rem .6rem; border-radius: var(--bs-border-radius);
  background: #f8f9fa; border: 1px solid #f8f9fa;
}
.master-dash > .bi { color: var(--muted); font-size: .95rem; }
.master-dash .form-check-input { margin-left: 0; float: none; }
.master-dash .form-switch { padding-left: 0; min-height: 0; display: flex; }

/* ==========================================================================
   3. Cards
   ========================================================================== */
.section-card {
  background: var(--card-bg); border-radius: var(--radius);
  border: 1px solid var(--card-border); box-shadow: var(--shadow-card);
}
.section-card + .section-card { /* margin-top: 1rem; */ }
.card-header-custom {
  padding: 1rem 1.25rem; border-bottom: 1px solid var(--line);
  display: flex; align-items: center; justify-content: space-between;
  gap: .6rem; flex-wrap: wrap;
}
.card-header-custom h6 { margin: 0; font-weight: 700; color: var(--ink-900); font-size: .9rem; }
.card-header-custom .head-sub { font-size: .72rem; color: var(--muted); font-weight: 400; }

/* ⚠ THE KPI TILES WERE REMOVED ON 2026-08-30, ON THE USER'S INSTRUCTION.
   `.kpi-card` / `-label` / `-value` / `-sub` / `-link` / `.kpi-row-7` are all
   gone with them, and nothing outside the dashboard ever used them.

   Why, so it is not rebuilt by reflex: three of the seven tiles were figures the
   "This Period Vs Previous" table already carried as rows (Efficiency, Entries)
   or now does (Working Days, moved there as the first row), and the other four
   were the ACTIVE column of Master Data Usage - literally the same
   `COUNT(*) WHERE status='active'` query, printed twice on one screen.

   ⚠ DO NOT WRITE MARKUP AGAINST THEM. If a headline strip is ever wanted again,
   the thing to check first is whether the figure is already a row somewhere on
   this screen - that is what made these duplicates rather than a summary. */

/* ==========================================================================
   4. Buttons & badges
   IMS applied these inline on every page; same values, named once.
   ========================================================================== */
.btn-soft {                                   /* topbar / secondary primary action */
  background: var(--accent-tint); color: var(--violet-ink); border: none;
  font-weight: 600; border-radius: var(--radius-field);
}
.btn-soft:hover, .btn-soft:focus { background: #ddd6fe; color: var(--violet-ink); }

/* ⚠ A DISABLED `.btn-soft` STILL HAS TO LOOK LIKE A BUTTON.
   Bootstrap's only disabled treatment is `opacity: .65`, and `.btn-soft` is a
   pale tint on a white card with `border: none` - so at 65% it dissolves into
   the header and reads as a caption, not as a control that is temporarily off.
   Reported on the Gauge By Add button, which is the one button in this app that
   is disabled as a STANDING state rather than for the second a save is in
   flight. The user's instruction, 2026-09-09: "disabled button should have some
   border as a part of theme".

   ⚠ THE RING IS AN INSET box-shadow, NOT A `border`. `.btn-soft` is declared
   `border: none`, so a real border would add 2px to the box and make the button
   change SIZE as it enables and disables - which is worse than the fade. An
   inset shadow paints identically and costs no layout.

   ⚠ `opacity: 1` IS LOAD-BEARING: without it Bootstrap's .65 washes these
   colours out too and the whole rule buys nothing. Specificity ties with
   `.btn:disabled` (0,2,0) and app.css loads last, which is what wins it.

   Measured on the shipped tokens: --muted #64748b on #fff is 4.76:1, so the
   label stays legible AA rather than merely present. Scoped to `.btn-soft` on
   purpose - a bare `.btn:disabled` rule would flash an outline on every
   in-flight save guard in the app. */
.btn-soft:disabled, .btn-soft.disabled {
  background: #fff; color: var(--muted); opacity: 1;
  box-shadow: inset 0 0 0 1px var(--accent-soft);
  cursor: not-allowed;
}
.btn-save {                                   /* dialog confirm */
  background: var(--violet-ink); color: #fff; border: none;
  border-radius: 8px; padding: .35rem 1.2rem; font-weight: 600;
}
.btn-save:hover, .btn-save:focus { background: #4c1d95; color: #fff; }
.btn-del {
  background: var(--danger); color: #fff; border: none; border-radius: 8px;
  padding: .375rem .75rem; font-size: .875rem; cursor: pointer;
  display: inline-flex; align-items: center; gap: .3rem;
}
.btn-del:hover { background: var(--danger-ink); color: #fff; }

/* ⚠ EVERY EXPORT / DOWNLOAD CONTROL IN THE APP, AND THE ONLY GREEN BUTTON.
   One look, one icon, one word: `bi-download` + the label `Export`, never a
   format name. "Export Excel" named the library that wrote the file, which is
   the one thing nobody opening it needs to know, and it made Export the widest
   control in a card header on a phone.

   ⚠ THE WASH, NOT A SOLID FILL. Solid --ok (#16a34a) under white text measures
   3.30:1 and fails AA at this size; --ok-bg under --ok-ink measures 4.57:1 and
   passes, and the hover pair 7.52:1. It is also the shape .btn-soft already
   uses - a tint behind its own dark ink - so Export reads as a sibling of Add
   rather than as a foreign control. This green is the app's only one, already
   shipping as .st-active and .act-ok.

   ⚠ INLINE-FLEX + gap, AND THE ICON CARRIES NO me-1. Below 576 the label span
   is display:none, and a gap collapses with it where a margin does not - with
   me-1 the glyph sat 4px left of its own centre on every phone.

   ⚠ THE LABEL IS HIDDEN BELOW 576, SO title + aria-label ARE MANDATORY. The
   icon is then the whole affordance and nothing else announces the button -
   the rule .action-btn carries directly below, which export buttons never got.

   ⚠ NO `display` IS SET ON A NARROWER SELECTOR. masters.js hides and shows its
   button with an inline style.display, and an inline none must keep beating
   this rule; clearing it back to '' is what falls through to inline-flex. */
.btn-export {
  background: var(--ok-bg); color: var(--ok-ink); border: none;
  font-weight: 600; border-radius: var(--radius-field);
  display: inline-flex; align-items: center; gap: .35rem;
}
.btn-export:hover, .btn-export:focus { background: #bbf7d0; color: #14532d; }

/* Icon-only, fixed square. Row actions carry no label: one icon + one tint per
   action, identical on every screen, so the Actions column stays narrow and the
   row height is set by the text, not by the buttons. Every one must still carry
   title + aria-label - the icon is the whole affordance. */
.action-btn {
  width: 28px; height: 28px; padding: 0; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: none; border-radius: 8px;
  font-size: .8rem; cursor: pointer; line-height: 1;
}
.action-btn:disabled { opacity: .45; cursor: not-allowed; }
.act-edit { background: #dbeafe; color: #1d4ed8; }
.act-edit:hover { background: #bfdbfe; }
.act-del  { background: var(--danger-bg); color: var(--danger); }
.act-del:hover  { background: #fee2e2; }
/* Print opens a controlled document, not an edit - grey rather than a colour, so
   the two coloured buttons beside it stay the ones that change something. */
.act-print { background: #f1f5f9; color: #475569; }
.act-print:hover { background: #e2e8f0; }
.act-perm { background: var(--info-bg); color: var(--info-ink); }
.act-perm:hover { background: #bae6fd; }
.act-ok   { background: var(--ok-bg); color: var(--ok-ink); }
.act-warn { background: var(--warn-bg); color: var(--warn-ink); }

.status-badge {
  border-radius: 20px; padding: 2px 10px;
  font-size: .72rem; font-weight: 600; display: inline-block; white-space: nowrap;
}
/* IMS lifecycle set - kept for the stock report and future operational screens */
.st-store        { background: var(--accent-tint); color: var(--violet-ink); }
.st-floor        { background: var(--info-bg);     color: var(--info-ink); }
.st-issued       { background: var(--ok-bg);       color: var(--ok-ink); }
.st-scrapped     { background: #f5f3ff; color: #6d28d9; }
.st-direct-scrap { background: #fef3c7; color: #b45309; }
.st-lost         { background: var(--danger-bg); color: var(--danger-ink); }
.st-direct-lost  { background: #fce7f3; color: #9d174d; }
.st-broken       { background: var(--warn-bg); color: var(--warn-ink); }
/* Account/role set - the ERP admin screens */
.st-active   { background: var(--ok-bg); color: var(--ok-ink); }
.st-inactive { background: #f1f5f9; color: var(--muted); }
.st-role     { background: var(--accent-tint); color: var(--violet-ink); }
.st-info     { background: var(--info-bg); color: var(--info-ink); }
.st-warn     { background: var(--warn-bg); color: var(--warn-ink); }
.st-danger   { background: var(--danger-bg); color: var(--danger-ink); }

.perm-key {
  font-family: Consolas, 'Courier New', monospace; font-size: .7rem;
  background: #f1f5f9; border: 1px solid var(--card-border);
  color: #475569; border-radius: 6px; padding: 1px 6px; white-space: nowrap;
}
.avatar-sm {
  width: 36px; height: 36px; border-radius: 50%; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: .72rem; font-weight: 700; color: #fff;
}
.group-label {
  font-size: .68rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .8px;
  margin-bottom: .4rem; display: flex; align-items: center; gap: .3rem;
}

/* ==========================================================================
   5. Tables - IMS look; cards below 768 so nothing scrolls sideways
   ========================================================================== */
.table-app { font-size: .83rem; margin-bottom: 0; width: 100%; }
.table-app thead th {
  background: #f8fafc; color: var(--muted); font-weight: 600; font-size: .78rem;
  border-bottom: 1px solid var(--card-border); white-space: nowrap;
  padding: .75rem 1rem;
}
/* Sortable header. The icon is always present, so the row never reflows on
   click and a sortable column is discoverable before you hover it. */
.table-app thead th.th-sort { cursor: pointer; user-select: none; }
.table-app thead th.th-sort:hover { background: #eef2f7; color: var(--text); }
/* --accent, not --primary: there has never been a --primary in :root, so both
   of these rules resolved to nothing and a sorted column looked exactly like an
   unsorted one apart from its arrow. Found 2026-08-04 building the Production
   grid, which sorts on every column. */
.table-app thead th.th-sort:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.table-app thead th.th-sort i { opacity: .35; font-size: .7rem; }
.table-app thead th.th-sort:hover i { opacity: .7; }
.table-app thead th.th-sort-on { color: var(--accent); }
.table-app thead th.th-sort-on i { opacity: 1; }

.table-app tbody td { vertical-align: middle; padding: .3rem 1rem; border-bottom: 1px solid var(--line); }
.table-app tbody tr:last-child td { border-bottom: none; }
.table-app tbody tr:hover { background: #fafbff; }
/* The grid is the one place in this app where a horizontal scrollbar is normal
   rather than a fault, so it is made to look deliberate instead of borrowed:
   thin, tinted to the field border, and matching the sidebar's treatment. */
.table-wrap {
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  scrollbar-width: thin; scrollbar-color: var(--field-border) transparent;

  /* ⚠ THE GESTURE STOPS AT THE TABLE - it does not chain on to the page
     (2026-08-25, the user's report: "header is able to scroll slightly
     horizontal where as it must be fix only table should scroll").

     Without this, a sideways swipe that reaches the end of the table carries on
     into the viewport, and on a touch device the whole page pans a little way -
     taking the sticky topbar and the filter bar with it, which is exactly what
     the user photographed. It is a SCROLL-CHAINING effect, not an overflowing
     element: nothing on the page is too wide (measured, docScrollWidth === 375
     at 375), which is why it never showed on a desktop where the wheel does not
     chain.

     ⚠ `-x` ONLY, AND `contain` RATHER THAN `none`. Vertical chaining is what
     lets a swipe that starts over the grid go on to scroll the PAGE, which is
     the ordinary way anybody reads a long report on a phone - taking that away
     would trap the finger inside the table. `contain` stops the chain without
     disabling the grid's own overscroll effects.

     ⚠ AND NOT `overflow-x: hidden/clip` ON `html`, which was the first attempt.
     See the note beside the `html` rule at the top of this file: it stops the
     pan and breaks vertical scrolling on every page in the app. */
  overscroll-behavior-x: contain;
}
.table-wrap::-webkit-scrollbar { height: 8px; }
.table-wrap::-webkit-scrollbar-track { background: transparent; }
.table-wrap::-webkit-scrollbar-thumb { background: var(--field-border); border-radius: 4px; }
.table-wrap::-webkit-scrollbar-thumb:hover { background: var(--faint); }

.row-count { font-size: .78rem; color: var(--muted); }
.empty-row { text-align: center; color: var(--muted); padding: 2rem 1rem !important; font-size: .83rem; }

/* Row actions. A hook on five screens since 2026-07-30 with no rule behind it -
   the buttons only ever sat on one line because the cell happened to be wide
   enough. Below, .table-pin makes that cell the fixed anchor the whole grid
   scrolls under, and a fixed anchor that can reflow is not fixed. */
.cell-actions { white-space: nowrap; }

/* ── Grid modifiers - both opt-in ─────────────────────────────────────────────
   .table-nowrap  every cell on one line at EVERY width, not just below 768.
                  A wrapped cell makes its row two or three lines tall while its
                  neighbours stay one, and the eye loses the row it was reading;
                  the table grows past the card instead and .table-wrap scrolls
                  it. Opt-in, because a column of prose - the Activity Log's
                  Description - would push the table several screens wide and
                  turn its own scrollbar into a thing nobody can aim.

   .table-pin     from 768px up, holds the LAST cell of every row (the Actions
                  cell) against the right edge of .table-wrap while the data
                  scrolls underneath it. Ten columns of real machine and part
                  names do not fit a laptop, and Edit and Delete are what the
                  register is opened to reach - scrolling right to find them,
                  then left to read which row you are on, is the failure this
                  prevents.

                  BELOW 768 the pin is switched off and the whole table moves as
                  one piece, on the user's instruction (2026-08-05). It is the
                  right call: 125px of pinned column out of a 375px screen leaves
                  the data 250px, and a machine name needs more than that on its
                  own. A phone scrolls the table; a desktop scrolls the data.

   Both are set on the <table>, so a screen opts in by class and the engine
   behind it never has to know. */
.table-nowrap thead th,
.table-nowrap tbody td { white-space: nowrap; }
/* A capped cell inside a nowrap grid: prose or a badge list that would
   otherwise push the table several screens wide ends in an ellipsis, the full
   text on its title. Used by the masters grid (Remark, Address, Note, linked
   Customers); identifying columns are never capped. */
.table-nowrap tbody td.cell-clip {
  max-width: 260px; overflow: hidden; text-overflow: ellipsis;
}
/* The same cap as a span, for a grid whose <td>s are built generically: the
   Visual By / Gauge By lists on the Visual & Quantity Record register. The full
   text is on its title and, on a touch screen, in a tooltip on tap (vqr.js). */
.vq-clip {
  display: inline-block; max-width: 220px; vertical-align: bottom;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.vq-clip:focus-visible { outline: 2px solid var(--accent-soft); outline-offset: 1px; }

@media (min-width: 768px) {
  /* .cell-actions, never :last-child. The empty state is one td with a colspan
     and it IS the last child - pinning it right would park "No production
     entries match these filters" against the edge of a table it is supposed to
     be centred across. */
  .table-pin .cell-actions {
    position: sticky; right: 0; z-index: 2;
    background: var(--card-bg);
  }
  .table-pin thead .cell-actions { background: #f8fafc; }
  /* The row highlight has to reach the pinned cell too: the cell paints its own
     opaque background over tr:hover, and a row that lights up everywhere except
     the column your cursor is heading for looks broken. */
  .table-pin tbody tr:hover .cell-actions { background: #fafbff; }

  /* Hairline first, so the column reads as a column even on a table that fits
     and never scrolls. */
  .table-pin .cell-actions::before {
    content: ''; position: absolute; top: 0; bottom: 0; left: 0;
    width: 1px; background: var(--card-border);
  }
  /* The shadow is the only part JS owns: CSS cannot know whether anything is
     currently hidden underneath. .is-pinned is set by syncPin() in app.js while
     there is more table to the left of the pin, and removed at the far right, so
     the depth appears exactly when there is depth. */
  .table-wrap.is-pinned .table-pin .cell-actions {
    box-shadow: -9px 0 12px -8px rgba(30, 41, 59, .22);
  }
}

/* Every table stays a TABLE at every width and scrolls sideways inside
   .table-wrap. The phone card layout (each row a stacked label/value list) was
   removed on 2026-08-02 at the user's instruction: a record is far easier to
   read as one line you scan across than as a tall list you scroll down, and the
   two layouts meant every screen had to be checked twice.

   Cells must NOT wrap - a wrapped cell makes the row two or three lines tall and
   the grid stops reading as a grid.

   The PAGE still never scrolls sideways (R9): the overflow belongs to
   .table-wrap, which is the only element allowed to scroll horizontally. */
@media (max-width: 767px) {
  .table-app { font-size: .78rem; }
  .table-app tbody td,
  .table-app thead th { white-space: nowrap; }
  .table-app tbody td { padding: .45rem .6rem; }
  .table-app thead th { padding: .55rem .6rem; }
}

/* ==========================================================================
   5a. Full-height grid - .grid-fill
   ==========================================================================
   The card is exactly as tall as the space left under it, the rows scroll inside
   .table-wrap, and the pager sits on the bottom edge of the screen where it can
   be reached without scrolling to the end of the page first. gridFill() in
   app.js sets the height; everything else is here.

   ⚠ OPT-IN, ON THE CARD. A screen carrying TWO grids (Rework) must not give each
   of them a viewport, and the short lists that have no pager at all (Backup,
   Notification Settings) have nothing to gain.

   ⚠ min-height: 0 IS LOAD-BEARING, TWICE. A flex item's default min-height is
   `auto`, which means "never smaller than my content" - so without it the wrap
   sizes to the whole table, the card overflows the height JS just gave it, and
   nothing scrolls at all. It is needed on the card as well as on the wrap,
   because the card is itself a flex item on screens that nest one.

   ⚠ AND THE HEADER HAS TO STICK. The rows scroll vertically now, and a grid whose
   column headings scroll off the top is a grid you cannot read: the one before
   it never needed this because the page scrolled and the header went with it. */
.grid-fill {
  display: flex; flex-direction: column; min-height: 0;
  overflow: hidden;                     /* the card's own radius clips the wrap */
}
.grid-fill > .table-wrap { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.grid-fill > .pager      { flex: 0 0 auto; }

/* A vertical swipe that reaches the last row must not carry on and scroll the
   page - the page has nowhere to go, and on a touch device the chain is what
   triggers pull-to-refresh over a grid somebody is only trying to read. The
   horizontal half is already contained on every .table-wrap; this is its twin,
   and it is scoped to .grid-fill because an ordinary grid SHOULD chain (see the
   note on overscroll-behavior-x above - taking that away traps the finger). */
.grid-fill > .table-wrap { overscroll-behavior-y: contain; }

.grid-fill .table-app thead th { position: sticky; top: 0; z-index: 3; }
/* .table-app thead th already paints #f8fafc; a sticky cell with no background
   would let the rows show through as they pass underneath it. Stated here as
   well so the two can never be separated by an edit to one of them. */
.grid-fill .table-app thead th { background: #f8fafc; }

@media (min-width: 768px) {
  /* The Actions cell is sticky on BOTH axes in a filled grid - right from
     .table-pin, top from the rule above - so it has to outrank each of them
     individually or the corner it occupies flickers between the two. */
  .grid-fill .table-pin thead .cell-actions { z-index: 4; }
}

/* ── The pinned totals row - tfoot.grid-total (2026-09-27) ────────────────────
   Batch Movement and Dispatch. In a .grid-fill card the rows scroll inside
   .table-wrap, so a sticky-bottom tfoot stays on screen while they pass under
   it - the user's "static total row". The figures are the server's, summed over
   every filtered row, never the page.

   ⚠ THE HORIZONTAL PADDING MUST BE THE BODY'S at every width, or right-aligned
   totals sit 8px off the figures they total (the #rpFoot lesson, §15e). */
.table-app tfoot.grid-total td {
  background: #f8fafc; border-top: 2px solid var(--card-border); border-bottom: 0;
  font-weight: 700; color: var(--ink-900); white-space: nowrap;
  padding: .5rem 1rem;
}
.table-app tfoot.grid-total .grid-total-lbl { font-weight: 600; color: var(--muted); }
.grid-fill .table-app tfoot.grid-total td {
  position: sticky; bottom: 0; z-index: 3; border-top: 0;
  box-shadow: inset 0 2px 0 var(--card-border);   /* a sticky cell's border scrolls away; the shadow does not */
}
@media (max-width: 767px) {
  .table-app tfoot.grid-total td { padding: .5rem .6rem; }
}
@media (min-width: 768px) {
  .table-pin tfoot.grid-total .cell-actions { background: #f8fafc; }
  .grid-fill .table-pin tfoot.grid-total .cell-actions { z-index: 4; }
}

/* ==========================================================================
   6. Filter bar & pagination
   ========================================================================== */
.filter-bar { padding: 1rem; display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
.filter-bar .form-control, .filter-bar .form-select { font-size: .82rem; }

/* Opt-in modifier: below 576 every control in the bar takes a full row.
   Two 170px selects DO fit side by side on a 375px screen, but only by leaving
   each too narrow to read "Notification Settings" in - and a filter whose
   options you cannot read is not a filter. The date trigger stretches with them
   so the bar reads as a stack rather than one wide control and two ragged ones.

   A modifier rather than a rule on .filter-bar itself: the masters and users
   bars mix fields with buttons, and a full-width button is not an improvement.
   Applied on the Activity Log, whose bar is three fields and nothing else. */
@media (max-width: 575px) {
  .filter-stack > * { flex: 1 1 100%; max-width: 100% !important; }
  .filter-stack .dr-wrap { display: block; }
  .filter-stack .dr-trigger { width: 100%; justify-content: space-between; }

  /* Not part of .filter-stack: any bar holding a date trigger wants this,
     including the ones that mix fields with buttons and so must not stack
     wholesale. The rule above stays for .filter-stack's own use; this one
     applies to every bar and is harmless where both match. */
  .filter-bar .dr-wrap { flex: 1 1 100%; }
  .filter-bar .dr-trigger { width: 100%; justify-content: space-between; }
}

/* ── The bar's own controls, and where they go on a phone ─────────────────────
   Below 768 every filter except Search moves into the screen's filter drawer and
   the bar is left with a search box and one icon. Eight controls one per row is
   eight rows of chrome above the first figure on a 375px screen. The move itself
   is initFilterCollapse() in app.js section 7b - ⚠ THE SAME NODES, never a second
   mobile copy; these rules only say how they should look in each home.

   ⚠ WIDTHS COME FROM A CLASS, NOT FROM A style="" ATTRIBUTE. An inline max-width
   is the one thing a media query cannot answer back to, so a control carrying
   one would arrive in the drawer still 250px wide inside a 100%-wide panel. */
.filter-bar .fb-field  { flex: 1 1 170px; min-width: 170px; max-width: 250px; }
.filter-bar .fb-search { flex: 1 1 180px; min-width: 180px; max-width: 280px; }

@media (max-width: 767.98px) {
  /* Search has the bar to itself now, so it takes it. */
  .filter-bar .fb-search { max-width: none; }
}

/* display: contents, so the slot itself lays out nothing: the controls it
   receives become children of .filter-panel-body's own flex column and take its
   gap, which is what makes a moved control indistinguishable from one the drawer
   was written with. An ordinary div here would add a gap of its own above the
   first field, and would still add it on a desktop where the slot is empty. */
.fb-slot { display: contents; }

/* In the drawer every control is a full-width row - the bar's flex bases and
   ceilings mean nothing in a 100%-wide panel. Scoped to the drawer body rather
   than written as an override, so nothing has to out-specify anything. */
.filter-panel-body .fb-field,
.filter-panel-body .fb-search  { max-width: none; min-width: 0; width: 100%; }
.filter-panel-body .dr-wrap    { display: block; }
.filter-panel-body .dr-trigger { width: 100%; justify-content: space-between; }

/* The drawer's closing note names the filters that stayed on the bar behind it.
   Below 768 they are IN this drawer, so the sentence is untrue and the only
   honest thing to do with it is not print it. */
@media (max-width: 767.98px) { .filter-panel-note { display: none; } }

/* ── Filter side panel ────────────────────────────────────────────────────────
   A screen with eight filters cannot put all eight in the bar: the bar wraps to
   three rows and the four that matter stop being findable. The ones used on
   every visit stay on screen; the rest live here, one click away, behind a
   trigger that CARRIES A COUNT - a filter you cannot see is a filter that makes
   the grid look empty for no reason.

   Bootstrap's offcanvas, which is already in the vendored bundle, plus the
   .modal-hd / .modal-footer this app already uses - both of which are
   standalone and do not require a .modal-content around them. Building a
   drawer from scratch would have been a third dialog idiom to maintain.

   Nothing inside commits until Apply, same as the date range picker. */
.filter-panel { width: 340px; }
.filter-panel-body {
  display: flex; flex-direction: column; gap: 1rem;
  padding: 1.15rem; background: #fff;
}
/* .fl-field carries a bottom margin for form rows; in a flex column with its own
   gap that reads as uneven spacing. */
.filter-panel-body .fl-field { margin-bottom: 0; }
.filter-panel-note {
  margin: 0; font-size: .74rem; color: var(--faint); line-height: 1.5;
  padding-top: .35rem; border-top: 1px dashed var(--line);
}
/* Reset sits hard left, Apply hard right: they are opposite intentions and
   should not read as a pair of adjacent buttons you might hit either of. */
.filter-panel-ft { flex-shrink: 0; }

/* [hidden] MUST come with the display rule. The browser hides [hidden] with
   `display:none` from its own stylesheet, and ANY author `display` beats it -
   so without this line the badge would sit there reading "0" forever. */
.f-count[hidden] { display: none; }
.f-count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 17px; height: 17px; margin-left: .4rem; padding: 0 .3rem;
  border-radius: 9px; background: var(--accent); color: #fff;
  font-size: .66rem; font-weight: 700; line-height: 1;
}
.btn-soft .f-count { background: var(--violet-ink); }

@media (max-width: 575px) {
  /* Full width on a phone: 340px of a 375px screen leaves a 35px strip of page
     behind it, which looks like a rendering fault rather than a backdrop. */
  .filter-panel { width: 100%; }
}

.pager {
  display: flex; align-items: center; justify-content: space-between;
  gap: .6rem; flex-wrap: wrap; padding: .75rem 1rem; border-top: 1px solid var(--line);
}
.pager-btns { display: flex; gap: .25rem; flex-wrap: wrap; }
.page-btn {
  border: 1px solid var(--card-border); background: #fff; border-radius: 7px;
  padding: 3px 10px; font-size: .78rem; cursor: pointer; color: #475569;
}
.page-btn:hover:not(:disabled) { background: var(--accent-wash); border-color: var(--accent-soft); color: var(--accent); }
.page-btn.active { background: var(--accent); border-color: var(--accent); color: #fff; }
.page-btn:disabled { opacity: .4; cursor: default; }
.page-size { display: inline-flex; align-items: center; gap: .4rem; font-size: .78rem; color: var(--muted); }
.page-size select { width: auto; padding: .15rem 1.6rem .15rem .5rem; font-size: .78rem; }

/* ==========================================================================
   7. (was: Tabs - master data)
   Removed 2026-08-02 with the tab bar. `.tab-bar`, `.tab-btn` and `.tab-panel`
   had no remaining markup anywhere. The master selector became a topbar dropdown
   then, and on 2026-08-31 it became the left rail in section 15f - so the tab
   bar this section is a tombstone for has now been replaced twice. See
   docs/DESIGN_SYSTEM.md.
   ========================================================================== */

/* ==========================================================================
   8. Forms - stock Bootstrap, lightly tuned
   ========================================================================== */
.form-label.small { font-size: .8rem; margin-bottom: .25rem; color: #334155; }
.form-control:focus, .form-select:focus {
  border-color: var(--accent-soft);
  box-shadow: 0 0 0 .2rem rgba(79,70,229,.12);
}
.is-invalid, .form-control.is-invalid, .form-select.is-invalid {
  border-color: var(--danger) !important;
  box-shadow: 0 0 0 .2rem rgba(220,38,38,.12) !important;
}

/* ── Floating-label field - THE standard text input ───────────────────────────
   Empty + unfocused: the label sits inside the box like a placeholder.
   Focused:           the border turns accent and the label rises onto it.
   Filled:            the label stays up, muted.
   The label carries a white background and side padding so it notches the
   border cleanly rather than overlapping it.

   Markup:
     <div class="fl-field">
       <input id="x" placeholder=" ">                 <- placeholder=" " is required
       <label for="x">Tag Name <span class="req">*</span></label>
     </div>
   Textareas add .fl-textarea so the label sits at the top, not centred.        */
.fl-field {
  position: relative; background: #fff;
  border: 1.5px solid var(--field-border); border-radius: var(--radius-field);
  transition: border-color .15s;
}
.fl-field input, .fl-field textarea, .fl-field select {
  display: block; width: 100%; border: none; outline: none; background: transparent;
  padding: 6px 10px; font-size: .9rem; color: var(--body); line-height: 1.4;
  border-radius: var(--radius-field);
}
/* Chrome/Edge paint a filled field with their own pale-blue wash and their own
   ink, and neither is overridable by background/color - the shipped screenshot
   is what that looks like against an all-white form. An inset shadow the width
   of the box is the one thing that covers it, and -webkit-text-fill-color is
   what the ink obeys. Only reachable when somebody CHOOSES to fill from the
   browser's list; the login form no longer pre-fills (see app/views/login.php). */
.fl-field input:-webkit-autofill,
.fl-field input:-webkit-autofill:hover,
.fl-field input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 1000px #fff inset;
  -webkit-text-fill-color: var(--body);
  caret-color: var(--body);
  border-radius: var(--radius-field);
}
.fl-field textarea { resize: none; padding-top: 6px; }
.fl-field select { appearance: none; cursor: pointer; padding-right: 34px; }
.fl-field.fl-select::after {
  content: ''; position: absolute; right: 14px; top: 50%; pointer-events: none;
  width: 7px; height: 7px; margin-top: -5px;
  border-right: 1.5px solid var(--faint); border-bottom: 1.5px solid var(--faint);
  transform: rotate(45deg);
}
.fl-field label {
  position: absolute; left: 12px; top: 47%; transform: translateY(-50%);
  font-size: .9rem; color: var(--faint); pointer-events: none;
  transition: all .15s ease; background: #fff; padding: 0 4px; line-height: 1.2;
  white-space: nowrap; max-width: calc(100% - 24px); overflow: hidden; text-overflow: ellipsis;
}
.fl-field label .req { color: var(--danger); }
.fl-field.fl-textarea label { top: 17px; transform: none; }

.fl-field:focus-within { border-color: var(--accent); }
.fl-field:focus-within label,
.fl-field.has-value:not(:focus-within) label,
.fl-field.fl-select label {                    /* a select always shows a value */
  top: 0; transform: translateY(-50%);
  font-size: .72rem; font-weight: 600;
}
.fl-field:focus-within label { color: var(--accent); }
.fl-field.has-value:not(:focus-within) label,
.fl-field.fl-select:not(:focus-within) label { color: var(--muted); }
/* The searchable select's list is open. On a touch screen the select is never
   focused (iOS would put its own picker up - app.js §10), so this stands in for
   :focus-within. Specificity (0,3,1) and later in the file, to beat the rule above. */
.fl-field.cs-open { border-color: var(--accent); }
.fl-field.fl-select.cs-open label { color: var(--accent); }

/* .fl-date - a native date input paints "dd-mm-yyyy" whether it holds a value
   or not, so the label can never sit down over it without landing on top of the
   format hint. Exactly the problem a select has, and the same fix. */
.fl-field.fl-date label {
  top: 0; transform: translateY(-50%);
  font-size: .72rem; font-weight: 600;
}
.fl-field.fl-date:not(:focus-within) label { color: var(--muted); }
.fl-field.fl-date input { cursor: pointer; }
/* iOS Safari draws a date (and time) input as its own button: the value
   CENTRED, and the box collapsing to nothing while it is empty - so on an iPhone
   a blank Order Date was a sliver and a filled one did not line up with the
   fields around it. Left-aligned and held to the height of a text field.
   @supports (-webkit-touch-callout: none) is true on iOS alone, so no desktop
   browser's date field is touched. */
@supports (-webkit-touch-callout: none) {
  .fl-field input[type="date"], .fl-field input[type="time"] {
    -webkit-appearance: none; min-height: calc(1.4em + 12px);
  }
  .fl-field input[type="date"]::-webkit-date-and-time-value,
  .fl-field input[type="time"]::-webkit-date-and-time-value { text-align: left; }
}

/* .fl-ro - filled in by something other than this field. Read-only and NOT
   disabled: a disabled input is skipped by tab and by the screen reader, and
   this is a value the operator is meant to read. The label carries its own white
   background to notch the border, so it has to be re-tinted or it sits in a
   white slot cut out of a grey field. */
.fl-field.fl-ro { background: #f8fafc; border-color: var(--card-border); }
.fl-field.fl-ro label { background: #f8fafc; }
.fl-field.fl-ro input { color: var(--muted); cursor: not-allowed; }

/* .fl-suffix - the unit, inside the field. Set beside the input instead, it
   wraps under it at 375px and reads as a caption for the NEXT field. */
.fl-field .fl-suffix {
  position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
  font-size: .72rem; color: var(--faint);
  pointer-events: none; white-space: nowrap;
}
.fl-field:has(.fl-suffix) input { padding-right: 68px; }
/* The number spinner would sit underneath the unit. Nothing on this form is
   stepped one at a time anyway - a cycle time is read off a gauge and typed. */
.fl-field:has(.fl-suffix) input[type=number] { -moz-appearance: textfield; appearance: textfield; }
.fl-field:has(.fl-suffix) input[type=number]::-webkit-outer-spin-button,
.fl-field:has(.fl-suffix) input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* Padding for a card that is a form rather than a grid. .card-header-custom
   sets the header's; this sets the body's, and matches it. */
.form-pad { padding: 1.25rem; }
@media (max-width: 575px) { .form-pad { padding: .9rem; } }

.fl-field.fl-invalid { border-color: var(--danger) !important; border-width: 2px !important; }
.fl-field.fl-invalid label { color: var(--danger) !important; }
.fl-hint { font-size: .7rem; color: var(--faint); margin-top: .25rem; }

/* A boolean field. Boxed to the same height as a text field so a row mixing the
   two does not look ragged. */
.sw-field {
  display: flex; align-items: center; min-height: 44px;
  padding: .35rem .75rem;
  border: 1px solid var(--field-border); border-radius: var(--radius-field);
  background: #fff;
}
.sw-field .form-check { padding-left: 2.4em; }
.sw-field .form-check-input { margin-left: -2.4em; cursor: pointer; }
.sw-field .form-check-input:checked { background-color: var(--accent); border-color: var(--accent); }
.sw-field .form-check-label { font-size: .85rem; color: var(--body); cursor: pointer; user-select: none; }

/* Multi-select: a filterable checkbox list. A native <select multiple> needs
   ctrl-click to deselect and is effectively unusable on a touch screen. */
.ms-box {
  border: 1px solid var(--field-border); border-radius: var(--radius-field);
  background: #fff; overflow: hidden;
}
.ms-hd {
  display: flex; align-items: center; justify-content: space-between; gap: .5rem;
  padding: .4rem .65rem; background: #f8fafc; border-bottom: 1px solid var(--line);
}
.ms-label { font-size: .78rem; font-weight: 600; color: var(--muted); }
.ms-count { font-size: .7rem; color: var(--accent); font-weight: 600; white-space: nowrap; }
.ms-search {
  width: 100%; border: 0; border-bottom: 1px solid var(--line);
  padding: .45rem .65rem; font-size: .82rem; outline: none;
}
.ms-search:focus { border-bottom-color: var(--accent); }
.ms-list { max-height: 168px; overflow-y: auto; -webkit-overflow-scrolling: touch; }
.ms-item {
  display: flex; align-items: center; gap: .55rem;
  padding: .4rem .65rem; font-size: .83rem; cursor: pointer; margin: 0;
  border-bottom: 1px solid var(--line);
}
.ms-item:last-child { border-bottom: none; }
.ms-item:hover { background: var(--accent-wash); }
.ms-item input { width: 15px; height: 15px; cursor: pointer; flex-shrink: 0; accent-color: var(--accent); }
.ms-empty { padding: .8rem .65rem; font-size: .78rem; color: var(--faint); text-align: center; }
.ms-box.fl-invalid { border-color: var(--danger); }

/* ── Multi-select extensions, added for the bulk breakdown screen ────────────
   A select-all and a per-row tag. Kept here with the component rather than in
   the bulk block below, because nothing about them is specific to that screen -
   the next multi-select that needs "tick everything" gets them free.

   The foot, not the header: select-all acts on the LIST, and a control sitting
   above the search box reads as if it acted on the search. */
.ms-item-lbl { flex: 1 1 auto; min-width: 0; }
.ms-foot {
  display: flex; align-items: center; justify-content: space-between; gap: .5rem;
  padding: .4rem .65rem; background: #f8fafc; border-top: 1px solid var(--line);
}
.ms-all {
  display: flex; align-items: center; gap: .45rem; margin: 0;
  font-size: .78rem; font-weight: 600; color: var(--muted); cursor: pointer; user-select: none;
}
.ms-all input { width: 15px; height: 15px; cursor: pointer; accent-color: var(--accent); }
.ms-foot-note { font-size: .7rem; color: #b45309; font-weight: 600; text-align: right; }

/* A row that is selectable but will be skipped. Tinted rather than disabled:
   the machine is a legitimate choice - somebody may want it in the batch to see
   it named in the dialog - and greying it out would say it cannot be picked. */
.ms-item-booked { background: #fffbeb; }
.ms-item-booked:hover { background: #fef3c7; }
.ms-tag {
  flex-shrink: 0; font-size: .64rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .3px; padding: .1rem .35rem; border-radius: 4px;
  background: #fde68a; color: #78350f;
}

/* ══ Bulk breakdown entry ════════════════════════════════════════════════════
   The twelve-hour picker and its summary dialog. Built from the same borders,
   tints and type scale as .ms-box and .hour-row so the screen reads as part of
   the production module rather than as a visitor to it. */

/* Said before anything is typed. Informational, not a warning - creating an
   incomplete entry is the correct use of this screen, not a risk being run. */
.bk-note {
  display: flex; align-items: flex-start; gap: .5rem;
  padding: .6rem .75rem; margin-bottom: 1rem;
  background: var(--accent-wash); border: 1px solid var(--card-border);
  border-radius: var(--radius-field);
  font-size: .78rem; line-height: 1.45; color: var(--body);
}
.bk-note .bi { color: var(--accent); flex-shrink: 0; margin-top: .1rem; }

/* ⚠ THE ROW'S FOUR TRACKS, DECLARED ONCE. The heading row, every hour row and
   both breakpoints below read this one custom property, so the header can never
   sit out of line with the columns it names. Label · reason · minutes · approver
   - the same four facts in the same order as .hour-rej on step 2. */
.bk-hours {
  --bk-cols: minmax(140px, 1fr) minmax(130px, 1.3fr) 58px minmax(120px, 1.1fr);
  border: 1px solid var(--field-border); border-radius: var(--radius-field);
  background: #fff; overflow: hidden;
}
.bk-hd, .bk-foot {
  display: flex; align-items: center; justify-content: space-between; gap: .5rem;
  padding: .4rem .65rem; background: #f8fafc;
}
/* ⚠ THE SAME FOUR TRACKS AS .bk-row, REPEATED RATHER THAN GUESSED AT - the same
   arrangement .hour-head keeps over .hour-row on step 2. A heading row laid out
   any other way drifts out of line with its own columns the first time one of
   them is resized. */
.bk-hd {
  display: grid; grid-template-columns: var(--bk-cols);
  border-bottom: 1px solid var(--line);
}
/* Wraps: it now names every reason in play with its minutes, which on a mixed
   batch is longer than one line at any width this panel is used at. */
.bk-foot {
  border-top: 1px solid var(--line); font-size: .74rem; color: var(--muted);
  flex-wrap: wrap; line-height: 1.45;
}
.bk-hd-col {
  font-size: .7rem; font-weight: 600; color: var(--muted);
  text-transform: uppercase; letter-spacing: .3px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bk-hd-min { text-align: center; }
.bk-all {
  display: flex; align-items: center; gap: .45rem; margin: 0;
  font-size: .78rem; font-weight: 600; color: var(--muted); cursor: pointer; user-select: none;
}
.bk-all input { width: 15px; height: 15px; cursor: pointer; accent-color: var(--accent); }

/* ⚠ NO max-height AND NO INNER SCROLL, unlike .ms-list above. All twelve hours
   are visible at once on purpose: the question this half of the screen asks is
   "which hours", and an answer that needs scrolling to read back is one people
   get wrong. Twelve rows is ~400px, which is affordable beside a machine list
   that is shorter, and fine stacked on a phone. */
.bk-row {
  display: grid; grid-template-columns: var(--bk-cols);
  align-items: center; gap: .5rem;
  padding: .32rem .65rem; border-bottom: 1px solid var(--line);
}
.bk-row:last-child { border-bottom: none; }
.bk-row.is-on { background: var(--accent-wash); }
.bk-chk {
  min-width: 0; margin: 0;
  display: flex; align-items: center; gap: .55rem;
  cursor: pointer; user-select: none;
}
.bk-chk input { width: 15px; height: 15px; cursor: pointer; flex-shrink: 0; accent-color: var(--accent); }
.bk-lbl { font-size: .83rem; color: var(--body); white-space: nowrap; }
.bk-min {
  width: 100%; min-width: 0; text-align: center;
  border: 1px solid var(--field-border); border-radius: 6px;
  padding: .2rem .3rem; font-size: .82rem; color: var(--body); background: #fff;
}
.bk-min:focus { outline: none; border-color: var(--accent); }
.bk-min:disabled { background: #f8fafc; color: var(--faint); }

/* ⚠ THE ROW SELECTS ARE .hour-sel - step 2's control, not a second one. The two
   screens record the same three facts about an hour, and a breakdown reason that
   looks like one thing on the bulk screen and another on the entry form is two
   components to keep in step forever. .bk-sel only adds what the denser row
   needs: a smaller line so twelve rows still fit beside a machine list. */
.bk-sel { font-size: .78rem; padding: 3px 6px; }
.bk-sel:disabled { background: #f8fafc; color: var(--faint); cursor: not-allowed; }
.bk-sum-warn { color: #b45309; font-weight: 600; }

/* The summary dialog. Rendered into confirmDialog's body, so it inherits that
   modal's width - hence the wrapping chip list rather than a table. */
.bk-sum-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: .75rem;
  padding: .2rem 0; font-size: .78rem; color: var(--muted);
}
.bk-sum-row b { color: var(--body); text-align: right; }
/* The minutes beside a reason on a mixed batch - the figure qualifies the name,
   so it must not read as loud as it. */
.bk-sum-min { font-weight: 500; color: var(--muted); }
.bk-sum-list { display: flex; flex-wrap: wrap; gap: .3rem; margin: .55rem 0 0; }
.bk-sum-chip {
  font-size: .72rem; padding: .15rem .4rem; border-radius: 5px;
  background: var(--accent-wash); color: var(--accent); font-weight: 600;
}
.bk-sum-skip {
  margin-top: .5rem; font-size: .72rem; line-height: 1.4;
  color: #78350f; background: #fffbeb; border: 1px solid #fde68a;
  border-radius: 6px; padding: .35rem .5rem;
}
.bk-sum-total {
  margin-top: .65rem; padding-top: .55rem; border-top: 1px solid var(--line);
  font-size: .82rem; color: var(--body);
}

/* ⚠ ON A SMALL SCREEN THE ROW SCROLLS SIDEWAYS - IT DOES NOT STACK (the user's
   instruction, 2026-08-24). The first cut broke each row into three lines below
   576px, which is what .hour-rej does on step 2. That is right for a form being
   filled in one hour at a time and wrong here: this screen's whole job is to
   answer twelve identical rows at once, and thirty-six stacked lines is a
   panel nobody can read down. Kept as one table, it scrolls - which is the same
   call the masters grid made with .table-keep.

   ⚠ THE SCROLLER WRAPS THE HEADING ROW AND THE LIST, NEVER THE WHOLE PANEL.
   The heading has to travel with the columns it names, and the foot - which is
   prose, and wraps - has to stay where it is. R9 is untouched: the panel
   scrolls, the page body never does. */
.bk-scroll { overflow-x: auto; }

/* ⚠ FIXED TRACKS AND min-width: max-content BELOW 768. Fractional tracks would
   just squeeze to fit and there would be nothing to scroll; without the
   min-content floor the grid overflows its own row, so the tint and the hair
   line stop at the viewport edge and only the controls carry on. */
@media (max-width: 767.98px) {
  .bk-hours { --bk-cols: 132px 148px 52px 148px; }
  .bk-hd, .bk-row { min-width: max-content; }
  .bk-sel { font-size: .76rem; }
}

/* A taller touch target rather than a denser one - this is a screen used with
   gloves on. */
@media (max-width: 575.98px) {
  .bk-row { padding: .45rem .65rem; }
  .bk-min { padding: .3rem .3rem; }
}

/* Manage-an-option-list dialog: add on one line, then the reorderable list. */
.opt-add { display: flex; align-items: stretch; gap: .5rem; }
.opt-add .btn { flex-shrink: 0; white-space: nowrap; }
.opt-hint {
  font-size: .72rem; color: var(--faint);
  margin: .6rem 0 .35rem; display: flex; align-items: center; gap: .25rem;
}
.opt-list {
  border: 1px solid var(--field-border); border-radius: var(--radius-field);
  background: #fff; overflow: hidden;
  max-height: 300px; overflow-y: auto; -webkit-overflow-scrolling: touch;
}
.opt-row {
  display: flex; align-items: center; gap: .5rem;
  padding: .35rem .5rem .35rem .25rem;
  border-bottom: 1px solid var(--line); background: #fff;
}
.opt-row:last-child { border-bottom: none; }
.opt-val { flex: 1 1 auto; font-size: .85rem; color: var(--body); word-break: break-word; }
.opt-grip {
  flex-shrink: 0; display: flex; align-items: center; justify-content: center;
  width: 30px; min-height: 30px; color: var(--faint); cursor: grab;
  /* Required, not cosmetic: without it a touch drag scrolls the dialog instead
     of moving the row, and pointermove stops firing once the browser takes over. */
  touch-action: none; user-select: none;
}
.opt-grip:active { cursor: grabbing; }
.opt-row:hover .opt-grip { color: var(--muted); }
.opt-row.opt-dragging {
  background: var(--accent-wash);
  box-shadow: 0 2px 8px rgb(0 0 0 / .10); border-radius: 6px;
  position: relative; z-index: 2;
}
.opt-empty { padding: .9rem .65rem; font-size: .78rem; color: var(--faint); text-align: center; }

/* ── reflist: a value that also names a row elsewhere ─────────────────────────
   Live on Breakdown Reasons, where each reason carries the supervisor who
   normally approves it (2026-08-19). The row select reuses .hour-sel - the app
   already has a compact select and a second one that looked almost the same
   would be the drift this stylesheet exists to avoid.

   ⚠ [hidden] NEEDS ITS OWN display:none. .fl-field sets display:flex, and an
   author display beats the UA rule for [hidden] - the same trap already noted
   at .f-count[hidden]. Without it the add row's reference
   picker shows on every plain list too. */
/* 160px, measured rather than chosen: the dialog is 500px at every width from
   576 up (Bootstrap caps .modal-dialog), which leaves 468px for the name field,
   this select and the Save button. At the 210px this started as, the SECONDARY
   control came out wider than the name it qualifies - 189 against 173 - which
   reads as though the approver were the point of the row. At 160 the name takes
   ~202 and leads, and the select still shows ~18 characters before .hour-sel's
   ellipsis, with the full name visible the moment it opens. */
.opt-ref { flex: 0 1 160px; min-width: 0; }
.opt-ref[hidden] { display: none; }
/* display:flex, not the default inline: as an inline box the select sits on a
   text baseline and the row carries the leading underneath it - 20px of it,
   measured, which is most of a row's height again once the select wraps below
   576. */
.opt-row-ref { flex: 0 0 175px; min-width: 0; display: flex; }
.opt-row-ref .hour-sel { padding: 3px 6px; }

/* The 44px touch target the pointer drag needs on a phone, where there is no
   hover to reveal the grip in the first place. */
@media (max-width: 575.98px) {
  .opt-add { flex-wrap: wrap; }
  .opt-add .btn { width: 100%; }
  .opt-grip { width: 38px; min-height: 38px; color: var(--muted); }
  .opt-row { padding: .45rem .5rem .45rem .25rem; }

  /* The reference takes its own line rather than squeezing a 175px select in
     beside a wrapping name. `order` puts it BELOW the delete button while the
     grip, the name and the delete stay on the first line; the indent lines it
     up with the name rather than the grip. */
  .opt-ref { flex: 1 1 100%; }
  .opt-row { flex-wrap: wrap; }
  /* 46px = the 38px grip plus the row's .5rem gap, so the select starts at the
     same x as the name it belongs to rather than 8px to its left. No margin-top:
     `gap` is a ROW gap as well as a column one once the row wraps, so it has
     already put 8px there and a margin only doubles it. */
  .opt-row-ref { flex: 1 1 100%; order: 3; margin-left: 46px; }
}

/* ── Bulk upload dialog ──────────────────────────────────────────────────────
   Three states in one modal - pick a file, review the plan, or read why the
   file was refused. Only one is ever visible. */

/* A <label> wrapping a hidden file input: the whole box is the click target and
   the drop zone at once, with no JS needed to open the picker. */
.imp-drop {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .3rem; text-align: center; cursor: pointer;
  padding: 1.6rem 1rem;
  border: 2px dashed var(--field-border); border-radius: var(--radius-field);
  background: var(--page-bg); color: var(--muted);
  transition: border-color .15s, background .15s;
}
.imp-drop:hover, .imp-drop-on { border-color: var(--accent); background: var(--accent-wash); }
.imp-drop > i { font-size: 1.6rem; color: var(--accent); }
.imp-drop-main { font-size: .88rem; font-weight: 600; color: var(--ink-900); }
.imp-drop-sub  { font-size: .74rem; color: var(--faint); }

.imp-tip { font-size: .74rem; color: var(--faint); margin-top: .7rem; line-height: 1.5; }
.btn-link-inline {
  background: none; border: 0; padding: 0;
  color: var(--accent); font: inherit; text-decoration: underline; cursor: pointer;
}

/* The chosen file and its remove button on one line. Removing the file is an
   action on the file - it belongs here, not competing with Cancel and Import in
   the footer. */
.imp-file {
  display: flex; align-items: center; gap: .45rem;
  padding: .45rem .4rem .45rem .55rem; margin-bottom: .7rem;
  border: 1px solid var(--line); border-radius: var(--radius-field); background: var(--page-bg);
}
.imp-file > i:first-child { color: var(--accent); flex-shrink: 0; }
.imp-file-name {
  flex: 1 1 auto; min-width: 0;
  font-size: .82rem; font-weight: 600; color: var(--ink-900); word-break: break-all;
}
.imp-file-x {
  flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; padding: 0;
  border: 0; border-radius: 6px; background: transparent;
  color: var(--muted); font-size: .8rem; cursor: pointer;
  transition: background .15s, color .15s;
}
.imp-file-x:hover, .imp-file-x:focus-visible { background: var(--danger-bg); color: var(--danger); }
/* A real touch target where there is no hover to hint at it, same as .opt-grip. */
@media (max-width: 575.98px) {
  .imp-file-x { width: 38px; height: 38px; font-size: .9rem; }
}

/* Three counts, side by side at every width - they are short, and stacking them
   would push the record list below the fold on a phone. */
.imp-stats { display: flex; gap: .5rem; }
.imp-stat {
  flex: 1 1 0; min-width: 0; text-align: center;
  padding: .55rem .3rem; border-radius: var(--radius-field);
  border: 1px solid var(--line); background: var(--page-bg);
}
.imp-stat-n { display: block; font-size: 1.15rem; font-weight: 700; color: var(--ink-900); line-height: 1.2; }
.imp-stat-l { display: block; font-size: .7rem; color: var(--muted); }
/* The updates are the ones worth noticing - they overwrite something. */
.imp-stat-warn { border-color: var(--warn-bg); background: var(--warn-bg); }
.imp-stat-warn .imp-stat-n, .imp-stat-warn .imp-stat-l { color: var(--warn-ink); }

.imp-updating {
  margin-top: .7rem; padding: .6rem .65rem;
  border: 1px solid #fed7aa; border-radius: var(--radius-field); background: var(--warn-bg);
}
/* The badges sit ON the warn panel, so they cannot also be warn-tinted - they
   would disappear into it. White chips, warm border. */
.imp-updating-list .status-badge {
  background: #fff; color: var(--warn-ink); border: 1px solid #fed7aa;
}
.imp-updating-hd {
  display: flex; align-items: flex-start; gap: .4rem;
  font-size: .76rem; font-weight: 600; color: var(--warn-ink); margin-bottom: .5rem;
}
.imp-updating-list {
  display: flex; flex-wrap: wrap; gap: .3rem;
  max-height: 160px; overflow-y: auto; -webkit-overflow-scrolling: touch;
}
.imp-more { font-size: .74rem; color: var(--muted); align-self: center; }

/* The per-row error list. Scrolls inside itself so the footer buttons stay put
   however many rows failed. */
.imp-rows {
  margin-top: .6rem; max-height: 240px; overflow-y: auto; -webkit-overflow-scrolling: touch;
  border: 1px solid var(--line); border-radius: var(--radius-field); background: #fff;
}
.imp-row-err {
  display: flex; align-items: flex-start; gap: .5rem;
  padding: .4rem .55rem; border-bottom: 1px solid var(--line);
  font-size: .78rem; color: var(--body);
}
.imp-row-err:last-child { border-bottom: none; }
.imp-row-n {
  flex-shrink: 0; font-weight: 600; color: var(--danger-ink);
  background: var(--danger-bg); border-radius: 5px; padding: 0 .35rem;
}
.imp-rows .imp-more { display: block; padding: .4rem .55rem; }

/* A password eye or clear button sitting inside the field. The type selector is
   the reliable path; :has() then covers any other field that carries a toggle. */
.fl-field .pw-toggle { right: 8px; }
.fl-field input[type="password"], .fl-field input[type="text"].has-toggle { padding-right: 42px; }
.fl-field:has(.pw-toggle) input { padding-right: 42px; }

/* Compact variant for filter bars and other dense rows. Same language, less height. */
.fl-field.fl-sm input, .fl-field.fl-sm select, .fl-field.fl-sm textarea {
  padding: 6px 10px; font-size: .82rem;
}
.fl-field.fl-sm label { left: 10px; font-size: .82rem; }
.fl-field.fl-sm:focus-within label,
.fl-field.fl-sm.has-value:not(:focus-within) label,
.fl-field.fl-sm.fl-select label { font-size: .68rem; }
.fl-field.fl-sm.fl-select select { padding-right: 30px; }
.form-check-input:checked { background-color: var(--accent); border-color: var(--accent); }
.form-check-input:focus { box-shadow: 0 0 0 .2rem rgba(79,70,229,.2); border-color: #6366f1; }
.form-switch .form-check-input { cursor: pointer; }

.upload-zone {
  border: 2px dashed var(--field-border); border-radius: 12px;
  padding: 2rem; text-align: center; cursor: pointer;
  transition: all .2s; background: #fafafa;
}
.upload-zone:hover, .upload-zone.drag-over { border-color: var(--accent); background: #f5f3ff; }
.upload-zone i { font-size: 2.5rem; color: var(--faint); display: block; margin-bottom: .5rem; }

.pw-toggle {
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  background: none; border: none; color: var(--faint); cursor: pointer;
  padding: .35rem .5rem; border-radius: 6px; z-index: 4; line-height: 1;
}
.pw-toggle:hover { color: #475569; background: #f1f5f9; }


/* ==========================================================================
   9. Dialogs
   Header and footer are TINTED SECTIONS with their own rules; the body is white
   between them. The three parts must read as separate bands, not one surface.
   ========================================================================== */
.modal-content { border-radius: var(--radius); border: none; overflow: hidden; }
.modal-top .modal-dialog { margin-top: 5vh; margin-bottom: 1rem; }

/* ── Header band ─────────────────────────────────────────────────────────── */
.modal-hd {
  display: flex; align-items: center; gap: .75rem; flex-shrink: 0;
  padding: .6rem 1.15rem;
  background: #f8fafc; border-bottom: 1px solid #e8edf5;
}
.modal-hd-icon {
  width: 30px; height: 30px; border-radius: 9px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center; font-size: .95rem;
}
.mhi-primary { background: var(--accent-tint); color: var(--violet-ink); }
.mhi-warning { background: #fef3c7; color: #b45309; }
.mhi-danger  { background: var(--danger-bg); color: var(--danger); }
.modal-hd-title { flex: 1; margin: 0; font-weight: 700; color: var(--ink-900); font-size: .95rem; }
.modal-hd-close {
  background: none; border: none; color: var(--faint); cursor: pointer;
  padding: .3rem; font-size: .9rem; border-radius: 6px;
  display: flex; align-items: center; line-height: 1;
}
.modal-hd-close:hover { color: #475569; background: #eef2f7; }

/* ── Body ────────────────────────────────────────────────────────────────── */
.modal-body { padding: 1.15rem; background: #fff; }

/* ── Footer band - status switch left, actions right ─────────────────────── */
.modal-footer {
  padding: .3rem 1.15rem; margin: 0;
  background: #f8fafc; border-top: 1px solid #e8edf5;
  display: flex; align-items: center; justify-content: space-between; gap: .6rem;
}
.modal-footer .footer-actions { display: flex; gap: .5rem; margin-left: auto; }
.status-toggle { cursor: pointer; width: 2.4em; height: 1.25em; margin-top: 0; }
.status-toggle:checked { background-color: var(--accent); border-color: var(--accent); }
.status-toggle-lbl {
  font-size: .84rem; font-weight: 600; color: var(--muted);
  cursor: pointer; user-select: none; margin-left: .1rem;
}

/* Destructive and cautionary dialogs tint the header band. */
.modal-hd.modal-hd-danger { background: var(--danger-bg); border-bottom-color: #fecaca; }
.modal-hd.modal-hd-danger .modal-hd-title { color: #991b1b; }
.modal-hd.modal-hd-warn { background: #fffbeb; border-bottom-color: #fde68a; }
.modal-hd.modal-hd-warn .modal-hd-title { color: #92400e; }

/* ==========================================================================
   10. Permission matrix
   ========================================================================== */
.perm-matrix { font-size: .8rem; }
.perm-matrix th { color: var(--muted); font-weight: 600; text-align: center; font-size: .72rem; padding: .4rem .3rem; }
.perm-matrix th:first-child, .perm-matrix td:first-child { text-align: left; }
.perm-matrix td { text-align: center; vertical-align: middle; padding: .4rem .3rem; }
.perm-matrix .form-check-input { cursor: pointer; margin: 0; }
.module-name { font-weight: 600; color: var(--ink-900); font-size: .8rem; }
.module-info { color: var(--faint); font-size: .8rem; cursor: pointer; margin-left: .35rem; }
.module-info:hover { color: var(--accent); }

/* Three levels, each stepped in from the one above (2026-08-03):

     AREA            the same areas as the sidebar - a module name alone does not
                     say which part of the app it belongs to
       Module        its five action columns
         Extra       a permission of that module with no column to sit in

   All in ONE table, never a table per area: the five action columns have to stay
   in a single alignment down the whole dialog.

   The indents are the only thing saying what belongs to what, so they are set
   here as one scale rather than per row. */
.perm-matrix .perm-group td {
  background: var(--accent-wash); border-top: 1px solid var(--card-border);
  padding: .4rem .55rem; font-size: .72rem; font-weight: 700;
  letter-spacing: .4px; text-transform: uppercase; color: var(--ink-700);
}
.perm-matrix tr.perm-group:first-child td { border-top: none; }

/* The area row is an ACCORDION HEADER (2026-08-17).
   Collapsing hides the rows in place - `hidden` on the <tr>, never a removal:
   saving is full-replace, so an unmounted row is a revoked permission. The UA
   rule for [hidden] is display:none, but the same author-declaration trap that
   bit .f-count[hidden] applies to anything that later declares a display on
   these rows, so it is stated here rather than relied on. */
.perm-matrix tr[hidden] { display: none; }

.perm-area-hd { display: flex; align-items: center; gap: .5rem; }

/* A button does NOT inherit type - the area row's uppercase/weight/tracking all
   have to be handed to it or the header changes font mid-row. */
.perm-area-toggle {
  flex: 1 1 auto; display: flex; align-items: center; gap: .5rem;
  background: none; border: 0; padding: 0; text-align: left;
  font: inherit; color: inherit;
  letter-spacing: inherit; text-transform: inherit;
}
.perm-area-toggle:hover { color: var(--accent); }
.perm-area-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.perm-caret { font-size: .7rem; transition: transform .15s ease; }
.perm-area-toggle[aria-expanded="true"] .perm-caret { transform: rotate(90deg); }

/* The count is the only thing a collapsed area shows about its contents, so it
   sits BESIDE THE NAME and not against the right edge: the area row spans all
   six columns, and below ~700px that table scrolls sideways inside .table-wrap -
   a right-aligned badge is then off-screen on exactly the device that most needs
   the areas shut. */
.perm-area-count {
  margin-left: .15rem; text-transform: none; letter-spacing: 0;
  font-size: .7rem; font-weight: 600; color: var(--accent);
  background: #fff; border: 1px solid var(--card-border);
  border-radius: 999px; padding: .05rem .45rem;
}
.perm-area-count.is-none { color: var(--faint); }
.perm-area-count.is-all  { color: #166534; border-color: #bbf7d0; background: #f0fdf4; }

/* Level 2. The guide line is what makes the nesting readable at a glance rather
   than inferred from whitespace. */
.perm-matrix .perm-module > td:first-child { padding-left: 1.35rem; }

/* Level 3, hung off its OWN module's row - not collected at the foot of the
   area, which is what put "Backup - Download" under Notification Settings. */
.perm-matrix .perm-extra td {
  padding: .3rem .55rem .3rem 2.9rem; font-size: .78rem; color: var(--body);
  position: relative;
}
.perm-matrix .perm-extra td::before {
  content: ''; position: absolute; left: 1.95rem; top: 0; bottom: 50%;
  border-left: 1px solid var(--field-border);
  border-bottom: 1px solid var(--field-border);
  width: .55rem; border-bottom-left-radius: 4px;
}
.perm-matrix .perm-extra .perm-key { margin-left: .1rem; }

@media (max-width: 575px) {
  .perm-matrix .perm-module > td:first-child { padding-left: .9rem; }
  .perm-matrix .perm-extra td { padding-left: 2.1rem; }
  .perm-matrix .perm-extra td::before { left: 1.3rem; }
}

/* ==========================================================================
   11. (removed) Activity feed
   ==========================================================================
   The dashboard's Recent Activity feed was removed on 2026-08-29, and
   .activity-item / -ico / -txt / -time went with it - that feed was their only
   user, the Activity Log screen being a table.

   ⚠ THE SECTION NUMBER IS KEPT AND THE GAP IS DELIBERATE. CLAUDE.md and
   DESIGN_SYSTEM.md cite §15b-2, §15c and §15d by number; renumbering twelve
   headings to close one hole would break every one of those references.
   ========================================================================== */

/* ==========================================================================
   12. Auth screens
   ========================================================================== */
.auth-body { background: var(--page-bg); }
.auth-wrap { display: flex; min-height: 100dvh; }
.auth-brand {
  flex: 1 1 46%; max-width: 560px;
  background: linear-gradient(150deg, #1e1b4b 0%, #312e81 50%, #4f46e5 100%);
  color: #fff; align-items: center; justify-content: center; padding: 3rem 2.5rem;
}
/* Centred as a block: the logo is an inline <img>, so text-align is what puts
   it on the same vertical axis as the company name and tagline below it.
   Matches .acb-name / .acb-sub, which already centre on the mobile card. */
.auth-brand-inner { max-width: 380px; text-align: center; }
.auth-logo {
  width: 120px; border-radius: 12px; margin-bottom: 1.4rem; padding: 8px;
  background: #fff; box-shadow: 0 0 32px rgba(79,70,229,.35); object-fit: cover;
}
.auth-brand h2 { font-size: 1.5rem; font-weight: 800; letter-spacing: .3px; margin: 0; }
.auth-tagline { font-size: .78rem; color: var(--accent-soft); margin-top: .4rem; letter-spacing: .4px; }
.auth-brand-divider { height: 1px; background: rgba(255,255,255,.15); margin: 1.6rem 0 1.2rem; }
.auth-brand-sub { font-size: .82rem; color: rgba(255,255,255,.7); font-weight: 600; }
.auth-points { list-style: none; padding: 0; margin: 1.1rem 0 0; }
.auth-points li { font-size: .82rem; color: rgba(255,255,255,.78); padding: .35rem 0; display: flex; gap: .6rem; align-items: center; }
.auth-points i { color: var(--accent-soft); }

.auth-pane {
  flex: 1 1 54%; display: flex; flex-direction: column;
  align-items: center; justify-content: center; padding: 1.5rem 1rem;
}
.auth-card {
  width: 100%; max-width: 400px; background: #fff;
  border: 1px solid var(--card-border); border-radius: var(--radius);
  box-shadow: 0 10px 40px rgba(15,23,42,.08); padding: 1.75rem 1.6rem;
}
.auth-card-brand { align-items: center; gap: .7rem; margin-bottom: 1.4rem; }
.auth-logo-sm {
  width: 80px; border-radius: 8px; flex: 0 0 auto; padding: 4px;
  margin-bottom:24px;background: #fff; border: 1px solid var(--card-border); object-fit: cover;
}
.acb-name { text-align:center;font-weight: 800; color: var(--ink-900); font-size: 1.3rem; line-height: 1.2; }
.acb-sub  { text-align:center;font-size: .8rem; color: var(--muted); letter-spacing: .3px; }
.auth-title { font-size: 1.18rem; font-weight: 800; color: var(--ink-900); margin: 0 0 .2rem; }
.auth-sub { font-size: .81rem; color: var(--muted); margin: 0 0 1.3rem; }
.auth-foot { font-size: .72rem; color: var(--faint); margin: 1.2rem 0 0; text-align: center; }
.auth-link { font-size: .8rem; color: var(--accent); text-decoration: none; font-weight: 600; }
.auth-link:hover { text-decoration: underline; }

/* ==========================================================================
   13. Toast - IMS alert styling; stacked so two toasts never overlap
   ========================================================================== */
.toast-stack {
  position: fixed; top: .75rem; right: .75rem; z-index: 9999;
  display: flex; flex-direction: column; gap: .5rem;
  max-width: min(360px, calc(100vw - 1.5rem));
}
.toast-stack .alert {
  margin: 0; border-radius: 10px; box-shadow: var(--shadow-pop); font-size: .83rem;
}

/* ==========================================================================
   14. Date range picker - shared component, every screen that filters by date
   ========================================================================== */
.dr-wrap { position: relative; display: inline-block; }
.dr-trigger {
  display: inline-flex; align-items: center; gap: .45rem;
  background: #fff; border: 1px solid var(--card-border); border-radius: var(--radius-field);
  padding: .35rem .7rem; font-size: .8rem; color: #475569; cursor: pointer; white-space: nowrap;
}
.dr-trigger:hover { border-color: var(--accent-soft); color: var(--accent); }

/* position:fixed and NO left/right/top of its own - DateRangePicker._place()
   sets both, in viewport coordinates, every time the panel opens and on any
   scroll or resize while it is open. The old `right: 0` anchoring is what put
   the panel off the left of the screen: the trigger sits at the left of the
   filter bar, so hanging the panel from its right edge sent it out of view.
   Fixed also means no ancestor's overflow can clip it. */
.dr-panel {
  display: none; position: fixed; z-index: 1300;
  background: #fff; border: 1px solid var(--card-border); border-radius: var(--radius);
  box-shadow: var(--shadow-pop); overflow: hidden;
  max-height: calc(100vh - 16px); overflow-y: auto;
}
.dr-panel.show { display: block; }

.dr-body { display: flex; align-items: stretch; }

/* Presets: a column beside the calendars on desktop, full-width rows stacked
   above them on a phone - the user's reference for the narrow layout. */
.dr-presets {
  display: flex; flex-direction: column; gap: 2px;
  padding: .6rem; border-right: 1px solid var(--line); min-width: 148px;
}
.dr-preset {
  text-align: left; background: none; border: none; border-radius: 7px;
  padding: .4rem .6rem; font-size: .8rem; color: #475569; cursor: pointer; white-space: nowrap;
}
.dr-preset:hover { background: var(--accent-wash); color: var(--accent); font-weight: 600; }

.dr-cals { display: flex; gap: 1.1rem; padding: .7rem .8rem; }
.dr-cal { min-width: 232px; }

.dr-cal-hd { display: flex; align-items: center; justify-content: space-between; margin-bottom: .5rem; }
.dr-cal-title { font-size: .82rem; font-weight: 700; color: var(--ink-900); }
.dr-nav {
  background: none; border: none; color: var(--muted); cursor: pointer;
  padding: .1rem .35rem; border-radius: 6px; line-height: 1; font-size: .8rem;
}
.dr-nav:hover { background: var(--accent-wash); color: var(--accent); }
/* Two months move as a pair: the left one cannot go forward, the right one
   cannot go back, so they can never cross or show the same month twice.
   visibility, not display, so the title stays centred. */
.dr-cal:first-child .dr-next,
.dr-cal:last-child  .dr-prev { visibility: hidden; }

.dr-dow, .dr-grid { display: grid; grid-template-columns: repeat(7, 1fr); }
.dr-dow { margin-bottom: .2rem; }
.dr-dow span {
  text-align: center; font-size: .68rem; font-weight: 600;
  color: var(--faint); padding: .2rem 0;
}
.dr-grid { gap: 1px; }
.dr-d {
  background: none; border: none; cursor: pointer; color: var(--body);
  font-size: .78rem; padding: .34rem 0; border-radius: 6px; text-align: center;
}
.dr-d:hover { background: var(--accent-wash); color: var(--accent); }
.dr-pad, .dr-pad:hover { cursor: default; background: none; }
.dr-d.is-today { font-weight: 800; color: var(--ink-900); }
.dr-d.in-range { background: var(--accent-wash); color: var(--accent); border-radius: 0; }
.dr-d.is-start, .dr-d.is-end {
  background: var(--accent); color: #fff; font-weight: 700;
}
.dr-d.is-start { border-radius: 6px 0 0 6px; }
.dr-d.is-end   { border-radius: 0 6px 6px 0; }
.dr-d.is-start.is-end { border-radius: 6px; }

.dr-foot {
  display: flex; align-items: center; justify-content: space-between; gap: .6rem;
  border-top: 1px solid var(--line); padding: .6rem .8rem; background: #fcfdff;
}
.dr-sum { font-size: .78rem; color: var(--muted); font-weight: 600; }
.dr-acts { display: flex; gap: .4rem; flex-shrink: 0; }

/* One month below 992 - two will not fit beside a preset column. */
@media (max-width: 991px) {
  .dr-cal:last-child { display: none; }
  .dr-cal:first-child .dr-next { visibility: visible; }
}

/* Phone: presets become full-width rows ABOVE a single month and the panel
   spans the viewport less its margin. _place() still clamps it, so it cannot
   leave the screen on either axis. */
@media (max-width: 575px) {
  .dr-panel { width: calc(100vw - 16px); }
  .dr-body { flex-direction: column; }
  .dr-presets {
    border-right: none; border-bottom: 1px solid var(--line);
    min-width: 0; padding: .5rem;
  }
  .dr-preset {
    border: 1px solid var(--card-border); border-radius: var(--radius-field);
    padding: .5rem .7rem; margin-bottom: 1px;
  }
  .dr-cals { padding: .6rem .5rem; }
  .dr-cal { min-width: 0; width: 100%; }
  .dr-d { padding: .5rem 0; }
  .dr-foot { padding: .6rem .5rem; }
  .dr-sum { font-size: .74rem; }
}

/* ==========================================================================
   15. Searchable select - the dropdown every <select> opens

   The native element is still there and still holds the value; only its own
   dropdown is suppressed. So there is nothing to style for the closed state -
   .fl-select already draws the field and its chevron. This is the panel alone.

   Appended to <body> and positioned with position:fixed by csPlace(), for the
   same reason as the date panel: inside .fl-field it would be clipped by the
   dialog's overflow, and anchored with CSS offsets it would open off-screen.
   ========================================================================== */
.cs-pop {
  position: fixed; z-index: 1400; background: #fff;
  border: 1px solid var(--card-border); border-radius: var(--radius-sm);
  box-shadow: var(--shadow-pop); overflow: hidden;
  max-width: calc(100vw - 16px);
}
/* Above the date panel (1300) and Bootstrap's modal (1055): a select inside a
   dialog must not open behind the dialog it belongs to. */

.cs-search { display: flex; align-items: center; gap: .4rem; padding: .45rem .6rem; border-bottom: 1px solid var(--line); }
.cs-search i { color: var(--faint); font-size: .78rem; }
.cs-search input {
  border: none; outline: none; width: 100%; font-size: .82rem;
  color: var(--body); background: none; padding: .1rem 0;
}
.cs-search input::placeholder { color: var(--faint); }

/* Short lists keep the input - typing still filters - but collapse it out of
   sight. display:none would make it unfocusable and the keyboard path would die
   with it, so it is squashed to nothing instead. */
.cs-pop.cs-quiet .cs-search {
  height: 0; padding: 0; border-bottom: none; overflow: hidden; opacity: 0;
}

.cs-list { max-height: 260px; overflow-y: auto; padding: .25rem; }
.cs-opt {
  padding: .4rem .6rem; font-size: .82rem; color: var(--body);
  border-radius: 6px; cursor: pointer; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
/* One highlight, driven by the keyboard. Hover deliberately does NOT paint its
   own: with both, the mouse resting anywhere over the list makes it impossible
   to see which option Enter would take. */
.cs-opt.is-active { background: var(--accent-wash); color: var(--accent); font-weight: 600; }
.cs-opt.is-sel::after { content: '\F26E'; font-family: 'bootstrap-icons'; float: right; opacity: .7; font-size: .72rem; }
.cs-empty { padding: .6rem; font-size: .78rem; color: var(--faint); text-align: center; }

/* Two-line option (data-cs-sub / data-cs-aside on the <option>, 2026-09-27).
   Main text and aside on the first line, the sub line under both. It WRAPS
   rather than ellipsising - the whole point is that nothing is cut off on a
   phone. A grid, so the selected tick (::after, float in the one-line row) gets
   a column of its own instead of floating into the aside. */
.cs-opt.cs-opt2 {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: baseline; white-space: normal; overflow: visible;
}
.cs-opt2 .cs-main  { grid-column: 1; grid-row: 1; font-weight: 600; overflow-wrap: anywhere; }
.cs-opt2 .cs-aside { grid-column: 2; grid-row: 1; margin-left: .6rem; font-size: .76rem; font-weight: 600; color: var(--muted); white-space: nowrap; }
.cs-opt2 .cs-sub   { grid-column: 1 / -1; grid-row: 2; font-size: .76rem; font-weight: 500; color: var(--muted); overflow-wrap: anywhere; margin-top: 1px; }
.cs-opt2.is-sel::after { grid-column: 3; grid-row: 1; float: none; margin-left: .4rem; }
.cs-opt2.is-active .cs-aside,
.cs-opt2.is-active .cs-sub { color: var(--accent); }

@media (max-width: 575px) {
  .cs-list { max-height: 45vh; }
  .cs-opt { padding: .55rem .6rem; }     /* touch target */
}

/* ==========================================================================
   15b. Step rail - the multi-step entry form
   ==========================================================================
   A RAIL, NOT A TAB BAR. Tabs say "these panes are peers, go anywhere"; the
   production entry's three steps are not peers - the hourly counts mean nothing
   before the machine and cycle time that give them a target, and Review has
   nothing to review until both are in. So the steps are numbered, ordered, and
   locked until earned. (`.tab-bar` / `.tab-btn` / `.tab-panel` were deleted in
   session 6 and are not coming back; there was nothing to reuse either way.)

   ONE set of markup, three shapes:
     <768   a single "Step 1 of 3 · Basic Details" line and a progress bar.
            Three items with hints is 200px of a 640px phone viewport spent on
            navigation, on a screen you opened to type into.
     768-991 a horizontal strip of three, hints dropped.
     ≥992   the vertical rail, 220px, sticky so it stays with you down a long
            form. Affordable on a laptop; a third of a 375px screen, which is
            why it is not there.                                              */
.step-wrap { display: flex; flex-direction: column; gap: 1rem; }
/* ⚠ THE LEFT COLUMN IS .step-side, NOT .step-rail. The rail is the nav alone;
   the side is the nav plus anything that belongs beside the form - today the
   bulk-entry block on a new entry. It exists because .step-wrap is a flex ROW
   from 992px, so a second block added as a child of it becomes a third column
   and takes its width out of the form. The 220px basis and the sticky both live
   here for the same reason: they describe the COLUMN, and the rail is only its
   first occupant. */
.step-side { display: flex; flex-direction: column; gap: 1rem; width: 100%; min-width: 0; }
.step-rail { width: 100%; }
/* min-width:0 or a wide select inside a flex child refuses to shrink and pushes
   the whole page sideways - the one thing R9 forbids outright. */
.step-body { width: 100%; min-width: 0; }

.step-item {
  display: none;                       /* the phone shows .step-mini instead */
  width: 100%; align-items: center; gap: .65rem;
  padding: .55rem .7rem; border: 1px solid transparent;
  border-radius: var(--radius-sm); background: transparent;
  text-align: left; cursor: pointer; font-size: .82rem;
}
.step-num {
  flex: 0 0 auto; width: 26px; height: 26px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: .76rem; font-weight: 700; line-height: 1;
  background: #e2e8f0; color: var(--muted); transition: background .15s, color .15s;
}
.step-txt { display: flex; flex-direction: column; min-width: 0; gap: 1px; }
.step-name {
  font-weight: 600; color: var(--body);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.step-hint { display: none; font-size: .69rem; color: var(--faint); }

.step-item.is-current { background: var(--accent-wash); border-color: var(--accent-soft); }
.step-item.is-current .step-num { background: var(--accent); color: #fff; }
.step-item.is-current .step-name { color: var(--ink-700); }

/* Done keeps its NUMBER and only changes colour. A tick would say more, but it
   would also mean hard-coding a Bootstrap Icons codepoint into a ::before, and a
   codepoint guessed wrong renders as an empty box on every completed step. */
.step-item.is-done .step-num { background: var(--ok-bg); color: var(--ok-ink); }

.step-item.is-locked { cursor: not-allowed; opacity: .5; }
.step-item.is-locked .step-name { color: var(--muted); }
.step-item:not(.is-locked):not(.is-current):hover { background: #f8fafc; }

/* The phone's whole rail. Its bar is the only thing left saying how far in you
   are once the numbered list is gone. */
.step-mini {
  background: var(--card-bg); border: 1px solid var(--card-border);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-card);
  padding: .6rem .8rem;
}
.step-mini-row { display: flex; align-items: baseline; gap: .5rem; }
.step-mini-count {
  font-size: .66rem; font-weight: 700; letter-spacing: .6px;
  text-transform: uppercase; color: var(--faint); flex: 0 0 auto;
}
.step-mini-name {
  font-size: .86rem; font-weight: 600; color: var(--ink-700);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.step-mini-bar {
  display: block; height: 4px; margin-top: .45rem;
  border-radius: 2px; background: #e2e8f0; overflow: hidden;
}
.step-mini-bar i { display: block; height: 100%; background: var(--accent); transition: width .2s ease; }

.step-actions { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-top: 1rem; }

/* ⚠ DOCUMENT ORDER IS SAVE, PREVIOUS, CANCEL; `order` paints them the other way
   round. Tab from the form's last field has to reach Save & Continue rather than
   Cancel (the user's call, 2026-08-06), and document order is the honest way to
   set tab order - scattering tabindex values to fix a sequence is how a form
   ends up with a tab order nobody can predict.

   Visually: Previous and Cancel hard left, Save hard right. Opposite intentions
   must not read as a pair of adjacent buttons you might hit either of - the same
   call the filter panel's footer makes. */
.step-actions .sa-prev        { order: 1; }
.step-actions .sa-cancel      { order: 2; }
.step-actions .footer-actions { order: 3; margin-left: auto; }

@media (max-width: 575px) {
  /* ⚠ ONE SHARED ROW AT EVERY WIDTH (the user's call, 2026-08-17).
     This block used to give Save its own full-width row, on the stated grounds
     that "at 375px three buttons on one line leaves each too narrow to carry
     its label". Measured, that is not true - it assumed the three would split
     the row equally. Their natural widths at 375px are Previous 88, Cancel 59
     and Save 152; with two 8px gaps that is 315px inside a 359px row.

     What it cost was worse than the space it saved: with Previous hidden on
     step 1, the row above held nothing but a 59px Cancel pinned left, so the
     rare action that loses your work sat alone ABOVE the one taken every time.

     Save keeps `order: 3` and stays hard right, and GROWS into whatever is
     left - 292px on step 1, 196px with Previous showing - so the primary is
     always the widest target on the row rather than leaving a gap beside it.
     flex-wrap on the parent is the safety net below 375px: Save drops to its
     own line rather than crushing its label. */
  .step-actions .footer-actions { flex: 1 1 auto; }
  .step-actions .footer-actions .btn { width: 100%; }

  /* ⚠ .sa-plain OPTS OUT OF THE STRETCH ABOVE, and exists because the rule is
     right for the entry form and wrong everywhere else. There, Save is the
     repeated action across three steps and being the widest target on the row
     is a help. On a screen whose primary button is a ONE-SHOT - bulk entry
     books every machine you ticked and consumes a production number for each -
     a 292px slab beside a 59px Cancel is not emphasis, it is a button that
     looks like the only thing on the row. Natural width, hard right. */
  .step-actions.sa-plain .footer-actions { flex: 0 0 auto; }
  .step-actions.sa-plain .footer-actions .btn { width: auto; }
}

@media (min-width: 768px) {
  .step-mini { display: none; }
  .step-item { display: flex; flex: 1 1 0; min-width: 0; }
  .step-rail {
    display: flex; gap: .35rem; padding: .5rem;
    background: var(--card-bg); border: 1px solid var(--card-border);
    border-radius: var(--radius); box-shadow: var(--shadow-card);
  }
}

@media (min-width: 992px) {
  .step-wrap { flex-direction: row; gap: 1.25rem; align-items: flex-start; }
  /* The column's width and its sticky behaviour - see the note on .step-side.
     The rail keeps only what describes the rail itself. */
  .step-side { flex: 0 0 220px; width: 220px; position: sticky; top: 1rem; }
  .step-rail { flex-direction: column; padding: .6rem; }
  .step-item { flex: 0 0 auto; }
  .step-hint { display: block; }
}

/* ── The bulk breakdown entry block ──────────────────────────────────────────
   Sits under the step rail on a NEW entry only. Styled as a quiet card rather
   than a button: it is an alternative route through the module, not an action
   on the form beside it, and a filled button here would compete with Save. */
.bk-cta {
  display: flex; align-items: center; gap: .65rem;
  padding: .65rem .7rem; text-decoration: none;
  background: var(--card-bg); border: 1px solid var(--card-border);
  border-radius: var(--radius); box-shadow: var(--shadow-card);
  transition: border-color .15s, background .15s;
}
.bk-cta:hover { background: var(--accent-wash); border-color: var(--accent); }
.bk-cta-icon {
  flex-shrink: 0; display: flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: 50%;
  background: var(--accent-wash); color: var(--accent); font-size: .9rem;
}
.bk-cta-txt { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: .1rem; }
.bk-cta-name { font-size: .82rem; font-weight: 600; color: var(--body); line-height: 1.2; }
.bk-cta-hint { font-size: .7rem; color: var(--muted); line-height: 1.3; }
.bk-cta-go { flex-shrink: 0; color: var(--faint); font-size: .75rem; }
.bk-cta:hover .bk-cta-go { color: var(--accent); }

/* ── Hourly count grid - step 2 of the production entry ───────────────────────
   ONE LINE PER HOUR, and the line has TWO HALVES.

   Left, what the hour PRODUCED: the label, whether it was worked, the quantity,
   and the running total that quantity produces. The reference screen splits
   those last two into a left column of twelve inputs and a right column of
   twelve read-only boxes, which puts hour 7's output and hour 7's total 700px
   apart with nothing joining them. Reading across one row is the whole point.

   Right, tinted, what the hour LOST (2026-08-06): one reason, the minutes, and
   the supervisor who approved it. The tint is the whole reason the two halves
   can share a line without being read as one list of seven equal fields - it
   says "different kind of fact" before anything is read.

   ⚠ THE TINT IS AMBER, NOT RED. Lost minutes are a normal, expected, recorded
   part of a shift; red is for something being wrong with the form.

   ⚠ ONE TABLE, NOT TWELVE CARDS (2026-08-06, the user's call). Each hour was a
   separately bordered, rounded, gapped card, and twelve of them read as twelve
   unrelated forms rather than one register - which is what a shift sheet is. So:
   one bordered container, a shaded header, and rows separated by a single hair
   line. The gap is ZERO on purpose - it is what lets the tinted rejection cells
   meet and form a continuous column band down the table.

   THREE SHAPES. Above 1200 the whole line fits. From 576 to 1199 the tinted half
   drops to a second line of its own, full width, still tinted, so it stays
   attached to its hour. Below 576 everything folds to one control per line -
   seven columns at 375px is what R9 forbids, and shrinking them to fit gives
   inputs too narrow to show four digits.                                       */
.hour-grid {
  display: flex; flex-direction: column; gap: 0;
  background: #fff;
  border: 1px solid var(--card-border); border-radius: var(--radius-field);
  overflow: hidden;                    /* the band and the rows meet the corners */
}

.hour-head, .hour-row, .hour-foot {
  display: grid;
  /* Qty and Min are three or four digits and no more, so they are narrow on
     purpose - a wide box invites a number that does not belong in it. */
  grid-template-columns: minmax(110px, 1fr) 58px 66px 86px minmax(300px, 1.7fr);
  gap: .45rem; align-items: center;
}
.hour-head {
  padding: .4rem .55rem;
  background: #f8fafc; border-bottom: 1px solid var(--card-border);
  font-size: .66rem; font-weight: 700; letter-spacing: .5px;
  text-transform: uppercase; color: var(--faint);
  /* Every heading centred, on the user's instruction (2026-08-06). The cells
     below follow their own heading rather than the column's data type - a
     centred heading over a right-aligned column reads as a mistake. */
  text-align: center;
}
.hour-head > span:first-child { text-align: left; }

/* The header's three sub-columns must line up with the controls beneath them, so
   it repeats .hour-rej's track sizes rather than guessing at them - and carries
   the same tint, so the band starts at the header rather than under it. The
   the same tint, so the band starts at the header rather than under it. */
.hour-head-rej {
  display: grid;
  grid-template-columns: minmax(130px, 1.4fr) 58px minmax(120px, 1.2fr);
  gap: .4rem;
  align-self: stretch; align-items: center;
  /* Right margin cancels the header's own padding so the band runs clean to the
     container edge instead of stopping 9px short of it. */
  margin: -.4rem -.55rem -.4rem 0; padding: .4rem .55rem .4rem .5rem;
  background: var(--warn-bg); border-left: 1px solid #fed7aa;
}

.hour-row {
  padding: .3rem .55rem;
  border-bottom: 1px solid var(--line);
}
.hour-row:last-child { border-bottom: none; }
/* No border to recolour any more, so the whole row washes instead. */
.hour-row:focus-within { background: var(--accent-wash); }
.hour-row.is-off { background: #f8fafc; }

/* The label cell carries the time AND the row's cycle marker - the flag that
   ends the cycle here, or the production number that already owns this hour.
   They share a cell rather than taking a seventh grid column: a column costs
   width in all three shapes of this row, and below 576 the label already spans
   the full width so the marker rides along with no layout of its own. */
.hour-lbl {
  display: flex; align-items: center; justify-content: space-between; gap: .4rem;
  font-size: .8rem; font-weight: 600; color: var(--body); min-width: 0;
}
.hl-time { white-space: nowrap; }
.hour-row.is-off .hour-lbl { color: var(--muted); }

/* ── Where the cycle ends ─────────────────────────────────────────────────────
   A machine can change job mid-shift, so one entry covers 08:00-12:00 and the
   next covers 12:00-20:00. The flag states where this one stops; the tag names
   the entry that owns an hour this one cannot use. */

/* ⚠ ALWAYS VISIBLE, NEVER HOVER-REVEALED - the same call the calculator's
   favourite stars settled: :hover does not fire on the touch screens this is
   used on, so a control that appears on hover is a control that does not exist.
   Faint until it means something, full strength once it does. */
.hl-end {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; padding: 0;
  border: none; background: none; border-radius: var(--radius-field);
  font-size: .8rem; line-height: 1; color: var(--faint); opacity: .45;
  cursor: pointer; transition: opacity .12s, color .12s, background-color .12s;
}
.hl-end:hover, .hl-end:focus-visible { opacity: 1; color: var(--accent); background: var(--accent-wash); }
.hour-row.is-end .hl-end { opacity: 1; color: var(--accent); }

/* The production number that owns this hour. Not a link: the way to take an
   hour back is to shorten the cycle holding it, on that entry - and a link out
   of a half-filled form is an invitation to lose what is in it. */
.hl-tag {
  flex: none; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
  font-size: .62rem; font-weight: 700; letter-spacing: .3px; white-space: nowrap;
  padding: .1rem .3rem; border-radius: 4px;
  background: #e2e8f0; color: var(--ink-700);
}

/* ⚠ THE CUT LINE IS WHAT MAKES THE SPAN READABLE AT A GLANCE. Twelve rows with
   eight of them greyed says "eight hours idle" just as loudly as twelve blank
   boxes did; the rule under the last hour of the cycle is what says "and this is
   where it stopped". */
.hour-row.is-end { border-bottom: 2px solid var(--accent); }

/* An hour outside this cycle. Both states wash out, and both keep their time
   legible - the row still has to be readable, because reading it is how somebody
   works out that the shift was split. */
.hour-row.is-claimed,
.hour-row.is-after-end { background: #f1f5f9; }
.hour-row.is-claimed .hl-time,
.hour-row.is-after-end .hl-time { color: var(--faint); font-weight: 500; }

/* ⚠ THE REJECTION HALF DIMS HERE AND ONLY HERE. .is-off deliberately leaves it
   at full contrast - an unworked hour is exactly where "No Operator, 60 minutes"
   belongs - but an hour belonging to another production number is not this
   entry's to explain at all. */
.hour-row.is-claimed .hour-rej,
.hour-row.is-after-end .hour-rej { opacity: .5; }

/* The register's split-shift marker, beside the production number. Deliberately
   quiet - it is a "there is more to this day" note, not a warning. */
.cycle-chip {
  display: inline-flex; align-items: center; gap: .1rem;
  font-size: .62rem; font-weight: 700; vertical-align: middle;
  padding: .05rem .28rem; border-radius: 4px;
  background: #e0e7ff; color: #3730a3;
}
.cycle-chip i { font-size: .72rem; }

/* The word "Worked" is gone from every row (the user's call, 2026-08-06) - the
   column heading says it once, twelve times was noise, and it cost 30px of a row
   that has seven things in it. The input keeps its aria-label, and below 576px
   where the heading is hidden the ::before below puts a word back. */
.hour-chk {
  display: inline-flex; align-items: center; justify-content: center;
  font-size: .74rem; color: var(--muted); cursor: pointer; user-select: none;
}
.hour-chk input { width: 15px; height: 15px; accent-color: var(--accent); cursor: pointer; margin: 0; }

.hour-in {
  width: 100%; text-align: center;
  border: 1.5px solid var(--field-border); border-radius: var(--radius-field);
  padding: 4px 5px; font-size: .85rem; color: var(--body); background: #fff;
  font-variant-numeric: tabular-nums;
}
.hour-in:focus { outline: none; border-color: var(--accent); }
.hour-in:disabled { background: #f1f5f9; color: var(--faint); cursor: not-allowed; }
/* No spinner. Twelve of them down the column is twelve controls nobody presses,
   and each one steals 16px from a field that has to hold six digits. */
.hour-in[type=number] { -moz-appearance: textfield; appearance: textfield; }
.hour-in::-webkit-outer-spin-button,
.hour-in::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* tabular-nums so the running total forms a straight edge down the column - it
   is read as a sequence, and proportional digits make it ripple. Centred rather
   than right-aligned since the headings were centred (2026-08-06): tabular
   figures in a fixed 86px column still line up, and a centred heading over a
   right-aligned column reads as a mistake. */
.hour-cum {
  text-align: center; font-size: .85rem; font-weight: 600;
  color: var(--ink-700); font-variant-numeric: tabular-nums;
}
.hour-row.is-off .hour-cum { color: var(--faint); }
.hour-cum::before { content: none; }

/* ── The rejection half ───────────────────────────────────────────────────────
   ⚠ NOT dimmed by .is-off. An hour that was not worked is exactly where "No
   Operator - 60 minutes" belongs, so only the quantity greys out; this block
   stays live at full contrast. That is the user's decision of 2026-08-06 and it
   is the one thing about this component not to "tidy up". */
/* ⚠ THE TOTALS ROW TAKES THIS RULE, NOT .hour-head-rej's. The two differ by the
   0.1rem of left padding each needs to cancel its own parent's, and the header
   therefore sits 1px off the inputs below it. A heading may; a SUM may not - it
   has to line up with the twelve figures it adds together, which is the whole
   argument for putting it at the foot of the column. Measured: 1px out against
   the header's padding, exact against the row's.                              */
.hour-rej, .hour-foot-rej {
  display: grid;
  grid-template-columns: minmax(130px, 1.4fr) 58px minmax(120px, 1.2fr);
  gap: .4rem; align-items: center;
  /* Stretched to the full row height with the row's own padding cancelled, so
     twelve of these MEET and read as one continuous column band rather than
     twelve floating chips. That is the whole reason .hour-grid has gap: 0. */
  align-self: stretch;
  margin: -.3rem -.55rem -.3rem 0; padding: .3rem .55rem .3rem .4rem;
  background: var(--warn-bg);
  border-left: 1px solid #fed7aa;
}
/* Filled in, so it earns a little more presence than an empty one. */
.hour-rej.has-rej { background: #ffedd5; }

.hour-sel {
  width: 100%; min-width: 0;
  border: 1.5px solid var(--field-border); border-radius: var(--radius-field);
  padding: 4px 6px; font-size: .8rem; color: var(--body); background: #fff;
  /* Truncated rather than allowed to widen the column: "Machine Cleaning" and a
     six-word supervisor name must not decide the row height. */
  text-overflow: ellipsis;
}
.hour-sel:focus { outline: none; border-color: var(--accent); }
.hour-sel:disabled { background: #f8fafc; color: var(--faint); cursor: not-allowed; }

/* ── The two totals, at the foot of the columns that make them ─────────────
   The user's instruction, 2026-08-24. Total produced sits under Qty., total
   breakdown minutes under Min. - each directly below the twelve figures it is
   the sum of. Both were tiles in the card underneath the grid until then, and a
   tile states a figure without saying where it came from.

   ⚠ IT IS NOT ITS OWN GRID. It is named beside .hour-row in every rule that
   sizes those tracks, at every breakpoint, so a retuned column can never leave a
   total sitting out of line with the numbers above it.

   No border-top: the last hour row keeps its own hair line once something
   follows it, and two rules stacked read as one 2px rule.                      */
.hour-foot {
  /* Matches .hour-row's padding exactly - .hour-foot-rej's negative margins are
     cut to cancel that figure, and a different one here breaks the amber band's
     right edge as well as the alignment. */
  padding: .3rem .55rem;
  background: #f1f5f9;
}
/* Right-aligned so it butts against the figure it names, and set as a column
   heading rather than as a sentence - it is a label. Spans the Hour and Worked
   tracks: neither has a total, and a caption confined to the first one sits a
   column away from its own number. */
.hour-foot > .hf-cap { grid-column: span 2; }
.hf-cap {
  text-align: right; min-width: 0;
  font-size: .66rem; font-weight: 700; letter-spacing: .5px;
  text-transform: uppercase; color: var(--faint);
}
/* Centred on the column above it, tabular so the digits sit under the digits. */
.hf-val {
  text-align: center; font-size: .95rem; font-weight: 700;
  color: var(--ink-700); font-variant-numeric: tabular-nums;
}
/* Amber only once the shift has lost something - a zero in a warning colour is
   a colour people stop reading. */
.hf-val.is-on { color: var(--warn-ink); }
/* The tracks with no total under them. Present so the row's five columns and the
   band's three still resolve; gone below 576 where the row becomes label/value */
.hf-gap { display: block; }

@media (max-width: 1199px) {
  .hour-head, .hour-row, .hour-foot {
    grid-template-columns: minmax(120px, 1.2fr) 66px 74px minmax(80px, .8fr);
  }
  /* On its own line the band has nothing to meet, so it becomes a block again -
     inset, rounded and fully bordered, which is what an isolated tinted area
     should look like. */
  .hour-head-rej, .hour-rej, .hour-foot-rej { grid-column: 1 / -1; }
  .hour-head-rej, .hour-rej, .hour-foot-rej {
    align-self: auto; margin: .25rem 0 0;
    border: 1px solid #fed7aa; border-radius: var(--radius-field);
  }
  .hour-head-rej { text-align: center; }
}

@media (max-width: 575px) {
  .hour-head { display: none; }
  .hour-row { grid-template-columns: auto 1fr; gap: .3rem .5rem; padding: .5rem .6rem; }
  .hour-lbl { grid-column: 1 / -1; }
  .hour-cum {
    grid-column: 1 / -1;
    display: flex; align-items: baseline; justify-content: space-between;
  }
  /* The column header is gone, so the number carries its own label again. */
  .hour-cum::before {
    content: 'Total after'; font-size: .66rem; font-weight: 700;
    letter-spacing: .5px; text-transform: uppercase; color: var(--faint);
  }
  /* Same reason: with the heading hidden, a bare checkbox says nothing about
     what ticking it means. This is the word "Worked" earning its place in the
     one place it still has to. */
  .hour-chk { justify-content: flex-start; gap: .35rem; }
  .hour-chk::after {
    content: 'Worked'; font-size: .7rem; font-weight: 600;
    letter-spacing: .3px; color: var(--muted);
  }
  /* One control per line. The three carry their own placeholder text - Reason,
     Min, Approver - so nothing here needs the header that has just been hidden. */
  .hour-rej { grid-template-columns: 1fr; gap: .3rem; }
  .hour-min { text-align: left; }

  /* The columns are gone, so the two totals stop being column feet and become
     what every other figure on a phone is: a label with its value beside it. */
  .hour-foot { grid-template-columns: 1fr auto; gap: .3rem .5rem; padding: .5rem .6rem; }
  .hour-foot > .hf-cap { grid-column: 1; }
  .hour-foot-rej { grid-template-columns: 1fr auto; }
  .hf-cap { text-align: left; }
  .hf-val { text-align: right; }
  .hf-gap { display: none; }
}

/* An hour over the 60-minute ceiling is marked on the keystroke. .hour-in is not
   a .form-control, so Bootstrap's .is-invalid does not reach it. */
.hour-in.is-invalid { border-color: var(--danger); background: var(--danger-bg); }

/* ── Rejection quantity - .rej-qty ────────────────────────────────────────────
   PIECES rejected across the entry, under the hourly grid. A separate card from
   the grid because it is a separate fact: the hour rows say why minutes were
   lost, this says how many pieces did not pass, and only this one changes a
   quantity.

   Amber like the hourly block, so "rejection" looks like one idea across the
   screen even though the two hold different things.                            */
.rej-qty {
  margin-top: .9rem; padding: .7rem .75rem;
  background: var(--warn-bg); border: 1px solid #fed7aa;
  border-radius: var(--radius-field);
}
.rej-qty-hd {
  display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap;
  margin-bottom: .6rem;
}
.rej-qty-hd h6 {
  margin: 0; font-size: .8rem; font-weight: 700;
  color: var(--warn-ink); text-transform: uppercase; letter-spacing: .5px;
}

/* The outcome of the six fields above it, so it is separated by a rule and
   weighted like a total rather than like another field. */
.rej-qty-out {
  display: flex; align-items: center; justify-content: left;
  gap: .5rem; flex-wrap: wrap;
  margin-top: .7rem; padding-top: .6rem;
  border-top: 1px solid #fed7aa;
}
.rq-lbl { font-size: .78rem; font-weight: 600; color: var(--warn-ink); }
.rq-val {
  font-size: 1.15rem; font-weight: 700; color: var(--ink-700);
  font-variant-numeric: tabular-nums;
}
/* More rejected than produced. Refused on save; said in colour before then. */
.rej-qty.over { border-color: var(--danger); }
.rej-qty.over .rq-val { color: var(--danger); }

/* ── Auto-advance toggle - .hour-auto ─────────────────────────────────────────
   A button that reports a STATE, so it has to look different when it is on -
   `aria-pressed` is the state, and styling from it means the two can never
   disagree. It replaced Fill down, which was an action and needed none of this. */
.hour-auto { display: inline-flex; align-items: center; gap: .4rem; }
.hour-auto[aria-pressed="true"] {
  background: var(--accent-tint); border-color: var(--accent-soft); color: var(--ink-700);
}
.ha-state {
  padding: 0 .4rem; border-radius: 999px;
  background: #e2e8f0; color: var(--muted);
  font-size: .64rem; font-weight: 700; letter-spacing: .5px; text-transform: uppercase;
}
.hour-auto[aria-pressed="true"] .ha-state { background: var(--accent); color: #fff; }

/* ── Total breakdown minutes, reason by reason ─────────────────────────
   EVERY configured reason, in the Configuration list's own order, whether this
   entry used it or not (the user's instruction, 2026-08-24). It replaced a chip
   row carrying the reasons in use, heaviest first - a correct ranking and a
   useless place to look, because a reason changed position on every entry and
   was absent altogether on the entries where it cost nothing. A fixed slot is
   what the printed form has always had, so the two now read the same way.

   ⚠ THE CARD USED TO HOLD SIX FIGURE TILES AS WELL - produced, rejected, after
   rejection, rework, hours worked, ending count. All six are gone (the user's
   instruction, 2026-08-24): the two totals worth having moved to the foot of
   their own columns inside the grid (.hour-foot), and the rest were each already
   stated somewhere the eye was going anyway. What is left is one band, so the
   card is a block rather than a three-column grid.

   ⚠ AND THE TOTAL IS NOT PRINTED IN THE HEADING ANY MORE, for the reason it was
   put there in the first place: one figure, in one place. The heading names what
   the band is instead.                                                         */
.hour-sum {
  margin-top: .9rem; padding: .7rem .75rem;
  background: var(--accent-wash); border: 1px solid var(--card-border);
  border-radius: var(--radius-field);
}
.hs-lbl {
  font-size: .64rem; font-weight: 700; letter-spacing: .5px;
  text-transform: uppercase; color: var(--faint);
}

.hs-brk { display: flex; flex-direction: column; gap: .4rem; }
.hs-brk-hd { display: flex; align-items: baseline; justify-content: space-between; gap: .5rem; }

/* 140px is what puts TWO slots on a 375px phone - the longest seeded reason
   (Machine Cleaning) measures 100px beside a three-digit figure, and the track
   is 148px there. auto-fill, not auto-fit: the tracks must keep their width when
   a plant configures three reasons rather than twelve, or three slots stretch
   across the whole band and read as three cells of their own rather than as a
   list of twelve that happens to be short. */
.hs-brk-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: .3rem .4rem; }
.hs-brk-item {
  display: flex; align-items: baseline; justify-content: space-between; gap: .4rem;
  min-width: 0; padding: 3px 8px; border-radius: var(--radius-field);
  background: var(--card-bg); border: 1px solid var(--card-border);
}
/* Wraps rather than ellipsises: a reason is added from the Configuration screen
   and nothing caps its length, and half a name is worse than two lines. */
.hs-brk-name {
  min-width: 0; font-size: .72rem; font-weight: 600; color: var(--muted);
  overflow-wrap: anywhere;
}
.hs-brk-min {
  font-size: .82rem; font-weight: 700; color: var(--faint);
  font-variant-numeric: tabular-nums;
}
/* Only a slot that cost the shift something is coloured - twelve amber boxes
   are twelve things to read, one amber box is the answer. */
.hs-brk-item.is-on { background: var(--warn-bg); border-color: #fed7aa; }
.hs-brk-item.is-on .hs-brk-name,
.hs-brk-item.is-on .hs-brk-min { color: var(--warn-ink); }
/* The empty state is a sentence, so it takes the whole band - left in a 152px
   track it would set six words over five lines. */
.hs-brk-none { grid-column: 1 / -1; font-size: .78rem; }


/* ==========================================================================
   15b-2. Production entry, step 3 - the review - .rv-*
   ==========================================================================
   Read-only, and rendered from the entry AS STORED. Its blocks are the printed
   daily report's blocks in the same order - job details, hour by hour, downtime,
   rejection, then the derived figures - so what somebody checks on screen is
   what they will later sign on paper. It is drawn in this application's own
   language rather than as a copy of that form: print-report.css reproduces a
   controlled document and was explicitly not a precedent for screens, and the
   form's fixed 47-row grid does not survive 375px.

   Nothing here is a card of its own. The step panel is already a .section-card,
   and five nested cards inside it is the boxes-in-boxes look R9 exists to avoid;
   the blocks are separated by a rule instead.                                  */

.rv-block + .rv-block,
.rv-block + .rv-pair,
.rv-pair  + .rv-block {
  margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--card-border);
}

/* -- Breakdown beside Rejection - .rv-pair ------------------------------------
   The user's instruction, 2026-08-24: the two halves of what a shift lost -
   minutes on one side, pieces on the other - share the screen 50/50. Read
   together, and neither fills a 1366px row on its own: the band is two rows of
   slots and the rejection block is a list plus four sums, so stacked they left a
   column of white space each, which is what R9 forbids.

   ⚠ THE PAIR IS THE BLOCK-LEVEL UNIT, which is why the rule above names it on
   both sides. Get that wrong and the horizontal rule that separates blocks lands
   INSIDE the pair, one column starting a rule lower than the other.

   Above 992 the divider is VERTICAL and belongs to the second column - the pair
   is left on the default `stretch`, deliberately, so that rule runs the full
   height of the taller half whichever half that is. Below 992 the pair collapses
   to one column and the ordinary horizontal rule comes back, which is the whole
   of what the media query undoes. 992 is the breakpoint .rv-calc already uses:
   two 500px halves are fine, two 180px halves are not.

   `min-width: 0` because a grid item's automatic minimum is its content, and the
   rejection block's sums do not wrap.                                          */
.rv-pair { display: grid; grid-template-columns: 1fr; }
.rv-pair > .rv-block { min-width: 0; }
@media (min-width: 992px) {
  .rv-pair { grid-template-columns: 1fr 1fr; column-gap: 1.75rem; }
  .rv-pair > .rv-block + .rv-block {
    margin-top: 0; padding-top: 0; border-top: none;
    padding-left: 1.75rem; border-left: 1px solid var(--card-border);
  }
}
.rv-hd {
  display: flex; align-items: center; justify-content: space-between;
  gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem;
}
.rv-hd h6 {
  margin: 0; font-size: .78rem; font-weight: 700;
  color: var(--ink-700); text-transform: uppercase; letter-spacing: .5px;
}
.rv-sub {
  font-size: .7rem; font-weight: 400; color: var(--muted);
  text-transform: none; letter-spacing: 0;
}

/* ── Job details - every value is a way back to the field that holds it ───────
   A button, not a link with an href: it moves between steps of a form that is
   already open. The pencil appears on hover and on keyboard focus only - twelve
   permanent pencils read as twelve things to do, when the answer is usually
   "these are all correct".                                                     */
.rv-grid {
  display: grid; gap: .25rem;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
}
.rv-item {
  position: relative;
  display: flex; flex-direction: column; gap: .1rem;
  width: 100%; min-width: 0; text-align: left;
  padding: .35rem .5rem;
  background: transparent; border: 1px solid transparent;
  border-radius: var(--radius-field);
  font: inherit; color: inherit;
}
.rv-item:not(.is-static):hover {
  background: var(--accent-wash); border-color: var(--card-border); cursor: pointer;
}
.rv-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.rv-item .bi-pencil {
  position: absolute; top: .4rem; right: .45rem;
  font-size: .7rem; color: var(--accent); opacity: 0; transition: opacity .12s ease;
}
.rv-item:hover .bi-pencil,
.rv-item:focus-visible .bi-pencil { opacity: 1; }

.rv-lbl {
  font-size: .64rem; font-weight: 700; letter-spacing: .5px;
  text-transform: uppercase; color: var(--faint);
}
.rv-val {
  font-size: .88rem; font-weight: 600; color: var(--ink-700);
  font-variant-numeric: tabular-nums; overflow-wrap: anywhere;
}
.rv-val small { font-size: .7rem; font-weight: 600; color: var(--muted); }
/* An em dash reads as "answered, with nothing" where a blank reads as unfinished. */
.rv-dash { color: var(--faint); font-weight: 500; }

/* ── Hour by hour ─────────────────────────────────────────────────────────────
   Inside .table-wrap like every other table here, so the seven columns scroll
   sideways within the card and never widen the page (R9). An hour that was not
   worked is greyed and shows no counter reading - repeating the previous one
   would suggest a reading was taken.                                           */
.rv-table tbody td, .rv-table thead th { vertical-align: middle; }
.rv-n { text-align: right; font-variant-numeric: tabular-nums; }
.rv-table tr.rv-off td { background: #f8fafc; }
/* ⚠ ONLY THE HOUR'S OWN THREE CELLS GREY OUT. The rejection half of an unworked
   hour must read at full strength - "No Operator, 60 minutes" on an empty hour is
   the most valuable line on the row, and dimming it hides exactly the thing the
   row exists to say. The same rule .hour-row.is-off follows on step 2, where
   .hour-rej is deliberately left alone. */
.rv-table tr.rv-off td:nth-child(-n+3) { color: var(--muted); }
.rv-table .rv-act { text-align: right; padding-top: .15rem; padding-bottom: .15rem; }
.rv-table .rv-act .btn { padding: .1rem .4rem; line-height: 1.2; }
.rv-table tfoot .rv-tot td {
  background: var(--accent-wash); font-weight: 700; color: var(--ink-700);
  border-top: 1px solid var(--card-border);
}

/* ── Downtime and rejection lists ─────────────────────────────────────────── */
.rv-rows { display: flex; flex-direction: column; }
.rv-row {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: .75rem; padding: .3rem .1rem; border-bottom: 1px solid var(--line);
  font-size: .83rem;
}
.rv-row:last-child { border-bottom: none; }
.rv-row-lbl { color: var(--body); min-width: 0; }
.rv-row-lbl small { color: var(--muted); font-size: .7rem; }
.rv-row-val {
  font-weight: 700; color: var(--ink-700); font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.rv-row-val small { font-size: .7rem; font-weight: 600; color: var(--muted); }
.rv-row.is-total {
  margin-top: .2rem; padding-top: .45rem;
  border-top: 1px solid var(--card-border); border-bottom: none;
}
.rv-row.is-total .rv-row-lbl { font-weight: 700; color: var(--ink-700); }
/* Produced, less rejected, equals after rejection - set apart so the three read
   as one piece of arithmetic rather than as three more list rows. */
.rv-sums {
  margin-top: .7rem; padding: .5rem .7rem;
  background: var(--accent-wash); border: 1px solid var(--card-border);
  border-radius: var(--radius-field);
}
.rv-tag {
  display: inline-block; margin-left: .3rem; padding: 0 .4rem;
  border-radius: 999px; background: #e2e8f0; color: var(--muted);
  font-size: .62rem; font-weight: 700; letter-spacing: .3px; text-transform: uppercase;
}
.rv-empty { margin: 0; padding: .35rem .1rem; font-size: .8rem; color: var(--muted); }

/* ── The calculations ─────────────────────────────────────────────────────────
   Label above its working, result held at the right. ONE layout at every width:
   the working is a sentence and wraps, so nothing here needs a breakpoint - and
   a three-column table of formulas is the first thing to break at 375px.

   The result column carries the tone, and only on the loss/increase line. A
   shift that beat its target and a shift that missed it are the same fact said
   twice if the efficiency is coloured as well, and two signals for one fact is
   how people stop reading both.                                                */
/* ⚠ TWO COLUMNS OF ROWS FROM 992px, AND THE FIGURE SITS BESIDE ITS OWN LABEL
   (the user's instruction, 2026-08-24: "figures need to be near by so you can
   create two columns"). Before this the block was one full-width column with
   `.rv-res` thrown to the far right by `justify-self: end` across a 1000px row -
   so a label and the number it names could be half a screen apart, and fourteen
   of them ran off the bottom of the card.

   The row is a 2x2 grid: label | value on the first line, the working | its hour
   restatement on the second. `.rv-res` no longer spans both rows - being level
   with the label is the whole point.

   One column below 992: two 500px halves are fine, two 180px halves are not, and
   the working line is prose-length. */
.rv-calc { display: grid; grid-template-columns: 1fr; column-gap: 1.75rem; }
@media (min-width: 992px) { .rv-calc { grid-template-columns: 1fr 1fr; } }
.rv-calc-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline; gap: .1rem .75rem;
  padding: .45rem .1rem; border-bottom: 1px solid var(--line);
}
.rv-calc-row:last-child { border-bottom: none; }
/* The block always renders an EVEN number of rows - twelve since 2026-08-24 - so
   at two columns the last TWO close the grid and both drop their rule. Add or
   remove a row in pairs, or one column ends a rule lower than the other. */
@media (min-width: 992px) {
  .rv-calc-row:nth-last-child(-n+2) { border-bottom: none; }
}
.rv-calc-lbl {
  grid-column: 1; grid-row: 1; font-size: .83rem; font-weight: 600; color: var(--body);
}
.rv-work {
  grid-column: 1; grid-row: 2; font-size: .72rem; color: var(--muted);
  font-variant-numeric: tabular-nums; overflow-wrap: anywhere;
}
.rv-res {
  grid-column: 2; grid-row: 1; justify-self: end;
  font-size: 1.05rem; font-weight: 700; color: var(--ink-700);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.rv-res small { font-size: .7rem; font-weight: 600; color: var(--muted); }
/* Minutes said again in hours, under the figure it restates - the blue column the
   legacy review carried. It is a restatement, never a second measurement, so it
   is quieter than the value above it. */
.rv-note {
  grid-column: 2; grid-row: 2; justify-self: end;
  font-size: .72rem; font-weight: 600; color: var(--info-ink);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.rv-note small { font-size: .65rem; font-weight: 600; opacity: .85; }
/* Attendance is a notation, not a quantity - tabular figures would space "P-2"
   like a number. */
.rv-att { font-variant-numeric: normal; letter-spacing: .02em; }
.rv-calc-row.is-good .rv-res, .rv-calc-row.is-good .rv-note { color: var(--ok-ink); }
.rv-calc-row.is-bad  .rv-res, .rv-calc-row.is-bad  .rv-note { color: var(--danger-ink); }

/* Downtime exceeding the staffed time - a data-entry fault, and the same thing
   the printed form says in its Remarks block rather than showing a bare 0. */
.rv-warn {
  margin: 0 0 .6rem; padding: .5rem .65rem;
  background: var(--warn-bg); border: 1px solid #fed7aa;
  border-radius: var(--radius-field);
  color: var(--warn-ink); font-size: .78rem; font-weight: 600;
}

@media (max-width: 575px) {
  .rv-grid { grid-template-columns: 1fr; }
  .rv-item { padding: .3rem .4rem; }
}

/* ==========================================================================
   15c. Dashboard - .dash-* / .dc-* / .dash-tbl / .mu-*
   ==========================================================================
   Charts are hand-built from HTML boxes and one inline SVG arc. There is no
   chart library and there must not be one: R8 forbids a CDN, and Chart.js was
   deleted on 2026-08-02 as 845K of vendored code with zero references. A bar
   chart is a flex row of divs; buying a dependency for that is how the 845K
   happened the first time.

   ⚠ The bar chart is HTML, NOT SVG, on purpose. An SVG scaled with
   preserveAspectRatio="none" stretches its own axis labels into illegible
   ovals, and doing it properly means recomputing text transforms on resize.
   Boxes in a flex row reflow for free and keep text crisp at 375px.
   ========================================================================== */

/* ── Card head - icon left, action pill right ────────────────────────────────
   ⚠ SCOPED WITH .dash-head, AND THAT MODIFIER IS THE WHOLE POINT. A bare
   `.card-header-custom h6 { display: flex }` reaches all eleven screens, and two
   of them put inline content inside that heading - production-form step 3 is
   `Review <span class="fs-72 muted">Check Every Figure…</span>`, which turns
   into two flex items the moment the parent stops being a block box, losing the
   word space between them. The dashboard opted in; nothing else did.

   The icon exists because this screen shows SEVEN cards at once and a reader
   scanning for downtime should not have to read seven headings to find it. */
.card-header-custom.dash-head h6 { display: flex; align-items: center; gap: .5rem; }
.card-header-custom.dash-head h6 i { color: var(--accent); font-size: 1rem; flex: 0 0 auto; }

/* The action opposite the heading. A pill rather than a bare text link: the same
   words, but a 30px-tall target instead of a 15px line of text, which is what a
   finger on a shop-floor tablet is actually aiming at.

   ⚠ .dash-acts KEEPS THE CAPTION AND THE PILL TOGETHER ON THE RIGHT. Every one
   of these headings already carried a .head-sub, and .card-header-custom spaces
   its children apart - so a pill added as a third child lands with the caption
   stranded in the middle of the row. */
.dash-act {
  display: inline-flex; align-items: center; gap: .3rem;
  background: var(--accent-tint); color: var(--violet-ink);
  border: none; border-radius: 999px; padding: .28rem .7rem;
  font-size: .72rem; font-weight: 600; text-decoration: none; white-space: nowrap;
}
.dash-act:hover, .dash-act:focus { background: #ddd6fe; color: var(--violet-ink); }
.dash-acts { display: inline-flex; align-items: center; gap: .6rem; min-width: 0; }
.dash-acts .head-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── Period bar ───────────────────────────────────────────────────────────── */
.dash-bar { gap: .6rem; }
.dash-range-note { font-size: .75rem; color: var(--muted); }

/* ⚠ `.dash-sample-chip` WAS REMOVED ON 2026-08-30 WITH THE FLAG THAT DROVE IT.
   Phase 2 pointed every measure at the real register, DASHBOARD_SAMPLE_DATA had
   been a constant `false` ever since, and the browser hid this chip on every
   load - a constant, a payload key, a line of JS and this block, all agreeing
   about nothing. */

/* ── Bar chart ────────────────────────────────────────────────────────────── */
.dash-chart { padding: 1rem 1.25rem 1.1rem; }
.dc-plot {
  display: flex; align-items: flex-end; gap: 3px;
  height: 176px; padding-bottom: .35rem;

  /* Quarter gridlines behind the bars - the reference chart's grid, drawn as a
     repeating background rather than four more elements per chart.

     ⚠ THE DAY LABEL IS INSIDE THE COLUMN, SO "THE BOTTOM OF THIS BOX" IS NOT
     THE BARS' ZERO. Anchoring the grid on `left bottom` put every line 19.67px
     below the baseline the bars actually stand on - measured, and invisible to
     anybody checking that the lines merely exist. The label's height is
     subtracted from both the anchor and the step, so the five rules land on the
     bar area's 0 / 25 / 50 / 75 / 100.

     ⚠ WHICH IS WHY --dc-lbl-h ALSO SIZES .dc-lbl ITSELF. A grid that assumed a
     height the label was free to change is a grid that silently slides off the
     baseline the next time somebody retunes that font. One number, both users.

     ⚠ AND --dc-cap-h IS THE SAME BARGAIN AT THE OTHER END. The per-bar
     percentage sits ABOVE the bars, so it takes height out of the stack: the
     anchor is untouched (the baseline is still the baseline) but the STEP must
     lose it too, or a full-height bar overshoots the top gridline by exactly the
     caption. It sizes .dc-pct itself for the same reason the label does. */
  --dc-lbl-h: 20px;
  /* Zero below 768, where .dc-pct is display:none - the grid must not reserve
     height for a caption that is not there. */
  --dc-cap-h: 15px;
  background-image: linear-gradient(to top, var(--line) 1px, transparent 1px);
  background-size: 100% calc((100% - var(--dc-lbl-h) - var(--dc-cap-h)) / 4);
  background-position: left calc(100% - var(--dc-lbl-h));
  background-origin: content-box;
  background-clip: content-box;
}
.dc-col { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; height: 100%; }
.dc-stack { position: relative; flex: 1 1 auto; display: flex; align-items: flex-end; }
/* ⚠ --purple, not --accent. The reference chart is violet-forward and the
   indigo bar sat a shade too close to the grid it stands on. The tone bars
   (is-over / is-under) are unchanged - they carry meaning, not theme. */
.dc-bar {
  width: 100%; background: var(--purple); border-radius: 4px 4px 0 0;
  min-height: 2px; opacity: .85; transition: opacity .15s;
}
.dc-col:hover .dc-bar { opacity: 1; }
/* ⚠ TWO TONES ONLY: green at or above target, ORANGE below it. There used to be
   a third, unmarked band between 85% and target that rendered in the neutral
   violet - on real data most days landed in it, so a missed target looked
   exactly like a met one. The plain .dc-bar violet now means only "no target was
   set", which cannot be over or under one. */
.dc-bar.is-over { background: var(--ok); }
.dc-bar.is-under { background: var(--warn); }
.dc-bar.is-zero { background: var(--card-border); }
/* The per-bar percentage. Height is --dc-cap-h so the gridlines can subtract it
   - see .dc-plot. Tabular figures, so a column of them does not jitter. */
.dc-pct {
  height: var(--dc-cap-h); flex: 0 0 auto;
  font-size: .58rem; font-weight: 700; text-align: center; line-height: var(--dc-cap-h);
  font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden;
}
.dc-pct.is-over { color: var(--ok-ink); }
.dc-pct.is-under { color: var(--warn-ink); }
/* ⚠ EVERY COLUMN KEEPS ITS PERCENTAGE (the user's instruction, 2026-08-30) - it
   is NOT thinned with the day labels. The two are read differently: the labels
   are a scale, where every second one still tells you where you are, while the
   percentage IS the figure the chart exists to show and a column without one
   reads as a day with no target. They are legible down to ~26px per column,
   which a month of days holds at tablet width and up; below 768 the columns get
   too narrow for four characters, so the captions are dropped ALL AT ONCE rather
   than clipped to a stump. */
@media (max-width: 767px) {
  .dc-pct { display: none; }
  .dc-plot { --dc-cap-h: 0px; }     /* the grid must not reserve height for it */
}
/* The target sits ON the column as a dashed rule - a second bar beside it would
   double the width of a twelve-hour chart and halve the readable one. */
.dc-tgt { position: absolute; left: 0; right: 0; border-top: 2px dashed var(--danger); opacity: .55; }
.dc-lbl {
  font-size: .62rem; color: var(--faint); text-align: center;
  padding-top: .3rem; white-space: nowrap; overflow: hidden;
  /* ⚠ Height declared, not left to the text - .dc-plot's gridlines subtract
     exactly this to find the bars' baseline. Change one and change both. */
  height: var(--dc-lbl-h); flex: 0 0 auto;
}
/* Past ~16 columns the labels collide; show every second one. */
.dash-chart.dc-thin .dc-col:nth-child(even) .dc-lbl { visibility: hidden; }

.dash-legend { display: inline-flex; align-items: center; gap: .3rem; font-size: .7rem; color: var(--muted); }
.dash-key { width: 10px; height: 10px; border-radius: 2px; display: inline-block; margin-left: .5rem; }
.dash-key-over  { background: var(--ok); }          /* tracks .dc-bar.is-over  */
.dash-key-under { background: var(--warn); }        /* tracks .dc-bar.is-under */
.dash-key-tgt { background: transparent; border-top: 2px dashed var(--danger); height: 0; width: 12px; border-radius: 0; }

/* ── Dashboard card tables - .dash-tbl ────────────────────────────────────────
   Breakdown By Reason, Rejection By Type, Machines and Operator Achievement.

   ⚠ THESE WERE FOUR LISTS OF PER-ROW CSS GRIDS AND THE COLUMNS DID NOT LINE UP.
   Each `.rk-row` was its own grid, so an `auto` track sized against ITS OWN row:
   measured live, one label track came out 143px and the next 157px. A table
   shares one set of widths across every row by construction - which is why this
   is markup, not a CSS fix (2026-08-30, the user's instruction).

   ⚠ `table-layout: fixed` + <col> widths from dashTable(): the numeric columns
   hold their width whatever is in them, and the name column takes the slack and
   ellipsises rather than pushing the figures out of a 400px card.

   ⚠ TIGHTER PADDING THAN .table-app's OWN. These sit in a col-xl-4 card, where
   1rem either side of five columns is 160px of the ~376px available. */
.dash-tbl { padding: .1rem .9rem .8rem; }
.dash-tbl table { margin-bottom: 0; table-layout: fixed; width: 100%; }
/* ⚠ `thead th` / `tbody td`, NOT the bare `th, td`. Below 768 there is a
   `.table-app thead th { padding: .55rem .6rem }` rule (§5, the grid's own
   responsive block) at specificity (0,1,2); `.dash-tbl th` is only (0,1,1) and
   loses to it however late it is declared, so the tighter padding these narrow
   cards need silently stopped applying on exactly the widths that need it most.
   Matching its specificity lets source order decide, and this file is later. */
.dash-tbl thead th, .dash-tbl tbody td { padding: .34rem .4rem; }
/* ⚠ `.dash-tbl table thead th`, at (0,1,3), and NOT because it needs the extra
   element. `.table-app thead th` is (0,1,2) and sets `white-space: nowrap` in
   two places, one of them inside the ≤767 block; matching its specificity leaves
   the winner to source order, which is a coin-toss to rely on across two
   sections of one stylesheet - and it was losing, so "Produced Qty" was clipped
   at 375 while every other rule in this block applied.
   ⚠ HEADINGS WRAP; VALUES NEVER DO (see .dash-tbl .num). A two-word caption over
   a fixed-width numeric column has nowhere to go but a second line. */
.dash-tbl table thead th {
  font-size: .64rem; letter-spacing: .3px; text-transform: uppercase;
  color: var(--faint); font-weight: 700; white-space: normal;
}
.dash-tbl tbody td { font-size: .78rem; color: var(--body); }
.dash-tbl .num { text-align: right; font-variant-numeric: tabular-nums; }
/* ⚠ THE NOWRAP IS SCOPED TO tbody, AND THAT IS THE POINT. `.dash-tbl .num` also
   matches the HEADER cell of a numeric column - at (0,2,0) it outranked the
   heading rule above and pinned "Produced Qty" to one line inside an 84px
   column, where it was clipped. A figure must never break; its caption must be
   free to. */
.dash-tbl tbody .num { white-space: nowrap; font-weight: 700; color: var(--muted); font-size: .74rem; }
/* The name column: one line, ellipsised, with the full text on its title. */
.dash-tbl .nm span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The swatch / band-dot column - no padding to steal from the name beside it. */
.dash-tbl .key { padding-left: 0; padding-right: 0; }
/* Operator Achievement's Highest / Lowest divider, a row of its own so both
   halves share the table's columns. */
.dash-tbl tr.split td {
  font-size: .62rem; font-weight: 700; text-transform: uppercase; letter-spacing: .5px;
  color: var(--faint); background: var(--soft); padding-top: .3rem; padding-bottom: .3rem;
}

/* A rank row's name as a link into its own report. It inherits the row's colour
   - a blue underlined name in a dense list of ten reads as ten warnings. */
.rk-link { color: inherit; text-decoration: none; }
.rk-link:hover, .rk-link:focus { color: var(--accent); text-decoration: underline; }

/* The donut's legend key, and the machine band dot. Both live in their own
   narrow column so a long name can never ellipsise them away. */
.rk-key { display: block; width: 10px; height: 10px; border-radius: 3px; }
.mc-dot { display: block; width: 9px; height: 9px; border-radius: 50%; background: var(--card-border); }
.mc-dot.is-ok { background: var(--ok); }
.mc-dot.is-warn { background: var(--warn); }
.mc-dot.is-bad { background: var(--danger); }
.dash-tbl tbody .mc-oee { font-weight: 800; }
.mc-oee.is-ok { color: var(--ok); }
.mc-oee.is-warn { color: var(--warn-ink); }
.mc-oee.is-bad { color: var(--danger); }

/* ── Master Data Usage - master · active records · unused-since count ──────────
   One row per master, the whole row a link into that master (Unused Since
   oldest-first). The chevron column is always present so the counts line up
   whether or not a given row is a link. */
/* A block being re-fetched by its own control - Master Data Usage's day gap and
   the comparison card's date range. */
.dash-mu.is-busy, #compareBox.is-busy, #operatorBox.is-busy,
#outputBox.is-busy, #leadersBox.is-busy { opacity: .5; pointer-events: none; }
/* The card's own table already carries the padding - see .dash-tbl. */
.dash-mu .dash-tbl { padding-left: 1.25rem; padding-right: 1.25rem; }

/* ⚠ THE LINK IS PER CELL, NOT PER ROW - a <tr> cannot be an <a>. Each cell's
   anchor fills the cell, so the whole row is clickable and the row still behaves
   as a table row for alignment. */
.mu-cell { display: block; color: inherit; text-decoration: none; }
.dash-mu tbody tr:hover .mu-cell { color: var(--accent); }
.mu-name { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.dash-mu tbody td.num .mu-cell { font-weight: 700; }
.mu-unused { color: var(--muted); }
.mu-unused.is-on { color: var(--danger); }
.mu-unused.mu-none { color: var(--faint); }
.mu-go { color: var(--faint); font-size: .8rem; }
.dash-mu tbody tr:hover .mu-go { color: var(--accent); }
/* The small dropdowns that live in a dashboard card header - Master Data Usage's
   day gap and the machine card's OEE threshold. */
.dash-sel { width: auto; }

/* ── KPI cards - .dash-kpis / .dk-* (2026-09-27) ─────────────────────────────
   ⚠ NOT A REBUILD OF THE DELETED .kpi-* STRIP (§3's tombstone), and the
   tombstone's test was applied: these five are fixed on the RUNNING MONTH while
   the comparison card opens on the last 30 days, and each label names its
   window in words - so no figure here is the same figure as a row below it.

   COLOUR, NOT ICONS (the user's call, same day): each card carries its own
   colour as a light wash, a 4px bar down the left edge and its label in that
   colour's dark ink. The icon is gone at every width, so the whole card is text.
   ⚠ STILL A WASH, NEVER THE OLD WHITE-ON-GRADIENT TILE - white text on those
   failed even the 3:1 floor. The washes are paler than the --*-bg tokens on
   purpose: the sub-line is small text and needs 4.5:1 against the card.
   ⚠ THE BAR IS A border-left, SO IT IS PART OF THE BOX - no pseudo-element to
   escape the radius, and hover changes the other three sides only.

   Five across from 1200, 3 + 2 on a tablet (a six-track grid: the first three
   take two tracks each, the last two take three), 2 + 2 + 1 on a phone. */
.dash-kpis { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: .75rem; }
.dk-card {
  --kc: var(--accent); --kc-wash: var(--accent-wash); --kc-ink: var(--ink-700); --kc-line: var(--card-border);
  display: block; min-width: 0;
  background: linear-gradient(135deg, var(--kc-wash) 0%, var(--kc-wash) 55%, #fff 100%);
  border: 1px solid var(--kc-line); border-left: 4px solid var(--kc);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-card); padding: .8rem .95rem;
  color: inherit; text-decoration: none;
}
.dk-card.kc-a { --kc: #7c3aed; --kc-wash: #f5f3ff; --kc-ink: #5b21b6; --kc-line: #ddd6fe; }
.dk-card.kc-b { --kc: #0284c7; --kc-wash: #f0f9ff; --kc-ink: #0369a1; --kc-line: #bae6fd; }
.dk-card.kc-c { --kc: #ea580c; --kc-wash: #fff7ed; --kc-ink: #c2410c; --kc-line: #fed7aa; }
.dk-card.kc-d { --kc: #16a34a; --kc-wash: #f0fdf4; --kc-ink: #15803d; --kc-line: #bbf7d0; }
.dk-card.kc-e { --kc: #dc2626; --kc-wash: #fef2f2; --kc-ink: #b91c1c; --kc-line: #fecaca; }
a.dk-card:hover { border-top-color: var(--kc); border-right-color: var(--kc); border-bottom-color: var(--kc); color: inherit; }
.dk-body { min-width: 0; }
.dk-lbl { font-size: .72rem; font-weight: 700; color: var(--kc-ink); line-height: 1.25; }
.dk-val {
  font-size: 1.3rem; font-weight: 700; color: var(--ink-900); line-height: 1.3;
  white-space: nowrap; font-variant-numeric: tabular-nums;
}
.dk-val.is-ok   { color: var(--ok-ink); }
.dk-val.is-warn { color: var(--warn-ink); }
.dk-val.is-bad  { color: var(--danger-ink); }
.dk-u { font-size: .62em; font-weight: 600; color: #475569; }
.dk-sub { font-size: .7rem; color: #475569; line-height: 1.3; }   /* wraps - never cut a figure */
@media (max-width: 1199.98px) {
  .dash-kpis { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .dk-card { grid-column: span 2; }
  .dk-card:nth-child(n+4) { grid-column: span 3; }
}
@media (max-width: 575.98px) {
  .dash-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .6rem; }
  .dk-card, .dk-card:nth-child(n+4) { grid-column: auto; padding: .65rem .75rem; }
  .dk-card:last-child { grid-column: 1 / -1; }
  .dk-val { font-size: 1.1rem; }
}

/* ── Top Customers & Breakdown Machines - .ld-* ──────────────────────────────
   Two lists in one card, each under its own small heading. The fourth fact per
   row (a customer's share of plant output, a machine's top reason) is a second
   line under the name, not a column - see leadersHtml(). */
.ld-sec + .ld-sec { border-top: 1px solid var(--card-border); }
.ld-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: .5rem;
  flex-wrap: wrap; padding: .7rem 1.25rem .1rem;
}
.ld-t { font-size: .78rem; font-weight: 700; color: var(--ink-900); }
.ld-s { font-size: .7rem; color: var(--muted); }
.ld-sec .dash-tbl { padding-left: 1.25rem; padding-right: 1.25rem; }
.ld-sec .empty-state { padding: 1.25rem 1rem; }
.dash-tbl .nm .ld-note { font-size: .68rem; font-weight: 400; color: var(--muted); line-height: 1.3; }
@media (max-width: 575.98px) {
  .ld-head, .ld-sec .dash-tbl { padding-left: .75rem; padding-right: .75rem; }
}

/* ── Period comparison ─────────────────────────────────────────────────────── */
.dash-cmp .table-app tbody td { padding-top: .28rem; padding-bottom: .28rem; }
.cmp-n, .cmp-c { text-align: right; }
.cmp-n { font-variant-numeric: tabular-nums; }
.cmp-prev { color: var(--muted); }
.cmp-u { font-size: .72em; color: var(--faint); }
/* ⚠ The change column is TONED, and the tone comes from the SERVER:
   dashboard_delta() knows that breakdown falling is good news, so a down arrow
   here is green on the rows where down is the right direction. A metric with no
   good direction - a target, an entry count - is left grey deliberately. */
.cmp-chg { font-size: .74rem; font-weight: 700; white-space: nowrap; color: var(--muted); }
.dash-note {
  display: flex; align-items: flex-start; gap: .4rem;
  margin: .75rem 1.25rem 0; padding: .45rem .6rem;
  background: var(--warn-bg); color: var(--warn-ink);
  border-radius: var(--radius-sm); font-size: .7rem; line-height: 1.4;
}
.cmp-chg.is-ok  { color: var(--ok); }
.cmp-chg.is-bad { color: var(--danger); }

/* ── Breakdown donut ───────────────────────────────────────────────────────── */
.bk-top {
  display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
  padding: 1rem 1.25rem .25rem;
}
.dash-donut { position: relative; width: 132px; flex: 0 0 auto; }
.dash-donut svg { width: 132px; height: 132px; display: block; }
/* The centre is a positioned overlay, not SVG <text>: the viewBox scales with
   the ring, and scaled text is exactly the illegibility the bar chart above is
   built from HTML boxes to avoid. */
.dn-mid {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; text-align: center;
  pointer-events: none;
}
.dn-val { font-size: 1.3rem; font-weight: 800; color: var(--ink-900); line-height: 1.1; }
.dn-cap {
  font-size: .58rem; font-weight: 700; letter-spacing: .4px;
  text-transform: uppercase; color: var(--faint);
}
.bk-facts { flex: 1 1 150px; min-width: 0; display: grid; gap: .5rem; }
.bk-fact { background: var(--page-bg); border-radius: var(--radius-sm); padding: .45rem .6rem; }
.bk-fact b { display: block; font-size: 1rem; font-weight: 800; color: var(--ink-900); line-height: 1.2; }
.bk-fact span {
  font-size: .62rem; color: var(--muted);
  text-transform: uppercase; letter-spacing: .4px;
}
/* ── Breakdown trend chart ─────────────────────────────────────────────────
   ⚠ THE POINTS ARE HTML AND THE LINES ARE SVG, and that split is deliberate.
   The plot has to stretch horizontally to fill its column, and an SVG stretched
   with preserveAspectRatio="none" turns a circle into an oval and text into a
   smear - which is why the Output chart above is built from HTML boxes. Here the
   polylines DO live in the stretched SVG, because a stretched line is still the
   right line; `vector-effect: non-scaling-stroke` is what keeps the stroke 2px
   at any width. Every dot, label and tick is a positioned HTML element instead
   and cannot distort at all. */
.bk-split { margin: 0; }
.bk-left  { border-right: 1px solid var(--card-border); }

.bk-chart { padding: 1rem 1.25rem .25rem; }
/* The shared .dash-note, pulled back inside the chart's own padding - it belongs
   to this chart rather than to the card, and its default margin is written for a
   note sitting under a full-width block. */
.bk-note { margin: 0 0 .6rem; }
.bk-cap {
  font-size: .68rem; color: var(--muted); line-height: 1.45; margin-bottom: .6rem;
}
.bk-hidden {
  display: inline-block; background: var(--accent-wash); color: var(--accent);
  border-radius: 999px; padding: .05rem .45rem; font-weight: 700; white-space: nowrap;
}

/* ⚠ ONE HEIGHT, THREE USERS - the y-axis column, the plot and the gridlines all
   size from it, so the ticks can never sit a few pixels off the lines they
   label. Change it and all three move together. */
.bk-chart { --bk-h: 230px; }

.bk-plotwrap { display: flex; gap: .5rem; }

/* The y captions sit OUTSIDE the plot, so the plot's 0-100% coordinate space is
   the data's alone - the same bargain --dc-lbl-h strikes on the bar chart. */
.bk-yaxis {
  position: relative; width: 34px; flex: 0 0 34px; height: var(--bk-h);
  /* ⚠ THE SAME 1px THE PLOT SPENDS ON ITS AXIS RULE, so the two boxes have the
     same INNER height and a tick anchored at 0% lands where the plot's 0% is.
     Without it every tick sat 1px below its gridline - measured, and invisible
     to any check that only asserts the two boxes are the same height, which they
     already were. */
  border-bottom: 1px solid transparent;
}
.bk-ytick {
  position: absolute; right: 0; transform: translateY(50%);
  font-size: .6rem; color: var(--faint); font-weight: 700; white-space: nowrap;
}

.bk-plot {
  position: relative; flex: 1 1 auto; min-width: 0; height: var(--bk-h);
  border-left: 1px solid var(--card-border); border-bottom: 1px solid var(--card-border);
}
.bk-grid {
  position: absolute; left: 0; right: 0; height: 0;
  /* ⚠ border-BOTTOM, NOT border-top. On a height:0 element anchored by `bottom`,
     a top border is drawn one pixel ABOVE the anchor and a bottom border is
     drawn exactly ON it - so with border-top every gridline sat 1px off the
     value it represents, all the way up the chart. */
  border-bottom: 1px dashed var(--line); pointer-events: none;
}
.bk-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }

.bk-dot {
  position: absolute; width: 7px; height: 7px; border-radius: 50%;
  transform: translate(-50%, 50%); border: 1.5px solid #fff; cursor: help;
}
.bk-pt {
  position: absolute; transform: translate(-50%, 50%);
  margin-bottom: 11px; font-size: .58rem; font-weight: 700;
  color: var(--ink-900); background: rgba(255, 255, 255, .82);
  border-radius: 3px; padding: 0 2px; white-space: nowrap; pointer-events: none;
}

.bk-xaxis { position: relative; height: 16px; margin-left: 34px; }

/* A month-on-month report chart that shows EVERY value (rgTrendHtml's minPx,
   2026-09-17): below the width that fits them it scrolls sideways inside this
   box, never the page (R9). -x only, so a vertical swipe still scrolls the page -
   the .table-wrap rule. */
/* ⚠ overflow-y: hidden IS LOAD-BEARING: overflow-x: auto alone turns the other
   axis to auto too, and the top y tick (centred on the plot's top edge) then
   raises a vertical scrollbar beside the chart. The padding is what keeps that
   half-tick inside the box instead of clipping it. */
.rg-trend-scroll { overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; padding: .5rem 0 .25rem; }
.bk-xlab {
  position: absolute; transform: translateX(-50%); top: 3px;
  font-size: .6rem; color: var(--faint); white-space: nowrap;
}

/* A row that switches a line on and off. The dim is the whole statement - the
   swatch, the name and the figures all fade together, so a hidden reason reads
   as hidden rather than as a row that failed to load. */
.rk-tog { cursor: pointer; }
/* A reason the chart does not draw. It keeps its row, its colour and its share -
   it is still in the total everything else is measured against - so the only
   thing it gives up is the pointer, because there is no line to toggle. */
.rk-noline { cursor: help; }
.rk-tog:hover td { background: var(--accent-wash); }
.rk-tog.is-off td { opacity: .38; }
.rk-tog:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* ── Responsive ───────────────────────────────────────────────────────────── */
@media (max-width: 1199px) {
  /* Below xl the two halves stack, so the divider between them stops being a
     vertical rule and becomes the horizontal one that separates stacked blocks -
     the same rule .rv-pair follows on the production review. */
  .bk-left { border-right: 0; border-bottom: 1px solid var(--card-border); padding-bottom: .5rem; }
  .bk-chart { padding-top: .35rem; }
}
@media (max-width: 991px) {
  .dc-plot { height: 150px; }
  .bk-chart { --bk-h: 190px; }
}
@media (max-width: 767px) {
  .dash-chart, .dash-tbl, .dash-mu .dash-tbl { padding-left: .9rem; padding-right: .9rem; }
  .bk-top { padding-left: .9rem; padding-right: .9rem; }
  .bk-chart { padding-left: .9rem; padding-right: .9rem; --bk-h: 165px; }
  /* The y captions give up their column - at this width every pixel of plot is
     worth more than a second decimal on a gridline. */
  .bk-yaxis { width: 26px; flex-basis: 26px; }
  .bk-xaxis { margin-left: 26px; }
  .bk-ytick, .bk-xlab { font-size: .55rem; }
  .dc-plot { height: 132px; gap: 2px; }
  /* ⚠ THE COMPARISON CARD'S HEADER NOW HOLDS A DATE RANGE PICKER, and at 375 the
     picker + caption + report pill come to 484px in a 328px box. Left nowrap it
     spilled 17px past the card and the PAGE scrolled sideways (R9). It wraps to
     two lines instead - the card is taller, nothing is cut off, and the body
     still never pans. */
  .dash-acts { flex-wrap: wrap; }
}
@media (max-width: 575px) {
  .dash-bar { gap: .45rem; }
  .dash-range-note { width: 100%; order: 3; }
  .dc-plot { height: 116px; }
  .dash-tbl tbody td, .mu-name { font-size: .74rem; }
  .dash-tbl thead th, .dash-tbl tbody td { padding-left: .3rem; padding-right: .3rem; }
}
@media (max-width: 414px) {
  .dash-chart, .dash-tbl, .dash-mu .dash-tbl { padding-left: .75rem; padding-right: .75rem; }
  .bk-top { padding-left: .75rem; padding-right: .75rem; }
  .dc-plot { height: 104px; }
}

/* ==========================================================================
   15d. Calculator - .calc-*
   ==========================================================================
   ⚠ THE MASTER DATA SCREEN CARRIES A COUSIN OF THIS RAIL - section 15f,
   `.mnav-*`, added 2026-08-31. It is a SEPARATE COPY, chosen over sharing one
   block, so the Bootstrap overrides, the sticky/max-height arrangement and the
   active row's language exist twice. A FIX FOUND HERE IS WORTH CARRYING THERE
   BY HAND; nothing will tell you.

   A shop-floor utility, and it is judged as one: it has to be usable with one
   thumb, standing at a machine, on a 375px phone that may be running as an
   installed app with no browser chrome at all.

   Four decisions follow from that and should not be undone:

     • THE ANSWER IS A BOX INSIDE THE FIELD GRID (.fl-result), never a card
       below it. It sits immediately after the last input, so on a phone it is
       on screen while the fields are being typed. A separate result section put
       it under a 190px drawing and every edit became scroll-down-scroll-up.
       This replaced a sticky-to-the-bottom result card, which solved the same
       problem by covering the form.

     • FIELDS ARE TWO PER ROW AT EVERY WIDTH. A dimension is five or six digits
       (see the suffix rule below, which is what makes them fit at 375px).

     • THE TAB STRIP SCROLLS SIDEWAYS AND THE PAGE NEVER DOES (R9). Five tabs do
       not fit across 375px, and wrapping them to two rows spends 90px of a 640px
       viewport on navigation.

     • Every control is at least 42px tall. These are gloved hands.

   No new colours: the result box is the accent wash and the accent border that
   .section-card and the KPI tiles already use.                                */

/* ── The menu: a VERTICAL rail that becomes a drawer ──────────────────────────
   Replaced the horizontal pill strip on 2026-08-17. The strip was right for
   five calculators and wrong for seventeen: a horizontal scroller puts most of
   the catalogue behind a gesture with no affordance, and the pills either shrink
   past legibility or run off the edge.

   ⚠ ONE LIST, TWO BEHAVIOURS. The markup is a Bootstrap `.offcanvas-xl`: a
   static block from 1200px, an off-canvas drawer below. There is no second copy
   for mobile - see the warning in the view, calcTabKeys() reads the whole
   document and duplicates would break tab switching outright.

   ⚠ THE BREAKPOINT IS 1200, NOT 992, AND THAT IS FORCED. .calc-split already
   claims 992px to put the drawing beside the form; a rail there would make three
   columns inside 990px and leave the form under 480px - narrower than the two
   fields per row this screen is built on. At 1200 the form still gets ~690px. */
.calc-shell { display: block; }
@media (min-width: 1200px) {
  /* ⚠ 264px, AND EVERY ONE OF THOSE PIXELS WAS ARGUED FOR.
     216 → 248 on 2026-08-22, 248 → 264 on 2026-08-24 when the list gained a
     permanently reserved scrollbar gutter (see .calc-nav-list below). The
     original note stands: The rename made the names sentences rather
     than words - "Find A Slip Gauge For Sine Table" measures 192px in the row
     font, against the 165px of text the 216px rail left after its padding and
     the favourite star. Three of the five ellipsised, so the rail no longer
     told you which calculator you were on, which is the one job it has.

     The three alternatives all cost more than 32px does: wrapping to two lines
     breaks the rhythm of the list (and .calc-tab-btn span says so), a smaller
     font is the wrong trade on a shop-floor screen, and shortening the names
     again is the change the user just asked for, backwards. Measured after: the
     form still lays out beside its drawing at 1200. */
  .calc-shell {
    display: grid; align-items: start; gap: 1.25rem;
    grid-template-columns: 264px minmax(0, 1fr);
  }
  /* ⚠ THE RAIL STICKS AND SCROLLS ITSELF (2026-08-24, the user's instruction:
     "give calculator menu scrollable not entire page unless calculator page is
     bigger than screen").

     It used to follow the page, on the stated ground that two scrollbars fight
     for one wheel. That reasoning held at five rows and stopped holding at
     seventeen: the rail is ~700px tall, so on a 768px-high screen it - not the
     calculator - was what made the page scroll, and reaching the bottom of the
     menu scrolled the panel you were reading off the top. The two scrollbars
     never actually compete, because the rail only takes the wheel while the
     pointer is over the rail.

     THE RESULT IS THE RULE THE USER ASKED FOR: the page scrolls when, and only
     when, the CALCULATOR is taller than the screen. Most of the seventeen are
     not, so most of them now sit still.

     ⚠ --calc-rail-top IS THE STICKY TOP, AND IT IS THE TOPBAR'S HEIGHT PLUS THE
     PAGE PADDING. .topbar is `position: sticky; top: 0`, so a rail stuck at 0
     slides UNDER it. Measured against the shipped shell: 65px of topbar at
     1200px and up, plus .page-body's 1rem at that width. ⚠ BOTH HALVES MATTER: the
     rail's resting top there IS 81px, and a sticky top any larger makes the card
     jump down by the difference the moment the page scrolls. max-height leaves
     the same 1rem at the bottom, so the card never runs into the viewport edge. */
  .calc-nav {
    position: sticky; top: var(--calc-rail-top);
    max-height: calc(100vh - var(--calc-rail-top) - 1rem);
    /* ⚠ THE CARD ITSELF HAS TO BE A COLUMN FLEX BOX, and this is the third
       Bootstrap default that had to be undone here. Above the breakpoint it
       turns the responsive offcanvas into a plain `display: block`, and a block
       parent does NOT hand its max-height down: .offcanvas-body sized to its
       content (710px) inside a 503px card and simply overflowed, clipped by the
       card's own `overflow: hidden`. Nothing scrolled, and the bottom four rows
       of the menu were unreachable. Flex is what makes the cap propagate. */
    display: flex !important; flex-direction: column;
  }
  /* ⚠ TWO BOOTSTRAP RULES HAVE TO BE UNDONE HERE, AND BOTH BIT.
     Above the breakpoint Bootstrap turns a responsive offcanvas into a plain
     block, and in doing so it sets:

       .offcanvas-xl              { background-color: transparent !important }
       .offcanvas-xl .offcanvas-body { display: flex }

     The first is why the rail had no white card behind it - and it carries
     !important, so ours has to as well; there is no way to out-specify it.
     The second is worse and less obvious: it makes .calc-nav-body a ROW flex
     container, so the list inside became a flex ITEM and shrank to its content.
     That is why the active row's tint stopped two thirds of the way across
     instead of filling the menu. Restoring `display: block` is the whole fix. */
  .calc-nav {
    background: var(--card-bg) !important;
    border: 1px solid var(--card-border);
    border-radius: var(--radius); box-shadow: var(--shadow-card);
    overflow: hidden;                 /* so a row's tint stops at the radius */
  }
  /* No padding around the list: rows run edge to edge, which is what lets the
     active row's left bar sit on the card's own edge. The search box gets its
     own inset instead. */
  /* ⚠ THE SELECTOR HAS TO OUT-SPECIFY BOOTSTRAP'S, NOT JUST FOLLOW IT.
     Theirs is `.offcanvas-xl .offcanvas-body` (0,2,0). A bare `.calc-nav-body`
     is (0,1,0) and loses however late it is loaded - which is exactly what
     happened: the menu kept Bootstrap's `display:flex`, the list stayed a row
     flex item, and every row shrank to its text instead of filling the menu. */
  /* 10px top and bottom, ZERO left and right - the user's own value, found by
     switching Bootstrap's display:flex off in DevTools. The vertical padding
     lifts the first and last rows off the card's rounded corners; horizontal
     padding is what would stop a row's tint reaching the edge, so there is
     none, and the search box carries its own inset instead. */
  /* ⚠ A COLUMN FLEX BOX NOW, NOT display:block - and the width guarantee the
     block fix bought is unchanged. Bootstrap's own `display: flex` made this a
     ROW container, which is what shrank every row to its text; a COLUMN
     container stretches its children across, same as block did. It is a flex
     box because the search box has to stay put while the list under it scrolls,
     which needs the list to be the only growing child.

     ⚠ min-height: 0 IS LOAD-BEARING ON BOTH. A flex item's default min-height
     is `auto`, so without it the body refuses to shrink below its content and
     the max-height above does nothing at all - the rail grows and the page
     scrolls again, which is the bug this whole block exists to fix. */
  .calc-nav.offcanvas-xl .offcanvas-body {
    display: flex; flex-direction: column; align-items: stretch;
    padding: 10px 0; min-height: 0; overflow: hidden;
  }
  .calc-nav.offcanvas-xl .offcanvas-body .calc-nav-search { flex: 0 0 auto; margin: 0 .5rem .5rem; }
  /* THE LIST is what scrolls. `overscroll-behavior: contain` stops a flick that
     reaches the end of the menu from carrying on into the page - on a trackpad
     that reads as the whole screen jumping. */
  .calc-nav.offcanvas-xl .offcanvas-body .calc-nav-list {
    flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
    /* ⚠ scrollbar-gutter: stable IS WHAT KEEPS THIS MEASURABLE. Without it the
       bar appears only on short viewports, so a row is 197px of text at 768px
       high and 182px at 600px - and four of the seventeen names ellipsised on
       the second one only. A width bug that shows on some screens and not
       others is one nobody can reproduce. The gutter is always reserved, so one
       measurement covers every viewport height, and the rail was widened
       248 → 264px to pay for it. */
    scrollbar-gutter: stable;
  }
}
/* Below 1200 Bootstrap owns the offcanvas geometry; only the width is ours. */
@media (max-width: 1199.98px) {
  .calc-nav { max-width: 300px; width: 86vw; }
  /* No horizontal padding here either, for the same reason as the rail: a row's
     tint and its active bar have to reach the drawer's edge, or the selected
     calculator reads as a floating chip rather than the chosen row. */
  /* ⚠ THE DRAWER SCROLLS INSIDE ITSELF TOO, and at seventeen rows it has to:
     the list is ~700px against a 667px phone. Bootstrap's .offcanvas-body is
     already `overflow-y: auto`, but that is inherited by luck - stating it here
     means a future padding or display change cannot take it away silently.
     Same column-flex shape as the rail, so the search box stays put while the
     list moves under it, and `overscroll-behavior` stops a flick at the end of
     the list from scrolling the page behind the drawer. */
  .calc-nav-body {
    display: flex; flex-direction: column; align-items: stretch;
    padding: .25rem 0 1rem; min-height: 0; overflow: hidden;
  }
  .calc-nav-body .calc-nav-search { flex: 0 0 auto; margin: .25rem .75rem .5rem; }
  .calc-nav-body .calc-nav-list {
    flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  }
}
.calc-content { min-width: 0; }   /* a grid child must be told it may shrink */

/* The trigger names where you are, not just what it opens - "Calculators" alone
   would make the current calculator unreadable once the drawer is shut. */
.calc-nav-trigger {
  display: flex; align-items: center; gap: .55rem; width: 100%;
  min-height: 44px; padding: .5rem .85rem; margin-bottom: 1rem;
  background: var(--card-bg); border: 1px solid var(--card-border);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-card);
  color: var(--ink-600); font-size: .85rem; font-weight: 600; cursor: pointer;
}
.calc-nav-trigger:hover { border-color: var(--accent-soft); background: var(--accent-wash); }
.calc-nav-trigger i { font-size: 1rem; opacity: .8; }
.calc-nav-trigger-lbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* No gap: the rows are a continuous list separated by a hairline, not a stack of
   floating chips. Groups were built and removed on 2026-08-17 ("make it easy"),
   so the list is flat - the filter is what keeps it findable past eight. */
.calc-nav-list { display: flex; flex-direction: column; }

.calc-nav-search { position: relative; margin-bottom: .5rem; }
.calc-nav-search i {
  position: absolute; left: .6rem; top: 50%; transform: translateY(-50%);
  font-size: .8rem; color: var(--faint); pointer-events: none;
}
.calc-nav-search input {
  width: 100%; padding: .45rem .6rem .45rem 1.9rem;
  border: 1px solid var(--field-border); border-radius: var(--radius-field);
  font-size: .82rem; outline: none; background: #fff;
}
.calc-nav-search input:focus { border-color: var(--accent); }
/* The UA's own clear affordance, so there is not a second one beside it. */
.calc-nav-search input::-webkit-search-cancel-button { cursor: pointer; }

/* A narrow scrollbar on the menu list, both engines. Every pixel it does not
   take is a pixel of calculator name - and this list is scrolled with a thumb
   or a wheel, never dragged by its bar. */
.calc-nav-list { scrollbar-width: thin; scrollbar-color: var(--field-border) transparent; }
.calc-nav-list::-webkit-scrollbar { width: 6px; }
.calc-nav-list::-webkit-scrollbar-track { background: transparent; }
.calc-nav-list::-webkit-scrollbar-thumb { background: var(--field-border); border-radius: 3px; }
.calc-nav-list::-webkit-scrollbar-thumb:hover { background: var(--muted); }

.calc-nav-empty {
  margin: .5rem 0 0; padding: .75rem .6rem; text-align: center;
  font-size: .76rem; color: var(--faint);
}
.calc-nav-empty[hidden] { display: none; }   /* author display beats the UA rule */

/* ── One row in the menu ──────────────────────────────────────────────────────
   The row is the WRAPPER; the calculator and its star are two buttons inside it
   (a button inside a button is invalid markup - see the note in the view).

   ⚠ IT BORROWS THE SIDEBAR'S ACTIVE LANGUAGE, ON PURPOSE: a tinted background
   plus a 3px accent bar down the left edge, exactly like .nav-item.active. The
   first cut gave every row its own border and radius, which put bordered pills
   inside a bordered card - two nested boxes per row, and at seventeen rows that
   reads as clutter rather than a list. A list needs one edge to run the eye
   down, not seventeen.

   Left-aligned and full width now that it is vertical: centred labels have no
   common left edge to scan. */
.calc-tab {
  display: flex; align-items: stretch; align-self: stretch; width: 100%;
  min-height: 38px;
  border: none; border-left: 3px solid transparent; background: transparent;
  color: var(--muted); transition: background .15s, color .15s, border-color .15s;
}
.calc-tab + .calc-tab { border-top: 1px solid var(--line); }
.calc-tab-btn {
  flex: 1 1 auto; min-width: 0;
  display: inline-flex; align-items: center;
  /* .8rem of lead-in, not .55: with the icon gone the label would otherwise sit
     almost on the active row's left bar. */
  padding: .45rem .2rem .45rem .8rem; border: none; background: transparent;
  cursor: pointer; text-align: left;
  font-size: .82rem; font-weight: 600; color: inherit;
}
/* The label may ellipsis rather than wrap - a two-line row breaks the rhythm of
   the list. There is no icon: they were removed on the user's instruction
   (2026-08-17), and with seventeen shop-floor calculators an icon per row is
   seventeen glyphs that all mean "calculator" and none of which distinguish. */
.calc-tab-btn span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.calc-tab:hover { background: var(--accent-wash); color: var(--ink-600); }
.calc-tab.active {
  background: var(--accent-tint); border-left-color: var(--accent); color: var(--violet-ink);
}
.calc-tab[hidden] { display: none; }        /* the filter hides rows outright */

/* One favourite per user - it decides which tab this screen opens on. Amber
   when set, and never coloured on hover alone: a star that lights up under the
   pointer reads as already chosen. */
/* Every row shows its star, on the user's instruction (2026-08-17): a hollow
   outline when it is not the favourite, solid amber when it is. Hiding them
   until hover was tried the same day and rejected - the star is then invisible
   on a touch screen, which is where this screen is used, and a control you
   cannot see is a control nobody knows exists. */
.calc-fav {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 30px; padding: 0; border: none; background: transparent;
  color: var(--faint); font-size: .78rem; cursor: pointer; line-height: 1;
  transition: color .15s;
}
.calc-fav:hover { color: var(--warn); }
.calc-fav[aria-pressed="true"] { color: #f59e0b; }

.calc-panel[hidden] { display: none; }

/* ── Two fields per row on a phone ────────────────────────────────────────────
   The view uses col-6 at every width (the user's instruction, 2026-08-15): a
   dimension is five or six digits, so a full-width box on a 375px screen is
   nine tenths white space and doubles the scrolling to reach the answer.

   ⚠ THIS RULE IS WHAT MAKES THAT FIT, and it is not cosmetic. The shared
   .fl-field rule reserves 68px of right padding for the unit - correct for
   "Qty./Hr." on the production form, fatal in a 155px box, where it leaves 67px
   for the number. Here the unit is "mm", "kg" or one symbol, so 34px is ample
   and the digits get the rest. Scoped to .calc-panel so the settings drawer's
   "g/cm³" keeps the full reservation.                                          */
.calc-panel .fl-suffix { right: 10px; font-size: .68rem; }
.calc-panel .fl-field:has(.fl-suffix) input { padding-right: 34px; }

/* ── The × inside a field ─────────────────────────────────────────────────────
   Shown only while the box holds something, so an untouched form is not a row
   of crosses, and the extra right padding is only reserved then - an empty
   field keeps every pixel for the digits about to be typed into it.           */
.fl-clear {
  position: absolute; right: 32px; top: 50%; transform: translateY(-50%);
  display: none; align-items: center; justify-content: center;
  width: 22px; height: 22px; padding: 0;
  border: none; border-radius: 50%; background: #eef2ff; color: var(--muted);
  font-size: .6rem; line-height: 1; cursor: pointer;
}
.fl-clear:hover { background: var(--accent-tint); color: var(--violet-ink); }
.fl-field.has-value .fl-clear { display: inline-flex; }
/* A field the mode has turned into an output (.fl-ro on the Sin panel) cannot
   be cleared - the value is not the operator's to remove, and the × would be an
   affordance for nothing. */
.fl-field.fl-ro .fl-clear { display: none; }
.calc-panel .fl-field.has-value:has(.fl-clear) input { padding-right: 60px; }

/* ── The answer, as the last box of the grid ──────────────────────────────────
   Same 44px box as the fields beside it so the row does not go ragged, but
   filled with the figure at 1.5rem in the brand ink - the one thing on this
   screen that is read from across a bench. .fl-label rather than a <label>:
   there is no control to be labelled, and a real <label> would move focus to
   nothing when tapped.                                                        */
.fl-field.fl-result {
  background: var(--accent-wash); border-color: var(--accent-soft);
  display: flex; align-items: center;
}
.fl-field.fl-result .fl-label {
  position: absolute; left: 12px; top: 0; transform: translateY(-50%);
  padding: 0 4px; background: var(--accent-wash);
  font-size: .72rem; font-weight: 600; color: var(--ink-600);
}
/* ⚠ THE SAME BOX AS THE FIELDS BESIDE IT - the user's instruction, 2026-08-15:
   "as per normal text box, just keep colour difference". A .fl-field input is
   6px of padding around a 1.4 line-height at .9rem, so the figure is sized to
   land on the same outside height; only the tint, the weight and the ink say
   this one is the answer. Do NOT give it a min-height or a larger line-height:
   a result box one row taller than its neighbours makes the whole grid ragged,
   which is what the first cut did.                                            */
.calc-out-val {
  flex: 1 1 auto; min-width: 0; padding: 6px 34px 6px 10px;
  font-size: 1.15rem; font-weight: 800; line-height: 1.1;
  color: var(--accent-dark); font-variant-numeric: tabular-nums;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fl-field.fl-result.is-empty .calc-out-val { color: var(--faint); }
/* A word that belongs to a figure - the piece count on the Blocks tab, which
   rides on the stack's heading. */
.calc-badge {
  flex-shrink: 0;
  font-size: .68rem; font-weight: 600; color: var(--ink-600); white-space: nowrap;
}
.fl-field.fl-result .fl-copy { display: inline-flex; right: 8px; background: transparent; font-size: .72rem; }
.fl-field.fl-result .fl-copy:hover { background: var(--accent-tint); }

/* ── Cross-section drawing ───────────────────────────────────────────────────
   ⚠ CAPPED IN WIDTH, NOT IN HEIGHT. These are dimensioned drawings with a
   viewBox, so preserveAspectRatio letterboxes them inside whatever box they are
   given: a max-height on a 1366px screen leaves the drawing small and marooned
   in white space, while removing the cap entirely grows it to 450px and pushes
   the answer off the fold. A max-width does neither - the figure is the same
   size at every width above 420px, and shrinks to fit below it.               */
.calc-fig {
  margin: 1rem 0 0; padding: .75rem;
  background: #f8fafc; border: 1px solid var(--card-border);
  border-radius: var(--radius-sm);
}
.calc-fig svg { display: block; width: 100%; max-width: 420px; height: auto; margin: 0 auto; }
/* The client's own drawing (2026-09-11). Same box as the SVG it replaces.
   ⚠ mix-blend-mode: multiply is what lets a white-backed PNG sit on the card's
   #f8fafc without a white rectangle showing - white multiplies to the card and
   the black linework stays black. It is safe only because this app is light
   mode only (R7); on a dark card it would black the drawing out. */
.calc-fig img { display: block; width: 100%; max-width: 420px; height: auto; margin: 0 auto; mix-blend-mode: multiply; }

/* ── Form beside the drawing ──────────────────────────────────────────────────
   Stacked by default - which is the phone, and the order the app it replaces
   uses: fields, answer, then the drawing under them. From 992px the two sit
   side by side, the drawing in a fixed column so it stops growing while the
   form takes whatever is left. align-items:start keeps the drawing at the top
   of its column instead of centring it against a taller form.                 */
/* ── A standing instruction above the field grid ─────────────────────────────
   How a dimension is measured, on the four panels where that changes what the
   answer means (2026-09-13). ⚠ NOT a .fl-hint: that is .7rem --faint, which is
   right for a line somebody may never need to read and wrong for one they must
   read before typing. --body on --accent-wash measures 13.25:1. ⚠ And not the
   grey working line under the grid either - that one is emptied by the bundle
   as soon as there is an answer to print.                                     */
.calc-note {
  margin: 0 0 .85rem; padding: .45rem .6rem;
  background: var(--accent-wash); border-left: 3px solid var(--accent-soft);
  border-radius: var(--radius-sm);
  font-size: .78rem; line-height: 1.35; color: var(--body);
}
.calc-split > .calc-form { min-width: 0; }
@media (min-width: 992px) {
  .calc-split {
    display: grid; align-items: start; gap: 1.25rem;
    grid-template-columns: minmax(0, 1fr) minmax(260px, 400px);
  }
  .calc-split > .calc-fig { margin-top: 0; }
}

/* ── Segmented control - sin mode, block set ─────────────────────────────── */
.calc-seg-lbl { display: block; font-size: .72rem; font-weight: 600; color: var(--muted); margin-bottom: .3rem; }

/* ⚠ CAPPED IN WIDTH. Left to stretch, two buttons reading "Calculate Height"
   and "Calculate Angle" become 500px each on a 1366px screen - a pair of slabs
   that look like the page's primary action rather than a mode switch. Full
   width is still right below 576, where a thumb needs the target.            */
/* ⚠ 460px, AND THE BUTTONS WRAP (2026-08-22). This was 380px with
   `white-space: nowrap` on the buttons, which is a clip waiting to happen:
   `overflow: hidden` here plus nowrap there means a caption longer than its
   share is CUT OFF BY THE PARENT, silently. It survived at "Calculate Height" /
   "Calculate Angle" and broke the moment the Sin modes were renamed for their
   direction - "Calculate Slip Gauge To Angle" needs ~214px with its padding, so
   the pair needed 428 against a 380 cap and the second one read
   "Calculate Slip Gauge To Ar…".

   ⚠ THE CAP ALONE IS NOT THE FIX, AND MUST NOT BE TREATED AS ONE. Below about
   460px the control is narrower than the cap anyway - at 375 the form is 328px -
   so two nowrap captions can never both fit and raising the number just moves
   the breakage off the desktop. Wrapping is what makes it correct at EVERY
   width; the cap only keeps them on one line where there is room. */
.calc-seg {
  display: flex; width: 100%; max-width: 460px; overflow: hidden; background: #fff;
  border: 1.5px solid var(--field-border); border-radius: var(--radius-field);
}
.calc-seg-btn {
  flex: 1 1 0;
  display: flex; align-items: center; justify-content: center; text-align: center;
  min-height: 42px; padding: .5rem .9rem;
  border: none; background: transparent; cursor: pointer;
  font-size: .82rem; font-weight: 600; color: var(--muted); white-space: nowrap;
}

/* ⚠ THE WRAP IS A MODIFIER, NOT THE DEFAULT - and making it the default broke
   the other control the same day. `.calc-seg` has two users: the Sin mode pair,
   whose captions are sentences, and the block set (83 / 87 Pieces), whose two
   words sit in a `col-sm-auto` column. An AUTO column is sized by its content's
   min-content width, and `white-space: nowrap` is what makes that the width of
   the caption - so lifting nowrap globally let that column collapse and "83
   Pieces" wrapped to two lines, growing a 44px control to 51px for no reason.

   min-width: 0 is the half of this that `flex: 1 1 0` does NOT give you: without
   it a flex item floors at its longest unbreakable run and overflows the parent
   whatever the basis says. With nowrap gone, the run is one word. */
.calc-seg-wrap .calc-seg-btn { white-space: normal; min-width: 0; line-height: 1.25; }
.calc-seg-btn + .calc-seg-btn { border-left: 1px solid var(--field-border); }
.calc-seg-btn[aria-pressed="true"] { background: var(--accent-tint); color: var(--violet-ink); }

/* The masters engine draws a short fixed select as this control ('widget' =>
   'segment', Quality Parameter's Reading Type). Required and unanswered, it is
   marked the way a field is - markInvalid() puts .is-invalid on the group itself,
   because the value lives in a hidden input nobody can see. */
.m-seg { max-width: 100%; }
.calc-seg.is-invalid { border-color: var(--danger); border-width: 2px; }

/* ── The one inline message on the screen ─────────────────────────────────────
   The block solver's, and only the block solver's. Every other fault is a toast
   plus a red box (calcShowErrors) - see the note in the view for why this one
   is different: it explains the TARGET, not a field, and it changes with every
   digit typed, so as a toast it would fire three times while 55.273 is keyed. */
.calc-msg {
  display: block; margin-top: .85rem; padding: .55rem .75rem;
  background: var(--warn-bg); border: 1px solid #fed7aa;
  border-radius: var(--radius-sm); color: var(--warn-ink);
  font-size: .8rem; line-height: 1.45;
}
.calc-msg[hidden] { display: none; }

/* ── The block stack - the order the blocks come out of the cupboard ─────── */
.calc-stack { margin-top: .85rem; }
.calc-stack[hidden] { display: none; }
.calc-stack-hd {
  display: flex; align-items: center; gap: .5rem; margin-bottom: .35rem;
  font-size: .72rem; font-weight: 600; color: var(--muted);
}
.calc-stack-hd .calc-badge { margin-left: auto; }
/* The copy button is a .fl-clear outside a field, so it has no .has-value to
   reveal it - shown here unconditionally, and static rather than absolute. */
.calc-stack-hd .fl-copy { position: static; display: inline-flex; transform: none; }
.calc-stack-list {
  list-style: none; counter-reset: blk; margin: 0; padding: 0;
  background: #fff; border: 1px solid var(--card-border);
  border-radius: var(--radius-sm); overflow: hidden;
}
.calc-stack-list li {
  counter-increment: blk;
  display: flex; align-items: center; gap: .6rem;
  padding: .45rem .75rem; border-bottom: 1px solid var(--line); font-size: .88rem;
}
.calc-stack-list li:last-child { border-bottom: none; }
.calc-stack-list li::before {
  content: counter(blk); flex-shrink: 0; width: 18px;
  font-size: .7rem; font-weight: 700; color: var(--faint);
}
.cs-blk { font-weight: 700; color: var(--ink-900); font-variant-numeric: tabular-nums; }
.cs-run { margin-left: auto; font-size: .78rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.calc-stack-ft {
  margin-top: .4rem; font-size: .72rem; color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* ── Responsive ──────────────────────────────────────────────────────────── */
@media (max-width: 575.98px) {
  /* Rows stay 44px on a phone even as the type shrinks - a menu row is a thumb
     target, and this list is used with gloves on. */
  .calc-tab { min-height: 44px; }
  .calc-tab-btn { padding: .5rem .3rem .5rem .6rem; font-size: .8rem; }
  .calc-nav-trigger { margin-bottom: .75rem; font-size: .82rem; }
  .calc-fav { width: 34px; }
  .calc-fig { padding: .5rem; }
  /* 155px of field at 375px: every pixel not spent on the unit is a digit. */
  .calc-panel .fl-suffix { right: 7px; font-size: .64rem; }
  .calc-panel .fl-field:has(.fl-suffix) input { padding-right: 27px; font-size: .95rem; }
  .calc-panel .fl-field.has-value:has(.fl-clear) input { padding-right: 50px; }
  .fl-clear { right: 26px; width: 20px; height: 20px; }
  .calc-out-val { font-size: 1.3rem; padding-right: 30px; }
  .calc-panel .fl-hint { font-size: .66rem; line-height: 1.3; }
  /* ⚠ .8rem, NOT .85 (2026-08-22). The Ø prefix cost about 13px and pushed the
     two longest labels over: "Ø Raceway Large I.D." measured 133px against the
     130px a 156px field leaves at 375, so it ellipsised to "Ø Raceway Large
     I.…" - losing the half that says WHICH raceway. .8rem measures it at 125
     and clears with 5px to spare.

     ⚠ THE FIX IS THE FONT, NOT THE INSET, and that was not the first attempt.
     Widening the label box means either moving its left inset off the input's
     12px text origin - so the label and the value it labels no longer line up -
     or extending it right, under the MM suffix. Both are visible at rest on
     every field; a label half a point smaller is visible on none. */
  .calc-panel .fl-field label { font-size: .8rem; }
  .calc-panel .fl-field:focus-within label,
  .calc-panel .fl-field.has-value:not(:focus-within) label { font-size: .68rem; }
}
@media (max-width: 374.98px) {
  .calc-tab-btn span { font-size: .74rem; }
}

/* ==========================================================================
   15e. Operator Report - .rp-*
   ==========================================================================
   One summed grid: a row per operator per month. Almost nothing here is new -
   the grid is .table-app + .table-nowrap and the card is .section-card.

   ⚠ THE PIVOT AND ITS VENDOR OVERRIDES WERE DELETED ON 2026-08-24 with the
   WebDataRocks library itself (the user's instruction). Gone: .rp-seg,
   .rp-view[hidden], .rp-pivot, .rp-pivot-note and every #rpPivot / #wdr-*
   rule that scaled the vendor toolbar. Do not write markup against them.
   ========================================================================== */

/* The count beside the filters. Not a badge: it states a fact about the screen
   rather than marking a status, and a coloured pill among the filter controls
   reads as another control. */
.rp-count { font-size: .76rem; color: var(--muted); font-weight: 600; white-space: nowrap; }
.rp-count strong { color: var(--ink-900); font-weight: 700; }

/* A month with no target prints a dash, never 0% (CALCULATIONS §3.7) - this is
   what makes the dash read as "nothing was expected" rather than as a value that
   failed to load. */
.rp-null { color: var(--faint); }

/* ⚠ THE HEADER WRAPS; THE CELLS DO NOT. `.table-nowrap` keeps every DATA row one
   line tall, which is what it is for, and it sets nowrap on `thead th` too -
   this line is what takes that back. Four of the Operator Report's captions are
   three words ("Pre-Machining Rejection Qty.") and holding those on one line
   costs over 200px of table width; wrapped, that grid fits with no horizontal
   scroll from 1280 up. The captions are the user's own wording and are not to be
   shortened to buy the width back. */
#rpTable thead th { white-space: normal; }

/* ⚠ AND THE PRODUCTION REPORT OPTS BACK OUT OF IT (2026-08-25, the user's
   instruction). Wrapping a caption is what pushes its sort icon onto a line of
   its own - the icon is inline content, so when the last line is full it is the
   thing that drops - and with fifteen columns sharing the width that happened on
   almost all of them, including "Shift". The header stood 112px tall with the
   icons orphaned beneath their captions.

   Restoring nowrap puts every caption and its icon on ONE line and takes the
   header to a single row. It also means the COLUMN WIDTHS LOOK AFTER THEMSELVES:
   each column becomes as wide as the wider of its caption and its longest value,
   which is why that bundle declares almost no widths.

   ⚠ IT IS A MODIFIER AND NOT A CHANGE TO THE RULE ABOVE, because the Operator
   Report needs the opposite. Specificity (1,1,2) beats (1,0,2), so the class
   wins wherever it is applied and nothing else moves.

   The cost is table width - this grid scrolls further inside .table-wrap - and
   that is the trade the user chose. The page body still never scrolls (R9). */
#rpTable.rp-head-nowrap thead th { white-space: nowrap; }

/* ── The date range INSIDE the card header - .rp-hdr-range ──────────────────
   The Machine Production Report has no filter card (2026-08-25, the user's
   instruction): it held one control and a count strip, cost about 100px above a
   report that is fifteen rows tall, and the point of removing it was to see
   every machine at once.

   ⚠ ALMOST NOTHING IS NEEDED, because .card-header-custom is already
   `display:flex; justify-content:space-between; flex-wrap:wrap` - the picker
   sits hard right beside the title and drops onto its own line on a phone with
   no rule of its own. What IS needed is the vertical padding back: a header
   holding a control is taller than one holding two lines of text, and 1rem top
   and bottom around a 34px trigger reads as a gap rather than a header.

   The trigger keeps its own width; it is not stretched to the header, because a
   date range that spans a 1900px card looks like a search box. */
.rp-hdr-range { padding-top: .6rem; padding-bottom: .6rem; }

/* ── The MoM toggle on the Production Report card header - .rp-head-tools / .rp-mom
   ──────────────────────────────────────────────────────────────────────────────
   2026-08-29, the user's instruction: a button that regroups the entry grid by
   Month-Year. .card-header-custom is `justify-content: space-between` with two
   children today (the title, the caption); a third would strand the caption in
   the middle, so the caption and the button share one flex box, hard right.

   The button borrows .rp-fbtn's washed-indigo look rather than adding a fourth
   button style to the app, and takes a SOLID fill when it is on so the current
   view is legible from more than the label alone. */
.rp-head-tools {
  display: inline-flex; align-items: center; gap: .55rem;
  flex-wrap: wrap; justify-content: flex-end;
}
/* ⚠ `.filter-bar .rp-mom` TOO (2026-09-17): the Operator Overall Report carries
   its toggle on the filter bar. Scoped by a parent on purpose - (0,2,0) is what
   beats Bootstrap's `.btn` colour, which a bare `.rp-mom` would lose to. */
.rp-head-tools .rp-mom, .filter-bar .rp-mom {
  display: inline-flex; align-items: center; white-space: nowrap;
  background: var(--accent-wash); border: 1px solid #c7d2fe; color: var(--accent);
  font-weight: 600;
}
.rp-head-tools .rp-mom:hover, .rp-head-tools .rp-mom:focus,
.filter-bar .rp-mom:hover, .filter-bar .rp-mom:focus {
  background: var(--accent-tint); color: var(--accent-dark);
}
.rp-head-tools .rp-mom.is-on, .filter-bar .rp-mom.is-on {
  background: var(--accent); border-color: var(--accent); color: #fff;
}
.rp-head-tools .rp-mom.is-on:hover, .rp-head-tools .rp-mom.is-on:focus,
.filter-bar .rp-mom.is-on:hover, .filter-bar .rp-mom.is-on:focus {
  background: var(--accent-dark); border-color: var(--accent-dark); color: #fff;
}

/* ── The filter bar, the chips and the drawer - .rp-fbar / .rp-chips / .rp-drawer
   ──────────────────────────────────────────────────────────────────────────────
   Built 2026-08-25 on the user's instruction, from their reference screenshot.
   The Production Report had eight controls in one bar: a wall of dropdowns on a
   desktop, and eight stacked rows on a phone before a single figure was visible.
   Four stay - SEARCH, the DATE RANGE, FILTERS and CLEAR - and the other seven
   moved behind the button.

   ⚠ THE DRAWER IS A BOOTSTRAP OFFCANVAS. The bundle is on every page and the
   calculator's rail already uses one, so the backdrop, the focus trap, ESC and
   the scroll lock are Bootstrap's rather than three more rules here. */

/* The search box leads and takes the slack; everything after it is its own size.
   Below 576 it takes the whole row, because a 140px search box beside a date
   range is a box nobody can read what they typed into. */
.rp-search { flex: 1 1 220px; min-width: 180px; max-width: 340px; }

@media (max-width: 575.98px) {
  .rp-search { flex: 1 1 100%; max-width: none; }
}

/* ── The tone of the bar ─────────────────────────────────────────────────────
   ⚠ LIGHT, NOT DARK - corrected 2026-08-25 on the user's instruction ("current
   is more darker"), against their reference screenshot.

   ⚠ THE CAUSE WAS A TOKEN THAT DOES NOT MEAN WHAT IT READS LIKE. The chip was
   written `background: var(--accent-soft)` on the assumption that "soft" meant a
   wash. It does not: `--accent-soft` is #a5b4fc, a saturated indigo used for the
   sidebar's active bar, and the fallback beside it could never apply because the
   token exists. The pale ones are `--accent-wash` (#f0f4ff) and `--accent-tint`
   (#ede9fe). Check the value, not the name.

   The bar reads in two tones, which is what the reference does:
     ACCENT-WASHED - the date range, and Filters. Controls that are on, or that
                     always carry a value.
     NEUTRAL       - Clear. It undoes rather than filters, and a tinted Clear
                     among tinted filters is a fifth thing that looks active. */

/* The date trigger is white everywhere else in the app; inside this bar it is
   washed, because it always holds a value and the reference shows it that way.
   ⚠ SCOPED TO .rp-fbar so the Activity Log, the dashboard and the other reports
   keep the plain white trigger they already have. */
.rp-fbar .dr-trigger {
  background: var(--accent-wash); border-color: #c7d2fe; color: var(--accent); font-weight: 600;
}
.rp-fbar .dr-trigger:hover { background: var(--accent-tint); }

/* ⚠ OVERRIDES .btn-soft's #ede9fe, WHICH READ AS THE DARKER OF THE TWO against a
   white card. Washed fill, a light indigo hairline, indigo text. */
.rp-fbar .rp-fbtn {
  display: inline-flex; align-items: center; white-space: nowrap;
  background: var(--accent-wash); border: 1px solid #c7d2fe; color: var(--accent);
  font-weight: 600;
}
.rp-fbar .rp-fbtn:hover, .rp-fbar .rp-fbtn:focus { background: var(--accent-tint); color: var(--accent-dark); }

/* ⚠ THE COUNT IS A SOLID BADGE AND IS HIDDEN AT ZERO. Solid because it is the
   one thing on the bar that has to be read at a glance - the reference makes it
   the only saturated element there - and hidden because a "0" beside Filters is
   a figure somebody has to read before learning it means "none". */
.rp-fcount {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 18px; height: 18px; margin-left: .4rem; padding: 0 5px;
  border-radius: 999px; background: var(--accent); color: #fff;
  font-size: .66rem; font-weight: 800; line-height: 1;
}
/* [hidden] loses to any author display - the trap already recorded at
   .f-count[hidden] and .hs-cell[hidden]. .rp-fcount HAS an author display, so
   without this the badge shows an empty pill at zero. */
.rp-fcount[hidden] { display: none !important; }

/* Neutral, because it undoes. */
/* ⚠ SCOPED `.rp-fbar .rp-clear` (0,2,0) RATHER THAN `.rp-clear` (0,1,0), and
   that is not belt and braces. Bootstrap's `.btn` sets `color: var(--bs-btn-color)`
   at the same (0,1,0) specificity, and at equal specificity the winner is
   whichever the cascade saw last - which is not something to leave to the order
   two stylesheets happen to load in. Measured before the fix: the button
   rendered #1e293b on a white fill with a #1e293b border, which is the darkest
   thing on a bar whose whole complaint was being too dark. */
.rp-fbar .rp-clear {
  white-space: nowrap; background: #fff; border: 1px solid var(--card-border);
  color: var(--muted); font-weight: 600;
}
.rp-fbar .rp-clear:hover:not(:disabled) { background: #f8fafc; color: var(--body); border-color: var(--field-border); }
.rp-fbar .rp-clear:disabled { opacity: .5; cursor: default; background: #fff; color: var(--faint); }

/* ── The chips ───────────────────────────────────────────────────────────────
   One per filter that is on. Each names its FIELD as well as its value, because
   "2nd" alone leaves the reader guessing which of seven dropdowns it came from.

   ⚠ THE LIGHTEST THING ON THE SCREEN. There can be seven of them at once, and
   seven saturated pills under a filter bar is a second toolbar competing with the
   grid they are describing. Washed fill, hairline border, indigo text. */
.rp-chips {
  display: flex; flex-wrap: wrap; gap: .4rem;
  padding: .6rem 1rem .75rem;
  border-top: 1px solid var(--line);
}
.rp-chips[hidden] { display: none !important; }

.rp-chip {
  display: inline-flex; align-items: center; gap: .25rem;
  padding: .2rem .3rem .2rem .55rem;
  border-radius: 999px; border: 1px solid #e0e7ff;
  background: var(--accent-wash);
  font-size: .72rem; font-weight: 600; color: var(--accent);
  max-width: 100%;
}
/* The field name is the quieter half - the VALUE is what is being read. */
.rp-chip-k { color: #818cf8; font-weight: 700; }

/* A button, not a span with a click handler: for a keyboard user it is the only
   way to take one filter off without opening the drawer. */
.rp-chip-x {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; padding: 0; margin-left: .1rem;
  border: none; border-radius: 50%; background: transparent;
  color: #818cf8; font-size: .95rem; line-height: 1; cursor: pointer;
}
.rp-chip-x:hover { background: #e0e7ff; color: var(--accent-dark); }
.rp-chip-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ── The drawer ──────────────────────────────────────────────────────────────
   ⚠ 380px, NOT Bootstrap's 400 - measured: seven fields plus their labels need
   340px of content, and 380 leaves the padding without stranding the panel's
   own scrollbar. Below 576 it takes the width it is given. */
.rp-drawer { width: 380px; max-width: 100%; }

.rp-drawer .offcanvas-header { border-bottom: 1px solid var(--line); padding: .85rem 1.15rem; }
.rp-drawer .offcanvas-title  { font-size: .85rem; font-weight: 700; color: var(--ink-900); }

/* ⚠ THE BODY SCROLLS, THE FOOT DOES NOT. Bootstrap's body is already
   `overflow-y: auto`; what it does not do is keep a footer out of that scroll,
   so without `flex-shrink: 0` the two buttons scroll away with the seventh field
   and Apply is unreachable on a short screen - which is the one control the
   whole panel exists to reach. */
.rp-drawer .offcanvas-body { padding: 1rem 1.15rem; display: flex; flex-direction: column; gap: .85rem; }
.rp-dw-field { width: 100%; }

.rp-drawer-foot {
  flex-shrink: 0;
  display: flex; gap: .6rem; padding: .85rem 1.15rem;
  border-top: 1px solid var(--line); background: var(--bg-soft, #f8fafc);
}
/* Apply is the primary and takes the room; Reset All is the one you press by
   accident, so it stays the size of its own label. */
.rp-drawer-foot .btn-save { flex: 1 1 auto; }

/* Part Drawing's filter bar (2026-09-18): Search, Filters and Clear on the LEFT,
   "Last Updated As On" pushed RIGHT as two lines - caption over value - so it
   stays narrow enough to share the row on a phone. */
.pd-fbar .rp-fbtn, .pd-fbar .rp-clear { display: inline-flex; align-items: center; gap: .3rem; }
.pd-last {
  margin-left: auto; display: flex; flex-direction: column; align-items: flex-end;
  line-height: 1.2; white-space: nowrap;
}
.pd-last-k { font-size: .68rem; color: var(--muted); }
.pd-last strong { font-size: .8rem; color: var(--ink-900); font-weight: 700; }
@media (max-width: 575.98px) {
  /* One row on a phone: the search box gives way, the buttons are icon-only. */
  .pd-fbar { flex-wrap: nowrap; }
  .pd-fbar .rp-search { flex: 1 1 0; min-width: 0; }
  .pd-fbar .rp-fbtn, .pd-fbar .rp-clear { padding-left: .5rem; padding-right: .5rem; }
}
/* Grid headers that may wrap (Part Drawing, 2026-09-18): a long caption breaks
   onto two lines so its column can stay narrow. The last word and the sort icon
   are kept together, so the icon never sits alone on a line. */
.table-nowrap thead th.th-wrap { white-space: normal; line-height: 1.25; }
/* A Part Drawing row opens its read-only dialog (#pdvModal, .rwv-* classes). */
#pdBody tr.pd-row { cursor: pointer; }
/* So does a Visual & Quantity Record row (#vqvModal, the same .rwv-* classes). */
#vqBody tr.vq-row { cursor: pointer; }
/* Its Total OK Qty. can be negative on a draft - red, as the grid prints it. */
.rwv-bal-val.is-neg { color: var(--danger-ink); }

/* Below 576 the header wraps to two lines and the picker takes the full width -
   a 240px control floated right under a heading reads as detached from it. */
@media (max-width: 575.98px) {
  .rp-hdr-range { gap: .5rem; }
  .rp-hdr-range > div:last-child,
  .rp-hdr-range .dr-trigger { width: 100%; }
}

/* The totals row. Sticky to the bottom of the scrolling body would need a fixed
   table height, which this grid deliberately does not have; it sits at the foot
   of the page's rows instead.

   ⚠ ITS HORIZONTAL PADDING MUST BE THE BODY'S, AND THE DEFAULT IS NOT (fixed
   2026-08-25, the user's report: "footer data and grid data are slightly
   misaligned for every column").

   `.table-app tbody td` sets `padding: .3rem 1rem`, but there is no matching rule
   for `tfoot`, so those cells fell through to Bootstrap's
   `.table > :not(caption) > * > *`, which is `.5rem .5rem`. THE CELL BOXES LINE
   UP PERFECTLY - measured, every column 0px out - and the FIGURES INSIDE THEM do
   not, because 8px of padding against 16px moves right-aligned text by exactly
   8px. It reads as a whole column being off by a character, and it is invisible
   to any check that measures the cells rather than what is drawn in them.

   The vertical padding is deliberately left larger than the body's: a totals row
   is worth a little more air than the rows it totals. */
#rpFoot td {
  background: #f8fafc; border-top: 2px solid var(--line);
  font-weight: 700; color: var(--ink-900);
  padding: .5rem 1rem;
}
#rpFoot .rp-foot-lbl { font-weight: 600; color: var(--muted); }

@media (max-width: 575.98px) {
  .rp-count { width: 100%; text-align: right; }
}

/* ==========================================================================
   15f. Master Data rail - .mnav-*
   ==========================================================================
   The master is chosen from a vertical rail: a static card from 1200px up, a
   Bootstrap off-canvas drawer below it. It replaced the topbar dropdown on
   2026-08-31 so the masters could carry GROUP HEADINGS, which a dropdown
   cannot show.

   ⚠ THIS IS A SEPARATE COPY OF THE CALCULATOR'S RAIL (section 15d, .calc-*),
   AND THE USER CHOSE THE COPY OVER SHARING ONE BLOCK. The two are cousins: the
   Bootstrap overrides below, the sticky/max-height arrangement and the active
   row's language are all the same argument, most of it found the hard way in
   15d. A FIX FOUND IN EITHER BLOCK IS WORTH CARRYING TO THE OTHER BY HAND -
   nothing will tell you. Both block headers name each other, so the second copy
   is at least findable from the first.

   What is deliberately NOT copied from 15d:
     - no filter box - six rows do not need finding, and the calculator's box
       only earns its place past eight;
     - no favourite star - the tab the user last used already answers "which
       master does this open on", and the dashboard's own deep links override it
       anyway. That also lets a row be a plain button element, where 15d needs a
       wrapper div (a button inside a button is invalid markup);
     - icons ARE kept - the calculator dropped them because seventeen glyphs all
       meaning "calculator" distinguish nothing, whereas a building, a person, a
       machine and a thermometer are six different things.

   ⚠ THE BREAKPOINT IS 1200 AND IT WAS CHOSEN, NOT INHERITED. The masters grid
   is 8-11 columns and never wraps a cell, so at 1024 it needs every pixel; a
   rail there would spend ~200 of them on a control used a few times a session.
   1200 is also 15d's breakpoint, so the application has ONE answer to "where
   does a side rail appear".
   ========================================================================== */
.mnav-shell {
  display: block;
  /* ⚠ MEASURED, NOT GUESSED - the lesson 15d paid for three times (216 → 248 →
     264 as its names grew). Measured against the shipped stylesheet, 2026-08-31:

       widest label ("Customers")        63.16px of text
       row chrome (3px active bar + 12.8 lead-in + 16.72 icon
                   + 8.8 gap + 8 right + the reserved scrollbar gutter)
                                         59.31px
       the card's own border              2.00px
       ------------------------------------------------
       the narrowest rail that fits it  124.47px

     208 therefore leaves 83.5px of headroom - a label of up to 146.69px before
     anything ellipsises, which is roughly "Breakdown Reasons". That is the
     seventh master's allowance, and it is the whole argument for the slack.

     ⚠ AND THE SLACK IS NOT FREE. Every pixel here is a pixel the grid does not
     get. Measured at 1366 with all six masters loaded: five of them fit their
     column exactly, and OPERATORS - 11 columns, 1181px - scrolls 92px inside
     .table-wrap where before the rail it fitted. The page itself never scrolls
     sideways at any width (R9). Do not raise this without re-measuring both
     halves; do not lower it without measuring the longest label first. */
  --mnav-w: 208px;
}
@media (min-width: 1200px) {
  .mnav-shell {
    display: grid; align-items: start; gap: 1.25rem;
    grid-template-columns: var(--mnav-w) minmax(0, 1fr);
  }
  /* ⚠ THE RAIL STICKS AND SCROLLS ITSELF, so the PAGE scrolls only when the
     grid is taller than the screen. --calc-rail-top is the topbar's height plus
     the page padding - one number with one meaning, and re-typing
     calc(65px + 1rem) here is the one piece of duplication that would actually
     break something: 15d asserts the rail's resting 81px against it. The
     --calc- prefix is the name it already had; it is not calculator-only. */
  .mnav {
    position: sticky; top: var(--calc-rail-top);
    max-height: calc(100vh - var(--calc-rail-top) - 1rem);
    /* ⚠ THE CARD HAS TO BE A COLUMN FLEX BOX. Above the breakpoint Bootstrap
       turns a responsive offcanvas into a plain display:block, and a block
       parent does NOT hand its max-height down - the body sizes to its content
       and overflows, clipped by the card's own overflow:hidden, so nothing
       scrolls and the bottom rows are unreachable. Flex propagates the cap. */
    display: flex !important; flex-direction: column;
    /* ⚠ THE BACKGROUND NEEDS !important. Bootstrap sets
       .offcanvas-xl { background-color: transparent !important } above the
       breakpoint; there is no way to out-specify it. */
    background: var(--card-bg) !important;
    border: 1px solid var(--card-border);
    border-radius: var(--radius); box-shadow: var(--shadow-card);
    overflow: hidden;                 /* so a row's tint stops at the radius */
  }
  /* ⚠ AND THE SELECTOR HAS TO OUT-SPECIFY BOOTSTRAP'S, NOT MERELY FOLLOW IT.
     Theirs is .offcanvas-xl .offcanvas-body { display: flex } at (0,2,0); a
     bare .mnav-body is (0,1,0) and loses however late it loads. Left as a ROW
     flex container it makes the list a flex ITEM, which shrinks every row to
     its own text - that is what makes an active row's tint stop two thirds of
     the way across instead of filling the card.

     ⚠ min-height: 0 IS LOAD-BEARING. A flex item's default is auto, so without
     it the body refuses to shrink below its content and the max-height above
     does nothing at all.

     10px top and bottom, ZERO left and right: the vertical padding lifts the
     first and last rows off the rounded corners, and horizontal padding is what
     would stop a row's tint and its 3px active bar reaching the card's edge. */
  .mnav.offcanvas-xl .offcanvas-body {
    display: flex; flex-direction: column; align-items: stretch;
    padding: 10px 0; min-height: 0; overflow: hidden;
  }
  .mnav.offcanvas-xl .offcanvas-body .mnav-list {
    flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
    /* Reserved always, not only when it appears: a bar that shows up on a short
       viewport and not a tall one makes the row width unmeasurable, which is
       how 15d ended up with four names ellipsising at 600px high and none at
       768px. Six masters will not scroll today; a seventh might. */
    scrollbar-gutter: stable;
  }
}
/* Below 1200 Bootstrap owns the offcanvas geometry; only the width is ours. */
@media (max-width: 1199.98px) {
  .mnav { max-width: 300px; width: 86vw; }
  /* Same column-flex shape as the rail so the list scrolls inside the drawer
     rather than the drawer growing, and overscroll-behavior stops a flick at
     the end of the list from scrolling the page behind it. No horizontal
     padding, for the same reason as the rail. */
  .mnav-body {
    display: flex; flex-direction: column; align-items: stretch;
    padding: .25rem 0 1rem; min-height: 0; overflow: hidden;
  }
  .mnav-body .mnav-list {
    flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  }
}
.mnav-content { min-width: 0; }   /* a grid child must be told it may shrink */

/* The trigger names WHERE YOU ARE, not just what it opens - "Master Data" alone
   would make the current master unreadable the moment the drawer is shut. */
.mnav-trigger {
  display: flex; align-items: center; gap: .55rem; width: 100%;
  min-height: 44px; padding: .5rem .85rem; margin-bottom: 1rem;
  background: var(--card-bg); border: 1px solid var(--card-border);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-card);
  color: var(--ink-600); font-size: .85rem; font-weight: 600; cursor: pointer;
}
.mnav-trigger:hover { border-color: var(--accent-soft); background: var(--accent-wash); }
.mnav-trigger i { font-size: 1rem; opacity: .8; }
.mnav-trigger-lbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* No gap: a continuous list separated by a hairline, not a stack of chips. */
.mnav-list { display: flex; flex-direction: column; }
.mnav-list { scrollbar-width: thin; scrollbar-color: var(--field-border) transparent; }
.mnav-list::-webkit-scrollbar { width: 6px; }
.mnav-list::-webkit-scrollbar-track { background: transparent; }
.mnav-list::-webkit-scrollbar-thumb { background: var(--field-border); border-radius: 3px; }
.mnav-list::-webkit-scrollbar-thumb:hover { background: var(--muted); }

/* ── The group heading ────────────────────────────────────────────────────────
   PLAIN AND ALWAYS OPEN (the user's call, 2026-08-31): no caret, no accordion,
   nothing remembered per user. With one group, a control that collapses it is a
   control that does nothing - and a rail whose only heading can be hidden is a
   rail that can be left showing nothing at all.

   ⚠ IF THEY EVER DO COLLAPSE, THEY FOLLOW ROLE MANAGEMENT'S RULE: hide the rows
   IN PLACE, never unmount them. Nothing here reads the DOM to decide what is
   saved, so unmounting would not lose data the way it does there - but the boot
   lookup and switchTab() both query every .mnav-item in the document, and a
   master inside a collapsed group has to stay findable by them.

   It carries the same horizontal lead-in as a row's label, so the heading and
   the names under it share one left edge. The first row after a heading drops
   its hairline: the heading is already the separator, and two rules 8px apart
   read as a mistake. */
.mnav-group {
  padding: .55rem .8rem .3rem;
  font-size: .68rem; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--faint);
}
.mnav-group + .mnav-item { border-top: none; }
/* Not the FIRST heading in the list - that one is already lifted by the body's
   own 10px, and a second gap above it makes the card look mis-padded. */
.mnav-item + .mnav-group {
  margin-top: .35rem; padding-top: .55rem; border-top: 1px solid var(--line);
}

/* ── One row ──────────────────────────────────────────────────────────────────
   ⚠ IT BORROWS THE SIDEBAR'S ACTIVE LANGUAGE, exactly as 15d does: a tinted
   background and a 3px accent bar down the left edge, like .nav-item.active.
   Bordered pills inside a bordered card are two nested boxes per row, and a
   list needs one edge to run the eye down, not six. */
.mnav-item {
  display: flex; align-items: center; gap: .55rem;
  align-self: stretch; width: 100%; min-height: 38px;
  padding: .45rem .5rem .45rem .8rem;
  border: none; border-left: 3px solid transparent; background: transparent;
  color: var(--muted); cursor: pointer; text-align: left;
  font-size: .82rem; font-weight: 600;
  transition: background .15s, color .15s, border-color .15s;
}
.mnav-item + .mnav-item { border-top: 1px solid var(--line); }
.mnav-item > i { flex: 0 0 auto; width: 1.1em; font-size: .95rem; opacity: .85; }
/* The label may ellipsis rather than wrap - a two-line row breaks the rhythm of
   the list, and the rail is sized so it should never come to that. */
.mnav-lbl {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.mnav-item:hover { background: var(--accent-wash); color: var(--ink-600); }
.mnav-item.active {
  background: var(--accent-tint); border-left-color: var(--accent); color: var(--violet-ink);
}
.mnav-item.active > i { opacity: 1; }

@media (max-width: 767.98px) {
  /* 44px is the touch target the drawer is used with. */
  .mnav-item { min-height: 44px; }
  .mnav-trigger { margin-bottom: .75rem; font-size: .82rem; }
}

/* ==========================================================================
   15g. Rework Pending - the KPI cards and the row dialog - .rw-kpi / .rwv-*
   ==========================================================================
   ⚠ NOT A REBUILD OF THE DELETED .kpi-* TILES (§3's tombstone). The user asked
   for these four on 2026-09-18, and the tombstone's test was applied: the two
   figures that were already on screen (entries and PCS pending, in the grid's
   count line) were REMOVED from there, so each figure prints once. Light card,
   tinted icon, dark figure - never the old white-on-gradient tile.

   The row dialog carries everything about one pending row and every operation
   on it, so the screen works from a phone (the user's instruction): the grid
   has View / Edit / Delete, and Add / Reduce live only in here. */
.rw-kpis { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: .75rem; }
.rw-kpi {
  display: flex; align-items: center; gap: .75rem; min-width: 0;
  background: var(--card-bg); border: 1px solid var(--card-border);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-card); padding: .8rem .95rem;
}
.rw-kpi-ic {
  flex: 0 0 40px; width: 40px; height: 40px; border-radius: 10px;
  display: flex; align-items: center; justify-content: center; font-size: 1.1rem;
}
.rw-kpi-ic.ic-a { background: var(--accent-tint); color: var(--violet-ink); }
.rw-kpi-ic.ic-b { background: var(--info-bg);     color: var(--info-ink); }
.rw-kpi-ic.ic-c { background: var(--warn-bg);     color: var(--warn-ink); }
.rw-kpi-ic.ic-d { background: var(--ok-bg);       color: var(--ok-ink); }
.rw-kpi-body { min-width: 0; }
.rw-kpi-lbl { font-size: .72rem; font-weight: 600; color: var(--muted); line-height: 1.25; }
.rw-kpi-val {
  font-size: 1.3rem; font-weight: 700; color: var(--ink-900); line-height: 1.2;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.rw-kpi-sub { font-size: .7rem; color: var(--muted); line-height: 1.3; }   /* wraps - never cut a figure */

/* The whole row opens the dialog - a thumb aims at a row, not a 28px icon. */
#rwBody tr.rw-row,
#hpBody tr.rw-row { cursor: pointer; }   /* Pending For H.T. borrows the same row */

/* ── The row dialog ───────────────────────────────────────────────────────── */
/* Pending Qty. on ONE line - label left, figure right (the user's call,
   2026-09-18: vertical height is scarce in a full-screen phone dialog). */
.rwv-bal {
  display: flex; align-items: baseline; justify-content: space-between; gap: .75rem;
  background: var(--accent-wash); border: 1px solid var(--card-border);
  border-radius: var(--radius-sm); padding: .45rem .8rem; margin-bottom: .75rem;
}
.rwv-bal-lbl { font-size: .78rem; font-weight: 600; color: var(--muted); }
.rwv-bal-val { font-size: 1.1rem; font-weight: 700; color: var(--ink-900); line-height: 1.2; white-space: nowrap; }
.rwv-bal-val small { font-size: .72rem; font-weight: 600; color: var(--muted); margin-left: .25rem; }
.rwv-bal-val.is-zero { color: var(--faint); }

/* Label left, value right, one row per field (the user's choice, 2026-09-18,
   option A). Two columns of label-over-value read as one zig-zag of
   similar-weight text; a fixed label column scans top to bottom like a spec
   sheet, and a long vendor name or remark wraps inside its OWN column so the
   label beside it never moves and nothing is cut off. */
.rwv-grid { border: 1px solid var(--card-border); border-radius: var(--radius-sm); overflow: hidden; }
.rwv-f {
  display: grid; grid-template-columns: 124px minmax(0, 1fr); gap: .75rem;
  align-items: baseline; padding: .5rem .8rem; min-width: 0;
}
.rwv-f + .rwv-f { border-top: 1px solid var(--line); }
.rwv-k { font-size: .74rem; font-weight: 500; color: var(--muted); }
.rwv-v { font-size: .86rem; font-weight: 600; color: var(--ink-900); overflow-wrap: anywhere; }

.rwv-sec {
  font-size: .78rem; font-weight: 700; color: var(--ink-900);
  margin: 1.1rem 0 .55rem; padding-top: .9rem; border-top: 1px solid var(--line);
}
.rwv-dir { max-width: none; margin-bottom: .75rem; }
.rwv-dir .calc-seg-btn[data-dir="in"][aria-pressed="true"]  { background: var(--ok-bg);     color: var(--ok-ink); }
.rwv-dir .calc-seg-btn[data-dir="out"][aria-pressed="true"] { background: var(--danger-bg); color: var(--danger-ink); }

/* The footer: Cancel and Save, equal width. A view-only user gets Cancel alone,
   full width. The buttons carry Bootstrap's footer margin, so it is zeroed and
   the gap does the spacing. */
#rwvModal .rwv-foot,
#hpvModal .rwv-foot { display: flex; flex-wrap: nowrap; gap: .5rem; }
#rwvModal .rwv-foot > .btn,
#hpvModal .rwv-foot > .btn {
  flex: 1 1 0; min-width: 0; margin: 0; padding: .4rem .5rem;   /* one padding for both, */
  border: 1px solid transparent;                                 /* one border - or the  */
}                                                                /* widths come out unequal */
#rwvModal .rwv-foot > .btn-light,
#hpvModal .rwv-foot > .btn-light { background: #fff; border-color: var(--card-border); }

@media (max-width: 991.98px) {
  .rw-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 575.98px) {
  .rw-kpis { gap: .5rem; }
  .rw-kpi { padding: .6rem .65rem; gap: .55rem; }
  .rw-kpi-ic { flex-basis: 32px; width: 32px; height: 32px; font-size: .95rem; border-radius: 8px; }
  .rw-kpi-val { font-size: 1.05rem; }
  /* Full screen on a phone: the body scrolls, the header and footer stay. */
  #rwvModal .modal-dialog,
  #hpvModal .modal-dialog { margin: 0; max-width: none; }
}

/* ==========================================================================
   16. Utilities
   ========================================================================== */
.text-title { color: var(--ink-900); }
/* Scoped to .fl-field label since 2026-07-30. A required marker is worth the
   same red wherever it appears - a legend, a card header, a section title. */
.req { color: var(--danger); }
.nowrap { white-space: nowrap; }
.fs-72 { font-size: .72rem; }
.fs-78 { font-size: .78rem; }
.muted { color: var(--muted); }
.faint { color: var(--faint); }
.skeleton {
  background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 37%, #f1f5f9 63%);
  background-size: 400% 100%; animation: sk 1.2s ease infinite; border-radius: 6px; color: transparent;
}
@keyframes sk { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
.empty-state { text-align: center; padding: 2.5rem 1rem; color: var(--faint); }
.empty-state i { font-size: 2.2rem; display: block; margin-bottom: .6rem; opacity: .5; }
.empty-state p { font-size: .85rem; margin: 0; }

@media (max-width: 1199px) { .page-body { padding: 1.25rem; } }
@media (max-width: 991px) {
  .page-body { padding: 1.25rem 1rem; }
  .topbar { padding: .7rem 1rem; }
}
@media (max-width: 767px) {
  .page-body { padding: 1rem .75rem; }
  .topbar { padding: .6rem .75rem; flex-wrap: wrap; row-gap: .4rem; }
  /* ⚠ BASIS 0, NOT auto - THE TOPBAR TRUNCATES ITS CAPTION RATHER THAN GROWING
     A SECOND ROW. This block is the one place .topbar wraps, and with wrap on,
     a flex line breaks on each item's BASE size before any item is allowed to
     shrink - so with `auto` the heading block's natural width (its .page-sub,
     which is the longer of the two lines) decided whether the right-hand
     cluster kept its seat. Measured on Production: at 414 the row was at
     exactly 100% of capacity - 278.45 + 9.6 + 106.77 = 394.8 - so the home
     button's 46px tipped it, and at 375 it had already tipped on its own,
     costing 37px of a sticky bar on the narrowest screen there is (R9). At
     basis 0 the block never forces the break and truncates instead, which is
     what .page-title and .page-sub carry text-truncate FOR.

     ⚠ MASTERS USED TO BE THE EXCEPTION AND NO LONGER IS (2026-08-31). Its
     `:has(.master-pick)` rules gave the right-hand cluster width:100%, which
     wrapped that topbar to two rows whatever the left half was based on -
     measured 92.39px at 375 and 414. The picker moved to a rail on the screen
     (section 15f) and those rules went with it, so Masters is now one row like
     everything else and the rule above governs it too. */
  .topbar > div:first-child { flex: 1 1 0; min-width: 0; }
  .topbar h5, .page-title { font-size: .95rem; }
  .page-sub { font-size: .7rem; }
  .filter-bar { padding: .75rem; }
}
@media (max-width: 575px) {
  .page-body { padding: .75rem .6rem; }
  .section-card { border-radius: 12px; }
  .topbar { padding: .5rem .6rem; }
  .topbar h5, .page-title { font-size: .9rem; }
  .card-header-custom { padding: .75rem .9rem; }
  .modal-dialog { margin: .5rem; max-width: calc(100vw - 1rem); }
  .modal-top .modal-dialog { margin-top: 2vh; }
  .modal-hd { padding: .7rem .9rem; }
  .modal-body { padding: .9rem; }
  .modal-footer { padding: .6rem .9rem; }
  /* Keep the switch and the buttons on one line; wrap only if truly out of room. */
  .modal-footer { flex-wrap: wrap; row-gap: .5rem; }
  .pager { padding: .6rem .8rem; }
}
/* 414 and 375: reclaim every pixel - nothing may overflow (R9) */
@media (max-width: 414px) {
  .page-body { padding: .6rem .5rem; }
  .filter-bar { padding: .6rem; gap: .4rem; }
  .filter-bar .form-control, .filter-bar .form-select { max-width: 100% !important; width: 100%; }
  .sidebar { width: 86vw; max-width: 300px; }
}
@media (max-width: 375px) {
  .topbar h5, .page-title { font-size: .85rem; }
  .table-app { font-size: .8rem; }
  .action-btn { width: 26px; height: 26px; font-size: .75rem; }
  .btn-save { padding: .35rem .9rem; }
}

/* ── First Piece Approval - the template's parameter rows (2026-09-23) ───────
   A grip, the Sr. No., the three fields and a delete button on ONE line from
   768px; below it the fields wrap inside .fp-fields (parameter on its own line,
   Specification and Tolerance side by side) while the grip, number and delete
   stay beside them. The drag itself is bindListDrag() (app.js §10a), which puts
   .opt-dragging on the row it moves - so that class is styled for these rows
   too, not only for the Manage dialogs' .opt-row. */
.fp-row {
  display: flex; align-items: center; gap: .5rem;
  padding: .5rem 0; border-bottom: 1px solid var(--card-border);
}
.fp-row:last-child { border-bottom: none; }
.fp-row.opt-dragging {
  background: var(--accent-wash);
  box-shadow: 0 2px 8px rgb(0 0 0 / .10); border-radius: 6px;
  position: relative; z-index: 2;
}
.fp-row:hover .opt-grip { color: var(--muted); }
.fp-sr { flex: 0 0 22px; text-align: center; font-weight: 700; font-size: .8rem; color: var(--muted); }
.fp-fields { flex: 1 1 auto; min-width: 0; }
/* The + on the last row - the washed indigo of .btn-soft, so it reads as "add"
   beside the red delete on the rows above. ⚠ [hidden] NEEDS ITS OWN RULE:
   .action-btn sets display:inline-flex, and an author display beats the UA's
   [hidden] - the .f-count[hidden] trap. */
.action-btn.act-add { background: var(--accent-tint); color: var(--violet-ink); }
.action-btn.act-add:hover { background: #ddd6fe; }
.fp-row .action-btn[hidden] { display: none; }
@media (max-width: 575.98px) {
  .fp-row { gap: .3rem; }
  .fp-row .opt-grip { width: 26px; }
}

/* ── First Piece Readings - the entry form and its keypad (2026-09-24) ────────
   .fr-param is one parameter: a header (Sr. No., name, Spec / Tol, a save
   state) over N reading cells. ⚠ THE CELLS ARE BUTTONS, NOT INPUTS - so the
   phone keyboard never opens; the keypad (.fr-pad) is fixed to the foot of the
   screen instead, full width on a phone, a 400px card bottom-right from 768.
   While it is open the page gets that much padding at the foot, so the last
   parameter can always be scrolled clear of it (--fr-pad-h, set by the bundle
   from the pad's own height). */
@media (min-width: 768px) {
  .fr-facts { display: grid; grid-template-columns: 1fr 1fr; }
  .fr-facts .rwv-f:nth-child(2) { border-top: none; }
  .fr-facts .rwv-f:nth-child(even) { border-left: 1px solid var(--line); }
}

.fr-param { border: 1px solid var(--card-border); border-radius: var(--radius-sm); padding: .55rem .6rem .65rem; }
.fr-param + .fr-param { margin-top: .6rem; }
.fr-param-hd { display: flex; align-items: flex-start; gap: .55rem; margin-bottom: .5rem; }
.fr-sr { flex: 0 0 auto; font-weight: 700; font-size: .78rem; color: var(--muted); line-height: 1.5; }
.fr-param-t { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.fr-name { font-weight: 700; font-size: .88rem; color: var(--ink-900); overflow-wrap: anywhere; }
.fr-tag { font-size: .66rem; font-weight: 600; color: var(--info-ink); background: var(--info-bg); border-radius: 999px; padding: .05rem .45rem; margin-left: .25rem; white-space: nowrap; }
.fr-spec { font-size: .74rem; color: var(--muted); overflow-wrap: anywhere; }
.fr-spec b { font-weight: 600; color: var(--body, #334155); }
.fr-dot { margin: 0 .25rem; color: var(--faint); }
.fr-state { flex: 0 0 auto; font-size: .7rem; }
.fr-st { font-weight: 600; color: var(--muted); white-space: nowrap; }
.fr-st-ok { color: var(--ok-ink); }
.fr-st-dirty { color: var(--warn-ink, #b45309); }
.fr-st-err { color: var(--danger-ink); }

/* Five cells to a row at every width - the paper's own five pieces - wrapping
   to a second row only when Pieces To Check is above five. */
.fr-cells { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: .4rem; }
.fr-cell {
  position: relative; display: flex; align-items: center; justify-content: center;
  min-height: 46px; padding: .9rem .2rem .3rem;
  background: #fff; border: 1.5px solid var(--field-border); border-radius: var(--radius-field);
  font-size: .92rem; font-weight: 700; color: var(--ink-900); font-variant-numeric: tabular-nums;
  cursor: pointer; white-space: nowrap; overflow: hidden;
}
.fr-cell-no { position: absolute; top: 2px; left: 6px; font-size: .62rem; font-weight: 600; color: var(--faint); }
/* ⚠ Measured at 375: five cells are 56px each, and "-12.345" or "Not OK" at
   .92rem overran by 2px. A smaller face and a tighter gap below 576 keep the
   widest reading inside its box. */
@media (max-width: 575.98px) {
  .fr-cells { gap: .3rem; }
  .fr-cell { font-size: .8rem; padding-left: 0; padding-right: 0; }
}
.fr-cell.is-set { background: var(--accent-wash); }
.fr-cell.is-ok  { background: var(--ok-bg); color: var(--ok-ink); }
.fr-cell.is-nok { background: var(--danger-bg); color: var(--danger-ink); }
/* Sign colour on a numeric reading - THIS SCREEN ONLY (the user's
   instruction, 2026-09-24): red below zero, green above, zero plain. Text
   colour only; the box keeps its wash. Not on the printed sheet. */
.fr-cell.is-neg .fr-cell-v, .fr-pad-val.is-neg { color: var(--danger-ink); }
.fr-cell.is-pos .fr-cell-v, .fr-pad-val.is-pos { color: var(--ok-ink); }
.fr-cell.is-active { border-color: var(--accent); box-shadow: 0 0 0 3px rgb(79 70 229 / .18); }
.fr-cell:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* A parameter's own OK / Not OK (2026-09-26), under its cells: a label and the
   same two-button control the overall result uses, so the two read alike. */
.fr-res { display: flex; align-items: center; gap: .6rem; margin-top: .5rem; }
.fr-res-lbl { flex: 0 0 auto; font-size: .74rem; font-weight: 600; color: var(--muted); }
.fr-res .fr-result { max-width: 240px; }
.fr-res .calc-seg-btn { min-height: 38px; padding: .4rem .6rem; }

.fr-result { max-width: 320px; }
.fr-result .calc-seg-btn[data-val="ok"][aria-pressed="true"]     { background: var(--ok-bg); color: var(--ok-ink); }
.fr-result .calc-seg-btn[data-val="not_ok"][aria-pressed="true"] { background: var(--danger-bg); color: var(--danger-ink); }

body.fr-pad-open .page-body { padding-bottom: calc(var(--fr-pad-h, 300px) + 1rem); }
.fr-pad {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 1035;
  background: #fff; border-top: 1px solid var(--card-border);
  box-shadow: 0 -6px 24px rgb(15 23 42 / .14);
  padding: .5rem .6rem calc(.6rem + env(safe-area-inset-bottom, 0px));
}
.fr-pad[hidden] { display: none; }
@media (min-width: 768px) {
  .fr-pad { left: auto; right: 16px; bottom: 16px; width: 400px; border: 1px solid var(--card-border); border-radius: var(--radius); }
}
.fr-pad-hd { display: flex; align-items: center; gap: .5rem; margin-bottom: .45rem; }
.fr-pad-who { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.fr-pad-name { font-weight: 700; font-size: .82rem; color: var(--ink-900); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fr-pad-piece { font-size: .7rem; color: var(--muted); }
.fr-pad-val {
  flex: 0 0 auto; min-width: 96px; text-align: right; font-size: 1.25rem; font-weight: 700;
  color: var(--accent-dark); font-variant-numeric: tabular-nums;
}
.fr-pad-keys { display: grid; grid-template-columns: repeat(4, 1fr); gap: .35rem; }
.fr-pad-keys[hidden] { display: none; }
.fr-pad-tog { grid-template-columns: 1fr 1fr; }
.fr-key {
  min-height: 46px; border: 1px solid var(--card-border); border-radius: var(--radius-field);
  background: #f8fafc; font-size: 1.1rem; font-weight: 600; color: var(--ink-900);
  display: flex; align-items: center; justify-content: center; gap: .3rem;
  cursor: pointer; user-select: none; touch-action: manipulation;
}
.fr-key:active { background: var(--accent-tint); }
.fr-key-fn { font-size: .82rem; background: #eef2f7; color: var(--ink-700); }
.fr-key-next { grid-column: span 2; background: var(--accent); border-color: var(--accent); color: #fff; font-size: .9rem; }
.fr-key-next:active { background: var(--accent-dark); }
.fr-key-ok  { background: var(--ok-bg); color: var(--ok-ink); border-color: #bbf7d0; }
.fr-key-nok { background: var(--danger-bg); color: var(--danger-ink); border-color: #fecaca; }

/* ==========================================================================
   15h. Batch Tracking - Batch Movement (.bt-*) and Final Visual Inspection (.fvi-*)
   ==========================================================================
   (2026-09-25) Batch Movement is a DESKTOP screen - a grid with a Batch Report
   dialog; Final Visual Inspection is a PHONE screen - cards and a one-column
   form. Both borrow what exists: the row dialog's .rwv-* facts and footer
   (§15g), First Piece Approval's .fp-row fast-entry lines, .status-badge.
   Light mode only (R7); no green BUTTON (R12) - green here is only a figure. */

/* A row tap opens the Batch Report - a thumb aims at a row, not an icon. */
#bmBody tr.rw-row,
#fvBody tr.rw-row { cursor: pointer; }   /* the QC list opens an entry the same way */

/* The Batch Report's footer - §15g's equal-width pair. */
#rpModal .rwv-foot { display: flex; flex-wrap: nowrap; gap: .5rem; }
#rpModal .rwv-foot > .btn {
  flex: 1 1 0; min-width: 0; margin: 0; padding: .4rem .5rem;
  border: 1px solid transparent;
}
#rpModal .rwv-foot > .btn-light { background: #fff; border-color: var(--card-border); }
/* Close Batch / Reopen Batch show one at a time; .btn's inline-block would beat
   the UA [hidden] rule (the .f-count trap). */
#rpModal .rwv-foot > .btn[hidden] { display: none; }

/* The seven stage tiles, in flow order. A stage holding nothing fades, so the
   eye lands on where the order actually is. */
.bt-stages { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: .5rem; }
.bt-stage {
  background: var(--card-bg); border: 1px solid var(--card-border);
  border-radius: var(--radius-sm); padding: .5rem .6rem; min-width: 0;
}
.bt-stage.is-zero { background: #f8fafc; }
.bt-stage.is-zero .bt-stage-val { color: var(--faint); }
.bt-stage.is-ok  { border-color: #bbf7d0; background: var(--ok-bg); }
.bt-stage.is-ok .bt-stage-val { color: var(--ok-ink); }
.bt-stage.is-bad { border-color: #fecaca; background: var(--danger-bg); }
.bt-stage.is-bad .bt-stage-val { color: var(--danger-ink); }
.bt-stage.is-info { border-color: #bae6fd; background: var(--info-bg); }
.bt-stage.is-info .bt-stage-val { color: var(--info-ink); }
.bt-stage.is-zero.is-ok, .bt-stage.is-zero.is-bad, .bt-stage.is-zero.is-info { background: #f8fafc; border-color: var(--card-border); }
/* A dispatched figure in the grid - the info ink, not Bootstrap's own blue. */
.bt-disp { color: var(--info-ink); }
.bt-stage-lbl { font-size: .72rem; font-weight: 600; color: var(--muted); line-height: 1.25; }
.bt-stage-val { font-size: 1.15rem; font-weight: 700; color: var(--ink-900); line-height: 1.3; font-variant-numeric: tabular-nums; }
.bt-stage-v {
  display: flex; justify-content: space-between; align-items: flex-start; gap: .4rem;
  font-size: .7rem; color: var(--muted); margin-top: .15rem; min-width: 0;
}
.bt-stage-v span { min-width: 0; overflow-wrap: anywhere; line-height: 1.3; }   /* wraps - half a vendor name names nobody */
.bt-stage-v b { flex: 0 0 auto; font-weight: 600; color: var(--ink-700); }

/* Order = the sum of the stages. Printed so a reader can see it holds; red
   only if it ever does not, which the ledger makes impossible by construction. */
.bt-recon {
  margin-top: .5rem; padding: .4rem .7rem; font-size: .78rem; color: var(--muted);
  background: var(--accent-wash); border: 1px solid var(--card-border); border-radius: var(--radius-sm);
}
.bt-recon b { color: var(--ink-900); }
.bt-recon.is-bad { background: var(--danger-bg); color: var(--danger-ink); border-color: #fecaca; }

.bt-qc { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; }
.bt-chip {
  display: inline-flex; align-items: baseline; gap: .3rem; white-space: nowrap;
  background: var(--accent-wash); border: 1px solid var(--card-border); border-radius: 999px;
  padding: .15rem .6rem; font-size: .74rem; color: var(--muted);
}
.bt-chip b { color: var(--ink-900); font-weight: 700; }
.bt-hist { border: 1px solid var(--card-border); border-radius: var(--radius-sm); }

/* ── Final Visual Inspection - the list is a TABLE (2026-09-25, "need table view") ──
   It replaced the phone-first cards it first shipped with. The table is the
   Visual & Quantity Record's grid (.table-nowrap .table-pin, .cell-clip on
   Rejections) and needs no rule of its own beyond the clickable row above.
   Only the entry form keeps its .fvi-* rules. */

/* Three figures, label over value - the entry form's Checking · Rejected · OK Qty. */
.fvi-sum { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .4rem; }
.fvi-sum > div { min-width: 0; display: flex; flex-direction: column; }
.fvi-sum span { font-size: .68rem; font-weight: 600; color: var(--muted); }

/* ── The entry form ─────────────────────────────────────────────────────────── */
.fvi-rej-hd {
  font-size: .78rem; font-weight: 700; color: var(--ink-900);
  margin: 1rem 0 .25rem; padding-top: .8rem; border-top: 1px solid var(--line);
}
.fvi-empty {
  font-size: .8rem; color: var(--muted); background: #f8fafc;
  border: 1px dashed var(--card-border); border-radius: var(--radius-sm); padding: .6rem .75rem;
}
.fvi-empty[hidden] { display: none; }
.fvi-sum {
  margin-top: .75rem; background: var(--accent-wash); border: 1px solid var(--card-border);
  border-radius: var(--radius-sm); padding: .5rem .75rem;
}
.fvi-sum b { font-size: 1.1rem; font-weight: 700; color: var(--ink-900); font-variant-numeric: tabular-nums; }
/* The Rejected and OK figures keep Bootstrap's text colour - it is set with
   !important and would otherwise lose to the rule above only by accident. */
.fvi-sum b.text-danger  { color: var(--danger-ink) !important; }
.fvi-sum b.text-success { color: var(--ok-ink) !important; }

/* From 768 the stock facts read in two columns (First Piece Readings' shape);
   from 992 the rejection lines and the summary stop at a readable width - a
   type and a quantity stretched across 1300px is a line nobody can scan (R9). */
@media (min-width: 768px) {
  #fvInfo { display: grid; grid-template-columns: 1fr 1fr; }
  #fvInfo .rwv-f:nth-child(2) { border-top: none; }
  #fvInfo .rwv-f:nth-child(even) { border-left: 1px solid var(--line); }
}
@media (min-width: 992px) {
  #fvRows, #fvNoTypes, .fvi-sum { max-width: 760px; }
}
@media (max-width: 575.98px) {
  .bt-stages { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media print {
  .sidebar, .sidebar-overlay, .topbar, .pager, .filter-bar, .action-btn, .btn { display: none !important; }
  .main { margin-left: 0 !important; }
  .page-body { padding: 0 !important; }
  .section-card { border: none; box-shadow: none; }
}
