:root {
  color-scheme: light;
  --surface-1: #fcfcfb;
  --surface-2: #ffffff;
  --page-plane: #f9f9f7;
  --text-primary: #0b0b0b;
  --text-secondary: #52514e;
  --text-muted: #898781;
  --gridline: #e1e0d9;
  --baseline: #c3c2b7;
  --border: rgba(11, 11, 11, 0.10);
  --border-strong: rgba(11, 11, 11, 0.16);

  --series-1: #2a78d6;
  --series-1-fill: rgba(42, 120, 214, 0.12);
  --series-2: #eb6834;
  --series-2-fill: rgba(235, 104, 52, 0.12);
  --series-3: #1baf7a;
  --series-3-fill: rgba(27, 175, 122, 0.12);
  --series-4: #eda100;
  --series-4-fill: rgba(237, 161, 0, 0.12);

  --status-good: #0ca30c;
  --status-warning: #fab219;
  --status-serious: #ec835a;
  --status-critical: #d03b3b;
  --status-neutral: #898781;

  --status-good-bg: rgba(12, 163, 12, 0.10);
  --status-warning-bg: rgba(250, 178, 25, 0.16);
  --status-serious-bg: rgba(236, 131, 90, 0.14);
  --status-critical-bg: rgba(208, 59, 59, 0.10);
  --status-neutral-bg: rgba(137, 135, 129, 0.12);

  --shadow-card: 0 1px 2px rgba(11, 11, 11, 0.04), 0 4px 16px rgba(11, 11, 11, 0.04);
  --shadow-modal: 0 8px 24px rgba(11, 11, 11, 0.12), 0 24px 64px rgba(11, 11, 11, 0.16);
  --radius: 10px;
}

@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) {
    color-scheme: dark;
    --surface-1: #1a1a19;
    --surface-2: #202020;
    --page-plane: #0d0d0d;
    --text-primary: #ffffff;
    --text-secondary: #c3c2b7;
    --text-muted: #898781;
    --gridline: #2c2c2a;
    --baseline: #383835;
    --border: rgba(255, 255, 255, 0.10);
    --border-strong: rgba(255, 255, 255, 0.16);

    --series-1: #3987e5;
    --series-1-fill: rgba(57, 135, 229, 0.16);
    --series-2: #d95926;
    --series-2-fill: rgba(217, 89, 38, 0.16);
    --series-3: #199e70;
    --series-3-fill: rgba(25, 158, 112, 0.16);
    --series-4: #c98500;
    --series-4-fill: rgba(201, 133, 0, 0.16);

    --status-good: #0ca30c;
    --status-warning: #fab219;
    --status-serious: #ec835a;
    --status-critical: #e66767;
    --status-neutral: #898781;

    --status-good-bg: rgba(12, 163, 12, 0.16);
    --status-warning-bg: rgba(250, 178, 25, 0.14);
    --status-serious-bg: rgba(236, 131, 90, 0.16);
    --status-critical-bg: rgba(230, 103, 103, 0.16);
    --status-neutral-bg: rgba(137, 135, 129, 0.16);

    --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.2), 0 4px 16px rgba(0, 0, 0, 0.24);
    --shadow-modal: 0 8px 24px rgba(0, 0, 0, 0.4), 0 24px 64px rgba(0, 0, 0, 0.5);
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --surface-1: #1a1a19;
  --surface-2: #202020;
  --page-plane: #0d0d0d;
  --text-primary: #ffffff;
  --text-secondary: #c3c2b7;
  --text-muted: #898781;
  --gridline: #2c2c2a;
  --baseline: #383835;
  --border: rgba(255, 255, 255, 0.10);
  --border-strong: rgba(255, 255, 255, 0.16);

  --series-1: #3987e5;
  --series-1-fill: rgba(57, 135, 229, 0.16);
  --series-2: #d95926;
  --series-2-fill: rgba(217, 89, 38, 0.16);
  --series-3: #199e70;
  --series-3-fill: rgba(25, 158, 112, 0.16);
  --series-4: #c98500;
  --series-4-fill: rgba(201, 133, 0, 0.16);

  --status-good: #0ca30c;
  --status-warning: #fab219;
  --status-serious: #ec835a;
  --status-critical: #e66767;
  --status-neutral: #898781;

  --status-good-bg: rgba(12, 163, 12, 0.16);
  --status-warning-bg: rgba(250, 178, 25, 0.14);
  --status-serious-bg: rgba(236, 131, 90, 0.16);
  --status-critical-bg: rgba(230, 103, 103, 0.16);
  --status-neutral-bg: rgba(137, 135, 129, 0.16);

  --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.2), 0 4px 16px rgba(0, 0, 0, 0.24);
  --shadow-modal: 0 8px 24px rgba(0, 0, 0, 0.4), 0 24px 64px rgba(0, 0, 0, 0.5);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--page-plane);
  color: var(--text-primary);
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: 14px;
  -webkit-font-smoothing: antialiased;
}

/* ---------- App shell: top nav + main area ---------- */
/* A fluid max-width (not a hard breakpoint-driven cap) so the page uses
   more of a wide monitor's width and shrinks gracefully on a laptop --
   clamp() scales the side padding smoothly instead of jumping at a fixed
   pixel boundary. Shared by the topbar, page header, and content so
   everything lines up under the same left/right edges at any width. */
.app-layout { min-height: 100vh; }
.shell-width { max-width: 1800px; margin: 0 auto; padding-left: clamp(16px, 3vw, 40px); padding-right: clamp(16px, 3vw, 40px); }

.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--surface-1);
  border-bottom: 1px solid var(--border);
}

.topbar-inner {
  max-width: 1800px;
  margin: 0 auto;
  padding: 10px clamp(16px, 3vw, 40px);
  display: flex;
  align-items: center;
  gap: 16px;
  /* Always one row above the mobile breakpoint (2026-09-09: "always fit
     onto one line") -- brand/nav/header-right no longer wrap onto
     separate lines relative to each other; .topbar-nav below is the one
     piece that can still run out of room, and it scrolls horizontally
     in that case instead of wrapping. */
  flex-wrap: nowrap;
}

.topbar-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  color: inherit;
  flex: none;
}
/* The real NURA logo (2026-09-09, downloaded from nurausa.com), fetched
   as-is and shown at native aspect ratio (h=26px) rather than forced into
   the old 30px icon-only square, since it's a full icon+wordmark lockup,
   not a standalone mark. Dark mode inverts it to white via a filter, not
   currentColor -- this is a real downloaded asset (charcoal fill baked
   into the file), not a hand-drawn shape we can recolor that way. */
.brand-logo-img { height: 26px; width: auto; flex: none; display: block; }
@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) .brand-logo-img { filter: brightness(0) invert(1); }
}
:root[data-theme="dark"] .brand-logo-img { filter: brightness(0) invert(1); }
.brand-sub { font-size: 11px; color: var(--text-muted); }

.topbar-nav {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 2px;
  flex: 1;
  min-width: 0;
  /* Scrolls horizontally rather than wrapping to a second row if it ever
     genuinely can't fit -- a thin/native scrollbar beats an awkward
     2-row nav (2026-09-09). */
  overflow-x: auto;
  scrollbar-width: thin;
}

.nav-link {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 8px;
  border-radius: 8px;
  color: var(--text-secondary);
  text-decoration: none;
  font-size: 12.5px;
  font-weight: 550;
  white-space: nowrap;
  flex: none;
  transition: background 0.12s ease, color 0.12s ease;
}
.nav-link svg { flex: none; opacity: 0.85; }
.nav-link:hover { background: var(--gridline); color: var(--text-primary); }
.nav-link.active { background: var(--status-good-bg); color: var(--series-1); }
.nav-link.active svg { opacity: 1; }
@media (prefers-color-scheme: dark) { :root:where(:not([data-theme="light"])) .nav-link.active { background: rgba(57,135,229,0.16); color: var(--series-1); } }
:root[data-theme="dark"] .nav-link.active { background: rgba(57,135,229,0.16); color: var(--series-1); }

.main-area { min-width: 0; }

.app-shell {
  max-width: 1800px;
  margin: 0 auto;
  padding: 20px clamp(16px, 3vw, 40px) 64px;
}

/* ---------- Page header ---------- */

.page-header {
  max-width: 1800px;
  margin: 0 auto;
  padding: 20px clamp(16px, 3vw, 40px) 0;
}

.page-header h1 {
  font-size: 20px;
  font-weight: 650;
  margin: 0;
  letter-spacing: -0.01em;
}

.page-header .subtitle {
  color: var(--text-secondary);
  font-size: 13px;
  margin-top: 2px;
}

.header-right {
  display: flex;
  align-items: center;
  gap: 12px;
  flex: none;
  margin-left: auto;
}

/* Below this, brand + nav + refresh controls no longer fit comfortably on
   one row -- stack them as three clean full-width rows instead of letting
   the nav wrap link-by-link and visually interleave with the brand/
   buttons (align-items:center centers those against the now-tall wrapped
   nav column otherwise, which reads as broken even though nothing
   actually overlaps). */
@media (max-width: 860px) {
  .topbar-inner { flex-direction: column; align-items: stretch; row-gap: 10px; }
  .topbar-nav { flex: none; }
  .header-right { margin-left: 0; justify-content: space-between; }
}

.refresh-meta {
  text-align: right;
  font-size: 12px;
  color: var(--text-muted);
  line-height: 1.5;
}

.btn {
  appearance: none;
  border: 1px solid var(--border-strong);
  background: var(--surface-2);
  color: var(--text-primary);
  border-radius: 8px;
  padding: 8px 14px;
  font-size: 13px;
  font-weight: 550;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  transition: background 0.12s ease, border-color 0.12s ease;
}
.btn:hover { background: var(--gridline); }
.btn:disabled { opacity: 0.55; cursor: default; }
.btn-primary {
  background: var(--series-1);
  border-color: var(--series-1);
  color: #fff;
}
.btn-primary:hover { filter: brightness(1.06); }

.icon-btn {
  width: 32px;
  height: 32px;
  border-radius: 8px;
  border: 1px solid var(--border-strong);
  background: var(--surface-2);
  color: var(--text-secondary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.icon-btn:hover { background: var(--gridline); }

.spin { animation: spin 0.9s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------- KPI tiles ---------- */

.kpi-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 12px;
  margin-bottom: 20px;
}

.kpi-tile {
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px 16px;
  box-shadow: var(--shadow-card);
  cursor: pointer;
  transition: border-color 0.12s ease, transform 0.12s ease;
}
.kpi-tile:hover { border-color: var(--border-strong); }
.kpi-tile.active { border-color: var(--series-1); box-shadow: 0 0 0 1px var(--series-1); }
.kpi-tile.no-click { cursor: default; }
.kpi-tile.no-click:hover { border-color: var(--border); }

.kpi-label {
  font-size: 12px;
  color: var(--text-secondary);
  font-weight: 550;
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 6px;
}

.kpi-value {
  font-size: 24px;
  font-weight: 650;
  letter-spacing: -0.02em;
  font-variant-numeric: proportional-nums;
}

.kpi-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: none;
}

/* ---------- Toolbar ---------- */

.toolbar {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 14px;
  flex-wrap: wrap;
}

.search-input {
  flex: 1;
  min-width: 220px;
  border: 1px solid var(--border-strong);
  background: var(--surface-1);
  color: var(--text-primary);
  border-radius: 8px;
  padding: 8px 12px;
  font-size: 13px;
}
.search-input:focus { outline: 2px solid var(--series-1); outline-offset: -1px; }

.select-input {
  border: 1px solid var(--border-strong);
  background: var(--surface-1);
  color: var(--text-primary);
  border-radius: 8px;
  padding: 8px 10px;
  font-size: 13px;
}

/* Multi-select filter widget (Common.initMultiSelect, 2026-09-02) --
   a .select-input-styled button that opens a checkbox panel, used for
   every status/category filter app-wide. */
.multi-select { position: relative; display: inline-block; }
/* width:100% (2026-09-15, real reported bug: clicking the visible GAP
   next to a filter -- e.g. between the status and category dropdowns in
   the Action Items modal -- didn't close an open panel, even though it
   plainly looks like "outside" the widget). Several call sites give the
   .multi-select CONTAINER itself flex:1 (Action Items modal, Scenario
   Planning, Shipping Calendar's status/category filters -- see their
   own inline styles) so two filters split a toolbar row evenly; the
   container correctly stretches to fill that space, but this BUTTON
   didn't, since inline-flex/min-width alone only sizes it to its own
   content -- leaving a wide invisible strip that's still genuinely
   INSIDE the .multi-select container (so common.js's outside-click
   listener, which checks closest(".multi-select"), correctly treated a
   click there as "inside" and left the panel open). Stretching the
   button to fill its container makes the visible surface match the real
   hit-box exactly, so there's no gap left to be confused by. A no-op
   change anywhere .multi-select ISN'T flex-stretched (plain toolbars,
   sidebars) -- there, the container is already exactly as wide as this
   button's own content, so 100% of it is the same size as before. */
.multi-select-btn {
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  min-width: 130px; width: 100%; justify-content: space-between;
}
.multi-select-btn-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.multi-select-caret { opacity: 0.6; flex: none; }
.multi-select-panel {
  display: none;
  position: absolute; top: calc(100% + 4px); left: 0; z-index: 20;
  min-width: 100%; width: max-content; max-width: 260px;
  max-height: 260px; overflow-y: auto;
  background: var(--surface-1); border: 1px solid var(--border-strong);
  border-radius: 8px; box-shadow: var(--shadow-card);
  padding: 6px;
}
.multi-select-panel.open { display: block; }
/* "Select all"/"Clear all" (2026-09-24, opt-in via initMultiSelect's
   showSelectAll param) -- sticky so it stays reachable at the top of the
   panel's own scroll area instead of scrolling away above a long
   checkbox list, matching .multi-select-panel's own padding/background
   so it reads as one continuous panel, not a separate floating bar. */
.multi-select-actions {
  position: sticky; top: -6px; margin: -6px -6px 4px; padding: 6px 8px;
  background: var(--surface-1); z-index: 1;
  display: flex; gap: 12px; border-bottom: 1px solid var(--gridline);
}
.multi-select-actions button {
  background: none; border: none; padding: 0; margin: 0 0 4px;
  font-size: 12px; font-weight: 650; color: var(--series-1); cursor: pointer;
}
.multi-select-actions button:hover { text-decoration: underline; }

/* Scenario Planning's "% of Demand Fulfilled" column, header + cells,
   while it's showing numbers scoped to the current customer filter
   instead of each vendor's static, part-wide share (2026-09-24, explicit
   request: "shade this column too so people know it changes based on the
   customers selected"). Reuses the same blue accent as every other
   "this reflects your current selection" affordance in the app (active
   nav link, focused vendor row, etc.) rather than introducing a new
   color. */
.vendor-share-scoped { background: var(--series-1-fill); }
th.vendor-share-scoped { color: var(--series-1); }
.multi-select-option {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 8px; border-radius: 6px; cursor: pointer;
  font-size: 13px; color: var(--text-primary);
}
.multi-select-option:hover { background: var(--gridline); }
.multi-select-option input { flex: none; cursor: pointer; }

.result-count {
  font-size: 12px;
  color: var(--text-muted);
  white-space: nowrap;
}

/* ---------- Table ---------- */

.table-card {
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  overflow: hidden;
}

.table-scroll {
  overflow-x: auto;
}

/* For a long, wide table (e.g. a customer with many open order lines):
   cap the height and scroll vertically INSIDE the pane, so the
   horizontal scrollbar sits at the bottom of the visible area and is
   reachable without scrolling the whole page down first. The sticky
   thead (.data-table thead th) then pins to the top of this pane
   instead of the page. */
.table-scroll.table-scroll-tall {
  overflow-y: auto;
  max-height: 72vh;
}

table.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
  min-width: 980px;
}

.data-table thead th {
  text-align: left;
  font-weight: 600;
  color: var(--text-secondary);
  padding: 10px 14px;
  border-bottom: 1px solid var(--gridline);
  white-space: nowrap;
  cursor: pointer;
  user-select: none;
  position: sticky;
  top: 0;
  background: var(--surface-1);
}
.data-table thead th:hover { color: var(--text-primary); }
.data-table thead th.num { text-align: right; }
.data-table thead th .sort-arrow { opacity: 0.4; font-size: 10px; margin-left: 4px; }
.data-table thead th.sorted .sort-arrow { opacity: 1; color: var(--series-1); }

.data-table tbody td {
  padding: 9px 14px;
  border-bottom: 1px solid var(--gridline);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.data-table tbody td.num { text-align: right; }
.data-table tbody tr { cursor: pointer; }
.data-table tbody tr:hover td { background: var(--gridline); }
.data-table tbody tr:last-child td { border-bottom: none; }

.part-num { font-weight: 600; }
.sparkline { display: block; vertical-align: middle; }

/* Visually groups related columns in a wide operational grid (SO demand /
   PO supply / inventory / status) so the eye can parse it without
   counting columns -- a heavier left rule at each group's first column. */
.data-table-grouped th.col-group,
.data-table-grouped td.col-group {
  border-left: 1px solid var(--border-strong);
}
.data-table-grouped thead th { line-height: 1.3; vertical-align: bottom; padding-bottom: 8px; }

/* Secondary figure folded under a cell's main number (e.g. Positions by
   Part's line-count/committed detail under its qty) -- condenses what
   used to be its own column into one line of muted context instead,
   inheriting the cell's own text-align so it stays lined up under the
   number above it. */
.cell-sub { display: block; font-size: 10.5px; color: var(--text-muted); margin-top: 1px; font-weight: 400; }

/* Positions by Part specifically: tighter padding and a narrower
   Product Description cap than the shared .data-table/.part-desc
   defaults, so the operational columns (SO demand/PO supply/inventory/
   status/reorder point) fit without side-scrolling on a normal laptop
   screen -- scoped to this table's id so it doesn't affect any other
   page's tables. */
#priorityTable.data-table th,
#priorityTable.data-table td { padding-left: 9px; padding-right: 9px; }
#priorityTable .part-desc { max-width: 150px; }
/* Part Number cap (2026-09-21) -- "PARTNUM (Factory)" runs much longer
   than a bare part number for parts with one on file, which pushed every
   later column rightward. Capped and truncated with an ellipsis (title
   attribute on the inner span carries the full text on hover) rather
   than letting the column grow -- keeping the operational columns to its
   right in a consistent place matters more here than never clipping a
   long factory name. Same inline-block-span-inside-a-td shape as
   .part-desc just above, not applied to .part-num directly since that
   class sits on the <td> itself on other pages/tables that don't need
   truncation (display:inline-block would break a bare <td>'s layout). */
#priorityTable .part-num-truncate {
  display: inline-block;
  max-width: 150px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}

/* Net Status's "Why" -- same plain-language explainStockStatus() text
   Action Plan shows as its own always-visible column, squeezed into the
   Net Status cell instead of a 10th column so the table doesn't need to
   widen. Wraps (vertically, never horizontally) within a capped width so
   a long sentence grows the row, not the table. min-width widened a
   little (2026-09-21) at Part Number/Product Description's expense
   (both narrowed above) so the table still fits without side-scrolling. */
#priorityTable td.status-cell { white-space: normal; vertical-align: top; }
#priorityTable .status-why {
  margin-top: 4px; font-size: 11px; line-height: 1.35; color: var(--text-muted);
  min-width: 168px; max-width: 260px;
}

/* Customers page's own open-orders table (2026-09-22, explicit request:
   remove the Suggested Next Step column, then make everything else fit
   without side-scrolling) -- same tighter-padding + truncated Part#/
   Description approach as Positions by Part's #priorityTable above, tuned
   to this table's own column set. table.data-table's shared min-width:
   980px (sized for the widest table on the platform) would otherwise
   force this narrower one to stay that wide regardless of how little
   room its own columns actually need, so it's overridden to let real
   content decide.
   table-layout: fixed + explicit per-column % widths (2026-09-23 follow-
   up, explicit request: "make the part # column a little thicker and the
   late if ordered now column thinner... there is a big gap between the
   order status column and the late if ordered now column") -- the
   browser's default AUTO table layout hands leftover width to whichever
   columns have the most inherent "flexibility" rather than what actually
   makes sense to read, which is exactly what caused that gap: Order
   Status (just a short badge) and Late if Ordered Now were both
   ballooning to ~150-195px on a wide screen while Part # stayed pinned at
   its truncation cap. Explicit widths make every column's share
   deliberate and stable across viewport sizes instead of guesswork. */
#custLinesTable.data-table { min-width: 0; table-layout: fixed; }
#custLinesTable.data-table th,
#custLinesTable.data-table td { padding-left: 9px; padding-right: 9px; }
#custLinesTable thead th:nth-child(1) { width: 9%; }  /* SO # */
#custLinesTable thead th:nth-child(2) { width: 17%; } /* Part # -- widened */
#custLinesTable thead th:nth-child(3) { width: 17%; } /* Description -- widened (2026-09-23 follow-up) */
#custLinesTable thead th:nth-child(4) { width: 9%; }  /* Qty Open */
#custLinesTable thead th:nth-child(5) { width: 14%; } /* Actual Ship Date */
#custLinesTable thead th:nth-child(6) { width: 11%; } /* Order Status -- wide enough its own header label fits one line, doesn't wrap */
#custLinesTable thead th:nth-child(7) { width: 12%; } /* Late if Ordered Now */
#custLinesTable thead th:nth-child(8) { width: 11%; } /* Calendar/Scenario links */
/* Order Status/Late if Ordered Now's own header LABEL no longer fits
   the shared .data-table thead th's white-space: nowrap once those
   columns were narrowed above -- it was overflowing invisibly behind
   the next header (2026-09-23, caught live: the sort arrow appeared to
   vanish, it was actually just clipped under Late if Ordered Now's
   header). Wrapping to two lines here instead of shortening the labels. */
#custLinesTable thead th { white-space: normal; line-height: 1.25; }
/* SO # cell (2026-09-23, caught live: "28431-blanket" + its "Entered"
   status badge is far longer than a plain 5-digit SO number and doesn't
   fit the base .data-table tbody td's white-space: nowrap at this
   column's now-tighter width -- it was visually bleeding straight into
   Part #'s text with no gap at all, not just running a little close).
   Wraps the status badge onto its own line under the SO # instead of
   forcing it to fit alongside on one line, same fix already applied to
   Late if Ordered Now above. */
#custLinesTable td:nth-child(1) { white-space: normal; }
/* Truncation caps below are now relative (100% of the fixed column's own
   width, via the TH percentages above) instead of a hardcoded px value --
   whatever room a column actually gets is what governs where the
   ellipsis kicks in, so these never need re-tuning in lockstep with the
   widths above again. */
#custLinesTable .part-desc { max-width: 100%; }
#custLinesTable .part-num-truncate {
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}
/* The Calendar/Scenario link pills stack instead of sitting side by side
   -- half the width of the old last column for about the same total
   height, since each pill is short and there's no reason both need to
   fit on one line. */
#custLinesTable .row-links {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
}

/* ---------- Dashboard: hero strip, action items, KPI sections ---------- */

.hero-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 14px;
  margin-bottom: 22px;
}
.hero-tile {
  display: flex; align-items: flex-start; gap: 12px;
  background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-card); padding: 16px 18px;
  text-decoration: none; color: inherit;
  transition: border-color 0.12s, transform 0.12s;
}
.hero-tile:hover { border-color: var(--series-1); transform: translateY(-1px); }
.hero-tile-icon {
  flex: none; width: 38px; height: 38px; border-radius: 10px;
  display: flex; align-items: center; justify-content: center;
}
.hero-tile-body { min-width: 0; flex: 1; }
.hero-tile-label { font-size: 12px; color: var(--text-secondary); font-weight: 600; margin-bottom: 4px; }
.hero-tile-value { font-size: 24px; font-weight: 700; letter-spacing: -0.01em; font-variant-numeric: tabular-nums; line-height: 1.15; }
.hero-tile-sub { font-size: 11.5px; color: var(--text-muted); margin-top: 5px; display: flex; align-items: center; gap: 6px; }

.dash-section { margin-bottom: 26px; }
.dash-section:last-child { margin-bottom: 0; }
.dash-section-header { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
.dash-section-title { font-size: 14px; font-weight: 700; }
.dash-section-sub { font-size: 12px; color: var(--text-muted); margin: 0 0 12px; }

/* Action Items -- the first thing a user should read on open, so it runs
   full width (not squeezed into a narrow sidebar) and each item is a
   real, readable explanation (Common.explainStockStatus's own sentence),
   not just a name and a number. Card tint follows the same
   good/warning/critical status colors used everywhere else in the app
   (reuses .gap-note's box, see below) -- Out of Stock and an already-
   Overdue order get the more alarming critical tint, a real-but-
   not-yet-late Reorder Now gets the lighter warning tint. */
.action-items-panel {
  background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-card); padding: 18px 20px; margin-bottom: 22px;
}
.action-items-header {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 16px; margin-bottom: 14px;
}
/* flex-wrap (removed 2026-09-14) let the Expand button drop onto its own
   wrapped line whenever the summary text ran long enough to need it --
   and justify-content: space-between has nothing to push a single
   wrapped item away from, so it landed back at the LEFT edge instead of
   staying under the title on the right. Growing the title/summary block
   instead (min-width:0 lets its own text wrap/truncate inside its own
   column) keeps this a single row, so the button never moves. */
.action-items-header > div:first-child { flex: 1 1 auto; min-width: 0; }
.action-items-header > .expand-btn { flex: none; }
.action-item-card {
  margin: 0 0 10px; cursor: pointer; transition: filter 0.12s;
}
.action-item-card:last-child { margin-bottom: 0; }
.action-item-card:hover { filter: brightness(1.05); }
.action-item-card-top { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
.action-item-card-name {
  flex: 1; min-width: 0; font-weight: 650; color: var(--text-primary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.action-item-card-desc { font-weight: 400; color: var(--text-secondary); }
.action-item-card-value { flex: none; text-align: right; font-weight: 700; font-variant-numeric: tabular-nums; }
.action-item-card-value-label {
  display: block; font-weight: 500; font-size: 10px; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: 0.02em; margin-top: 1px;
}
.action-item-card-explain { margin: 0; }

/* Card accent -- left border reflects whether this KPI's OWN trend
   direction is the good one or the bad one (defined per-card in
   dashboard.js's KPI_DIRECTION; not every card gets a color -- some are
   genuinely neutral/context-dependent, e.g. Open PO Value, and stay
   plain rather than being forced into a false "up=good" reading). */
.insight-card { border-left-width: 3px; }
.insight-card-good { border-left-color: var(--status-good); }
.insight-card-bad { border-left-color: var(--status-critical); }

.insight-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 14px;
  margin-bottom: 20px;
}

.insight-card {
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  padding: 16px 18px 8px;
}

.insight-card-header-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}

.insight-card-title {
  font-size: 13px;
  font-weight: 650;
}
.insight-card-title-explained { cursor: help; border-bottom: 1px dotted var(--text-muted); }

/* Generic "hover to see how this is calculated" affordance -- applied to
   any KPI/stat-box/compare-card/table-column label across the app (see
   Common.explainedLabel() in common.js, and plain title="" + this class
   on static HTML labels/headers). Inline-block so the dotted underline
   hugs the text, not the full width of a block-level label -- inside a
   table header, wrap just the header text in a <span class="explained-
   label"> rather than putting the class on <th> itself, so the cell
   keeps its normal table layout. */
.explained-label { display: inline-block; cursor: help; border-bottom: 1px dotted var(--text-muted); }

/* Table column headers carry their explanation as a plain title="" (not
   .explained-label -- a dotted underline there fights the header's own
   underline/sort-arrow styling), but still get the help cursor so
   hovering them reads the same as everywhere else in the app. */
th[title] { cursor: help; }

.expand-btn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  border: 1px solid var(--border);
  background: var(--surface-1);
  color: var(--text-muted);
  border-radius: 6px;
  padding: 3px 8px;
  font-size: 11px;
  font-weight: 600;
  cursor: pointer;
}
.expand-btn:hover { color: var(--series-1); border-color: var(--series-1); }

.cost-flag {
  color: var(--status-warning);
  font-size: 12px;
  flex: none;
  cursor: help;
}

.insight-trend-row {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  margin-bottom: 8px;
}
.insight-trend-single { justify-content: flex-start; padding: 4px 0 2px; }

.insight-endpoint { flex: none; }
.insight-endpoint-date { font-size: 10.5px; color: var(--text-muted); margin-bottom: 2px; }
.insight-endpoint-value { font-size: 17px; font-weight: 650; font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
.insight-endpoint-value.muted-value { color: var(--text-muted); font-weight: 600; font-size: 15px; }

.insight-sparkline { flex: 1; min-width: 0; }

.insight-value-big { font-size: 22px; font-weight: 650; letter-spacing: -0.01em; font-variant-numeric: tabular-nums; }

.insight-change-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
  min-height: 22px;
}
.change-badge {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 11.5px;
  font-weight: 650;
}
.insight-count { font-size: 11.5px; color: var(--text-muted); }
.insight-count.muted { font-style: italic; }

.insight-list {
  border-top: 1px solid var(--gridline);
  margin: 0 -18px;
  padding: 2px 18px 4px;
}
.insight-list-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 0;
  border-bottom: 1px solid var(--gridline);
  cursor: pointer;
  font-size: 12px;
}
.insight-list-row:last-child { border-bottom: none; }
.insight-list-row:hover .insight-list-name { color: var(--series-1); }
.insight-list-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-secondary);
}
.insight-list-value { flex: none; font-weight: 600; font-variant-numeric: tabular-nums; }

/* ---------- Pagination ---------- */

.pagination {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;
  padding: 12px 16px;
  border-top: 1px solid var(--gridline);
}
.page-range { font-size: 12.5px; color: var(--text-muted); }
.page-controls { display: flex; align-items: center; gap: 4px; }
.page-btn {
  min-width: 30px;
  height: 30px;
  padding: 0 8px;
  border-radius: 7px;
  border: 1px solid var(--border);
  background: var(--surface-1);
  color: var(--text-secondary);
  font-size: 12.5px;
  font-weight: 550;
  cursor: pointer;
}
.page-btn:hover:not(:disabled) { background: var(--gridline); color: var(--text-primary); }
.page-btn.active { background: var(--series-1); border-color: var(--series-1); color: #fff; }
.page-btn:disabled { opacity: 0.4; cursor: default; }
.page-btn.page-nav { font-weight: 600; padding: 0 10px; }
.page-ellipsis { color: var(--text-muted); padding: 0 2px; font-size: 12.5px; }
.part-desc {
  color: var(--text-secondary);
  max-width: 320px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  display: inline-block;
  vertical-align: bottom;
}

.muted { color: var(--text-muted); }

/* ---------- Status badge ---------- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px 3px 7px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
}
.badge .dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
.badge-explained { cursor: help; }
/* A badge whose displayed text has been overridden (e.g. "Overdue" in
   place of "Reorder Now") -- bolder than the standard 600 weight so the
   more urgent framing actually reads as more urgent. */
.badge-strong { font-weight: 800; }

.badge-critical { background: var(--status-critical-bg); color: var(--status-critical); }
.badge-critical .dot { background: var(--status-critical); }
.badge-serious { background: var(--status-serious-bg); color: var(--status-serious); }
.badge-serious .dot { background: var(--status-serious); }
.badge-warning { background: var(--status-warning-bg); color: #93610a; }
.badge-warning .dot { background: var(--status-warning); }
.badge-good { background: var(--status-good-bg); color: var(--status-good); }
.badge-good .dot { background: var(--status-good); }
.badge-neutral { background: var(--status-neutral-bg); color: var(--text-secondary); }
.badge-neutral .dot { background: var(--status-neutral); }

@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) .badge-warning { color: #f5c451; }
}
:root[data-theme="dark"] .badge-warning { color: #f5c451; }

/* ---------- Empty state ---------- */

.empty-state {
  padding: 64px 24px;
  text-align: center;
  color: var(--text-secondary);
}
.empty-state h3 { margin: 12px 0 6px; color: var(--text-primary); }
.empty-state p { margin: 0 0 18px; font-size: 13px; }

/* ---------- Modal ---------- */

.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(11, 11, 11, 0.45);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 40px 20px;
  overflow-y: auto;
  z-index: 50;
}

.modal {
  background: var(--surface-1);
  border-radius: 14px;
  box-shadow: var(--shadow-modal);
  width: 100%;
  max-width: 780px;
  padding: 0;
  border: 1px solid var(--border);
}

.modal-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  padding: 20px 24px;
  border-bottom: 1px solid var(--gridline);
}

.modal-header h2 { margin: 0 0 3px; font-size: 17px; font-weight: 650; }
.modal-header .modal-sub { color: var(--text-secondary); font-size: 13px; }

.modal-body { padding: 20px 24px 24px; }

.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: 10px;
  margin-bottom: 20px;
}
.stat-grid-5 {
  grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
}
.stat-box {
  background: var(--page-plane);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 12px;
}
.stat-box .stat-label { font-size: 11px; color: var(--text-muted); margin-bottom: 4px; }
.stat-box .stat-value { font-size: 16px; font-weight: 650; font-variant-numeric: tabular-nums; }

.section-title {
  font-size: 12px;
  font-weight: 650;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin: 20px 0 10px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.reorder-math {
  background: var(--page-plane);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px 14px;
  font-size: 13px;
  display: flex;
  flex-wrap: wrap;
  gap: 6px 4px;
  align-items: center;
}
.reorder-math .term { white-space: nowrap; }
.reorder-math .op { color: var(--text-muted); margin: 0 4px; }
.reorder-math .result { font-weight: 650; }

.chart-wrap { position: relative; height: 180px; margin-bottom: 4px; }

.po-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.po-table th {
  text-align: left; color: var(--text-muted); font-weight: 600;
  padding: 6px 8px; border-bottom: 1px solid var(--gridline);
}
.po-table td { padding: 6px 8px; border-bottom: 1px solid var(--gridline); }
.po-table td.num { text-align: right; font-variant-numeric: tabular-nums; }
/* A formatted date ("Oct 6, 2026") is always short and never needs to
   wrap -- forcing it onto one line (2026-09-11) instead of letting the
   column narrow it across two or three, which the KPI popups' Run Short
   Date/Promised Ship Date/Date Fulfilled columns were doing. */
.po-table td.date-cell, .po-table th.date-cell, .data-table td.date-cell, .data-table th.date-cell { white-space: nowrap; }

/* Sortable .po-table headers (2026-09-23, KPI card drill-down modals --
   explicit request: "make every column... sortable when you click on the
   column") -- only th[data-key] gets the pointer/hover treatment, not
   every .po-table th, since this class is also used for non-sortable
   tables elsewhere (e.g. the part-detail modal's own PO/SO line lists).
   Same sort-arrow/.sorted visual language as .data-table thead th
   already uses everywhere else, just scoped to this class instead. */
.po-table th[data-key] { cursor: pointer; user-select: none; }
.po-table th[data-key]:hover { color: var(--text-primary); }
.po-table th .sort-arrow { opacity: 0.4; font-size: 10px; margin-left: 4px; }
.po-table th.sorted .sort-arrow { opacity: 1; color: var(--series-1); }

.close-btn {
  width: 30px; height: 30px; border-radius: 8px; border: 1px solid var(--border-strong);
  background: var(--surface-2); color: var(--text-secondary); cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center; flex: none;
}
.close-btn:hover { background: var(--gridline); }

.toggle-link {
  font-size: 11px; color: var(--series-1); cursor: pointer; font-weight: 600;
  background: none; border: none; padding: 0;
}

footer.app-footer {
  margin-top: 28px;
  color: var(--text-muted);
  font-size: 12px;
  text-align: center;
}

/* ---------- Shared page structure ---------- */

.page-section {
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  padding: 18px 20px;
  margin-bottom: 16px;
}
.page-section-title {
  font-size: 14px;
  font-weight: 650;
  margin: 0 0 4px;
}
/* A page-section-title that opens the shared part detail modal on click
   (Scenario Planning, Shipping Calendar -- 2026-09-24, explicit request:
   "have the part number pop up pull up like we have elsewhere"). Just a
   hover affordance -- the click binding itself is per-page JS. */
.page-section-title.title-link { cursor: pointer; }
.page-section-title.title-link:hover { color: var(--series-1); text-decoration: underline; }
.page-section-sub {
  font-size: 12.5px;
  color: var(--text-secondary);
  margin: 0 0 14px;
}

.tag-pill {
  display: inline-block;
  background: var(--gridline);
  color: var(--text-primary);
  border-radius: 5px;
  padding: 1px 7px;
  font-size: 11.5px;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}

a.toggle-link { text-decoration: none; }
a.toggle-link:hover { text-decoration: underline; }

.tier-count-pill {
  display: inline-block;
  border-radius: 5px;
  padding: 1px 8px;
  font-size: 11.5px;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}
.tier-count-pill.tier-now { background: var(--status-critical-bg); color: var(--status-critical); }
.tier-count-pill.tier-soon { background: var(--status-warning-bg); color: var(--status-warning); }
a.tier-count-pill { text-decoration: none; cursor: pointer; }
a.tier-count-pill:hover { filter: brightness(0.92); }

/* Cross-page part links -- Positions by Part's "on the Action Plan"
   link and Action Plan's own "Calendar" link -- deliberately NOT
   tier-now/tier-soon's red/amber (those read as severity, already said
   by the badge right next to it) but the app's one consistent "this is
   clickable" blue, same language as sorted table headers/active nav, so
   it reads as a link at a glance rather than a second status pill. */
.tier-count-pill.action-plan-link,
.tier-count-pill.calendar-link {
  background: var(--series-1-fill);
  color: var(--series-1);
  border: 1px solid var(--series-1);
}
.tier-count-pill.action-plan-link:hover,
.tier-count-pill.calendar-link:hover { background: var(--series-1); color: #fff; filter: none; }

/* Brief highlight for the specific row a ?part= deep link (Positions by
   Part's "on the Action Plan" link) landed on, inside its now-expanded
   card, so it's not just "somewhere in this card of several parts." */
@keyframes rowFlash {
  0% { background: var(--status-warning-bg); }
  100% { background: transparent; }
}
.row-flash td { animation: rowFlash 2.2s ease-out; }

/* ---------- Action Plan / Replenishment: vendor PO cards ---------- */

.po-card {
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  margin-bottom: 10px;
  overflow: hidden;
}
.po-card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 13px 16px;
  cursor: pointer;
}
.po-card-header:hover { background: var(--page-plane); }
.po-card-title { display: flex; align-items: center; gap: 10px; }
.po-card-title .vendor-name { font-weight: 650; font-size: 14px; }
.po-card-title .line-count { font-size: 12px; color: var(--text-muted); }
.po-card-meta { display: flex; align-items: center; gap: 18px; }
.po-card-meta .meta-item { text-align: right; }
.po-card-meta .meta-label { font-size: 10.5px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.03em; }
.po-card-meta .meta-value { font-size: 14px; font-weight: 650; font-variant-numeric: tabular-nums; }
.po-card-chevron { transition: transform 0.15s ease; color: var(--text-muted); }
.po-card.expanded .po-card-chevron { transform: rotate(90deg); }
.po-card-body { display: none; border-top: 1px solid var(--gridline); }
.po-card.expanded .po-card-body { display: block; }
.po-card-actions { display: flex; justify-content: flex-end; padding: 8px 16px; border-top: 1px solid var(--gridline); }

/* Per-part PO schedule breakdown (2026-09-14) -- a sub-row directly below
   each part's own row, splitting that SAME suggested/projected qty into a
   timed sequence of smaller orders instead of assuming it's all placed
   at once. See action_plan.js's buildPoSchedule() docstring. */
.po-schedule-row td { border-bottom: 1px solid var(--gridline); background: var(--page-plane); }
.po-schedule { padding: 8px 14px 10px 34px; }
.po-schedule-intro { font-size: 11.5px; margin-bottom: 6px; }
.po-schedule-batch {
  padding: 3px 0 3px 10px;
  border-left: 2px solid var(--border-strong);
  margin-bottom: 6px;
  font-size: 12px;
}
.po-schedule-batch:last-child { margin-bottom: 0; }
.po-schedule-batch-now { border-left-color: var(--status-critical); }
.po-schedule-num {
  display: inline-block;
  font-size: 10.5px;
  font-weight: 700;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  margin-right: 6px;
}
.po-schedule-covers { font-size: 11px; margin-top: 2px; }

/* Dashboard KPI drill-down chevron (2026-09-15) -- Overdue Sales Orders /
   Surplus Orders cards only (dashboard.js's DRILL_DOWN_CARDS); expands a
   sub-row beneath the part showing the specific order lines behind that
   part's contribution to the card's $ figure. */
.drill-toggle {
  background: none; border: none; padding: 2px; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  color: var(--text-muted); border-radius: 4px;
}
.drill-toggle:hover { background: var(--surface-2); color: var(--text-primary); }
.drill-chevron { transition: transform 0.15s ease; }
.drill-toggle.open .drill-chevron { transform: rotate(90deg); }
.drill-detail-row td { background: var(--page-plane); }

/* "Confirm Quantity" button (2026-09-17) -- Common.confirmQtyNote(),
   shown next to/under any Incoming (Open PO) figure for a part whose
   Monday.com total disagrees with Fishbowl's own On Order figure. A real
   <button> (not just a hover tooltip) -- click opens
   Common.openPoReconciliationModal()'s side-by-side reconciliation
   popup. */
.confirm-qty-note {
  display: inline-block;
  background: var(--status-warning-bg);
  color: var(--status-warning);
  border: none;
  border-radius: 5px;
  padding: 1px 7px;
  font-size: 10.5px;
  font-weight: 650;
  font-family: inherit;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  cursor: pointer;
  white-space: nowrap;
}
.confirm-qty-note:hover { filter: brightness(0.95); }

/* Subtotal row under each table in the On Order Reconciliation popup
   (2026-09-17) -- Common.openPoReconciliationModal() -- lets the sum of
   the individual lines be checked directly against the header stat
   right above it. */
.po-recon-total td { font-weight: 650; border-top: 2px solid var(--border-strong); background: var(--page-plane); }

/* Per-order mismatch highlight in the On Order Reconciliation popup
   (2026-09-22) -- flags a specific PO line that doesn't reconcile
   against the other side (missing entirely, or a qty difference on the
   same PO #), rather than leaving the user to diff two tables by eye. */
.po-recon-mismatch td { background: var(--status-warning-bg); }
.po-recon-flag { color: var(--status-warning); cursor: help; font-size: 12px; }

/* ---------- Classification: ABC x XYZ matrix ---------- */

.matrix-grid {
  display: grid;
  grid-template-columns: 24px 44px repeat(3, 1fr);
  grid-auto-rows: minmax(84px, auto);
  gap: 8px;
  max-width: 780px;
}
.matrix-axis-label {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-size: 11px;
  font-weight: 650;
  color: var(--text-muted);
  text-align: center;
  align-self: stretch;
  display: flex;
  align-items: center;
  justify-content: center;
}
.matrix-col-label, .matrix-row-label {
  font-size: 11px;
  font-weight: 650;
  color: var(--text-muted);
  text-align: center;
  display: flex;
  align-items: center;
  justify-content: center;
}
.matrix-cell {
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 14px;
  cursor: pointer;
  transition: border-color 0.12s ease, transform 0.12s ease;
  min-height: 84px;
}
.matrix-cell:hover { border-color: var(--series-1); }
.matrix-cell .cell-count { font-size: 22px; font-weight: 650; }
.matrix-cell .cell-value { font-size: 12px; color: var(--text-secondary); margin-top: 2px; }
.matrix-cell .cell-tag { font-size: 10.5px; font-weight: 650; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.03em; margin-bottom: 6px; }

/* ---------- Suppliers: on-time performance bar ---------- */

.perf-bar-track {
  width: 100%;
  height: 6px;
  border-radius: 999px;
  background: var(--gridline);
  overflow: hidden;
}
.perf-bar-fill { height: 100%; border-radius: 999px; }

/* ---------- Settings form ---------- */

.settings-form { max-width: 480px; }
.form-row { margin-bottom: 18px; }
.form-row label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 4px; }
.form-row .form-hint { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
.form-row input[type="number"], .form-row input[type="text"] {
  width: 100%;
  border: 1px solid var(--border-strong);
  background: var(--surface-1);
  color: var(--text-primary);
  border-radius: 8px;
  padding: 8px 12px;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}
.form-row input:focus { outline: 2px solid var(--series-1); outline-offset: -1px; }
.save-status { font-size: 12.5px; color: var(--status-good); margin-left: 10px; }

/* ---------- Forecast page ---------- */

.forecast-layout { display: grid; grid-template-columns: 300px 1fr; gap: 16px; align-items: start; }
/* A CSS Grid "1fr" track can still grow past its share to fit a wide
   child's intrinsic content width (grid's default min-width:auto on
   items) -- latent on every page using this layout, but only surfaced
   once the Customers page's line table grew wide enough (2026-09-08,
   the new Suggested Next Step column) to actually trigger it: the whole
   right column pushed past the viewport instead of the table's own
   .table-scroll doing its job. min-width:0 here is the standard grid fix
   -- lets the track shrink to its actual 1fr share so .table-scroll's
   own overflow-x:auto is what handles any content wider than that. */
.forecast-layout > * { min-width: 0; }
.movers-list { max-height: 620px; overflow-y: auto; }

/* Scenario Planning specifically: stretch the movers panel to match the
   right (part-detail) column's real height instead of capping it at a
   fixed 620px -- so the two panels end at the same line no matter how
   tall the right side's chart/vendor table content ends up being. */
.scenario-layout { align-items: stretch; }
.scenario-layout .movers-panel { display: flex; flex-direction: column; min-height: 0; }
.scenario-layout .movers-panel .search-input { flex: none; }
.scenario-layout .movers-list { flex: 1; max-height: none; min-height: 0; }

/* Shipping Calendar: same idea as .scenario-layout above, just against
   its own two separate .page-section cards (left movers list, right
   calendar) instead of one shared card -- see
   shipping_calendar.js:syncMoversPanelHeight() for why this needs a JS
   assist rather than align-items:stretch alone (grid's own intrinsic
   sizing can't clip a long scrollable list before stretch ever applies). */
.calendar-layout { align-items: stretch; }
.calendar-layout #shcMoversPanel { display: flex; flex-direction: column; min-height: 0; }
.calendar-layout #shcMoversPanel .search-input { flex: none; }
.calendar-layout .movers-list { flex: 1; max-height: none; min-height: 0; }

/* Customers page (2026-09-09): unlike Scenario Planning/Shipping
   Calendar's right column (a chart + a short table, rarely taller than
   one screen), a customer's order table can run to hundreds of rows --
   syncing the list's height to match it (Shipping Calendar's approach,
   see syncMoversPanelHeight() above) would make the list itself
   scroll-to-oblivion long instead of fixing anything. Pinning it to the
   viewport instead: align-self:start opts this one grid item out of
   .scenario-layout's align-items:stretch (which this page also carries
   for its other shared rules), sticky keeps it in view while the order
   table scrolls past underneath, and the capped max-height is what makes
   the list scroll internally rather than growing forever. 70px clears
   the sticky topbar (see .topbar); 90px leaves a little breathing room
   at the bottom of the viewport. */
.customers-layout .movers-panel {
  align-self: start;
  position: sticky;
  top: 70px;
  max-height: calc(100vh - 90px);
}
.customers-layout .movers-list { overflow-y: auto; }
.mover-row {
  display: flex; align-items: center; justify-content: space-between;
  padding: 9px 10px; border-radius: 8px; cursor: pointer; gap: 8px;
}
.mover-row:hover { background: var(--gridline); }
.mover-row.active { background: var(--status-good-bg); }
.mover-row .mover-num { font-weight: 650; font-size: 12.5px; }
.mover-row .mover-desc { font-size: 11.5px; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 170px; }
.mover-row .mover-value { font-size: 12px; font-variant-numeric: tabular-nums; color: var(--text-secondary); flex: none; }
.method-pill {
  display: inline-flex; align-items: center; gap: 5px;
  background: var(--status-good-bg); color: var(--status-good);
  border-radius: 999px; padding: 2px 10px; font-size: 11.5px; font-weight: 650;
}
.legend-row { display: flex; gap: 18px; align-items: center; margin-bottom: 10px; font-size: 12px; color: var(--text-secondary); }
.legend-swatch { display: inline-block; width: 14px; height: 3px; border-radius: 2px; margin-right: 6px; vertical-align: middle; background: var(--series-1); }
.legend-swatch-dashed {
  background: none !important;
  border-top: 2px dashed var(--series-2);
  height: 0;
  width: 16px;
}
.legend-swatch-dashed-3 {
  background: none !important;
  border-top: 2px dashed var(--series-3);
  height: 0;
  width: 16px;
}
.legend-swatch-dashed-4 {
  background: none !important;
  border-top: 2px dashed var(--series-4);
  height: 0;
  width: 16px;
}

/* Shared "important callout" box -- started as Scenario Planning's
   stock-vs-forecast gap note (the text companion to the chart's canvas
   bracket, see scenario.js's stockGapPlugin) and reused for the
   "some real demand/supply has no known date, so isn't reflected"
   caveat on both Scenario Planning and Shipping Calendar (2026-08-31)
   -- that fact is easy to lose as one clause inside a longer sentence,
   and it materially changes how much to trust the number next to it,
   so it gets a real box instead of small muted text. */
.gap-note {
  margin: 8px 0 4px;
  padding: 9px 12px;
  border-radius: var(--radius);
  border-left: 3px solid;
  font-size: 12.5px;
  line-height: 1.5;
}
.gap-note-critical { background: var(--status-critical-bg); border-color: var(--status-critical); }
.gap-note-good { background: var(--status-good-bg); border-color: var(--status-good); }
.gap-note-warning { background: var(--status-warning-bg); border-color: var(--status-warning); color: var(--text-primary); }
/* Confirm ETA action items (2026-09-14) -- calmer than Reorder Now's
   amber warning tint, matching that status's own gray badge -- there's
   already a real PO on file, just needs a date confirmed. */
.gap-note-neutral { background: var(--status-neutral-bg); border-color: var(--status-neutral); }
.legend-swatch-dot {
  display: inline-block;
  width: 9px; height: 9px;
  border-radius: 50%;
  margin-right: 6px;
  vertical-align: middle;
  background: var(--series-3);
}
.legend-swatch-good { display: inline-block; width: 14px; height: 3px; border-radius: 2px; margin-right: 6px; vertical-align: middle; background: var(--status-good); }
.legend-swatch-dashed-good {
  background: none !important;
  border-top: 2px dashed var(--status-good);
  height: 0;
  width: 16px;
}

/* ---------- scenario planning ---------- */
.scenario-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: flex-end;
  margin: 14px 0 18px;
}
.scenario-control { display: flex; flex-direction: column; gap: 5px; min-width: 150px; }
.scenario-control label { font-size: 11px; color: var(--text-muted); font-weight: 600; }
.scenario-control-btn { min-width: 0; }
.scenario-input-with-suffix {
  display: flex; align-items: center; gap: 6px;
  border: 1px solid var(--border-strong);
  background: var(--surface-1);
  border-radius: 8px;
  padding: 0 10px;
}
.scenario-input-with-suffix input {
  border: none; background: none; color: var(--text-primary);
  font-size: 13px; padding: 8px 0; width: 100%; outline: none;
}
.scenario-input-with-suffix span { font-size: 12px; color: var(--text-muted); }

.compare-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; }
/* Shipping Calendar's own stat row (#shcStats) carries 6 boxes, one more
   than .compare-grid's other real user (Scenario Planning's 5-box
   #partCompareGrid) -- a scoped modifier, not a change to the shared
   base rule, so Scenario Planning's card sizing is untouched (2026-09-09:
   "put it onto one line"). Fixed at exactly 6 columns (not auto-fit/
   minmax, which wrapped to a 7th row below a certain width no matter how
   small the minimum got -- auto-fit's implicit sizing rounds up in a way
   that isn't reliably predictable from a pixel guess) -- minmax(0, 1fr)
   lets each column genuinely shrink to fit instead of wrapping, which is
   exactly the one-line guarantee asked for here. */
.compare-grid-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
.compare-grid-compact { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px; }
.compare-grid-compact .compare-card { padding: 8px 10px; }
.compare-grid-compact .compare-label { margin-bottom: 5px; }
.compare-grid-compact .compare-scenario { font-size: 14px; }
.compare-grid-compact .compare-delta { margin-top: 4px; }
.compare-card {
  background: var(--page-plane);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px 14px;
}
.compare-label { font-size: 11px; color: var(--text-muted); margin-bottom: 8px; }
.compare-values { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.compare-current { font-size: 12.5px; color: var(--text-muted); text-decoration: line-through; text-decoration-color: var(--border-strong); }
.compare-scenario { font-size: 18px; font-weight: 700; font-variant-numeric: tabular-nums; }
.compare-delta { margin-top: 7px; }

.pipeline-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 10px; margin: 12px 0 4px; }
.pipeline-card { background: var(--page-plane); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; }
.pipeline-card-label { font-size: 11px; color: var(--text-muted); font-weight: 650; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.03em; }
.pipeline-card-value { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; margin-bottom: 6px; }
.pipeline-card-detail { font-size: 12.5px; color: var(--text-secondary); }
.pipeline-card-detail + .pipeline-card-detail { margin-top: 3px; }

.scenario-part-inputs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 10px;
  margin: 14px 0 18px;
  padding: 14px;
  background: var(--page-plane);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.calendar-adjust-note {
  display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap;
  background: var(--status-warning-bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 14px;
  font-size: 12.5px;
  margin: 0 0 14px;
}
.scenario-part-input label { display: block; font-size: 11px; color: var(--text-muted); font-weight: 600; margin-bottom: 5px; }
.scenario-part-input input, .scenario-part-input select {
  width: 100%;
  border: 1px solid var(--border-strong);
  background: var(--surface-1);
  color: var(--text-primary);
  border-radius: 8px;
  padding: 7px 10px;
  font-size: 13px;
}
.scenario-part-input input:disabled { background: var(--gridline); color: var(--text-secondary); cursor: not-allowed; }
/* Full-width, zero-height grid item -- forces whatever follows (the PO
   Forecast row) onto a fresh row regardless of viewport width, since
   auto-fit alone would otherwise pack Month 1 PO Forecast onto the tail
   of the Demand row on a wide screen. */
.scenario-row-break { grid-column: 1 / -1; height: 0; margin: 0; padding: 0; }
.scenario-scheduled-note { font-size: 11px; color: var(--text-muted); margin-top: 4px; }

@media (max-width: 860px) {
  .forecast-layout { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
  .matrix-grid { grid-template-columns: 18px 32px repeat(3, 1fr); }
}

/* ---------- shipping calendar ---------- */
.legend-swatch-hatched {
  display: inline-block;
  width: 14px; height: 10px;
  border-radius: 3px;
  margin-right: 6px;
  vertical-align: middle;
  border: 1px solid var(--border-strong);
  background: repeating-linear-gradient(45deg, var(--gridline), var(--gridline) 2px, var(--surface-1) 2px, var(--surface-1) 4px);
}
.calendar-toolbar { display: flex; align-items: center; gap: 10px; margin-top: 4px; }
.calendar-month-label { font-size: 14px; font-weight: 650; min-width: 150px; text-align: center; }
.calendar-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 4px;
  margin-top: 8px;
  margin-bottom: 18px;
}
.calendar-header-cell {
  font-size: 11px;
  font-weight: 650;
  color: var(--text-muted);
  text-align: center;
  padding: 2px 0 6px;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.calendar-day {
  min-height: 92px;
  background: var(--page-plane);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 3px;
  overflow: hidden;
}
.calendar-day:not(.calendar-day-empty) { cursor: pointer; }
.calendar-day:not(.calendar-day-empty):hover { border-color: var(--border-strong); }
.calendar-day.is-today { border-color: var(--series-1); border-width: 2px; }
.calendar-day-empty { background: transparent; border-color: transparent; }
.calendar-day-num { font-size: 11px; font-weight: 650; color: var(--text-secondary); }
.calendar-badge {
  font-size: 10px;
  border-radius: 4px;
  padding: 1px 5px;
  font-weight: 650;
  line-height: 1.5;
  white-space: nowrap;
}
.calendar-badge-confirmed { background: var(--series-1-fill); color: var(--series-1); }
.calendar-badge-estimated { background: var(--gridline); color: var(--text-secondary); border: 1px dashed var(--border-strong); }
.calendar-badge-planned { background: var(--series-3-fill); color: var(--series-3); border: 1px dashed var(--series-3); }
.calendar-badge-incoming { background: var(--status-good-bg); color: var(--status-good); }
.calendar-badge-incoming-est { background: var(--status-good-bg); color: var(--status-good); border: 1px dashed var(--status-good); }
/* Same purple "hypothetical" family as .calendar-badge-planned (not the
   green "real incoming" family) -- direction (out vs in) is carried by
   the badge text ("Plan" vs "Plan In"), color carries hypothetical-vs-
   real, so a hypothetical PO is never mistaken for a real estimated one. */
.calendar-badge-planned-incoming { background: var(--series-3-fill); color: var(--series-3); border: 1px dashed var(--series-3); }
.calendar-day-balance { font-size: 10px; color: var(--text-muted); margin-top: auto; font-variant-numeric: tabular-nums; }
.calendar-day-balance.is-negative { color: var(--status-critical); font-weight: 650; }
/* Customers page's order calendar popup (2026-09-14) -- tints a whole
   day cell by its most-severe at-risk line, mirroring dashboard.js's
   action-item card tint precedence (critical/warning/neutral). */
.calendar-day-risk-critical { background: var(--status-critical-bg); border-color: var(--status-critical); }
.calendar-day-risk-warning { background: var(--status-warning-bg); border-color: var(--status-warning); }
.calendar-day-risk-neutral { background: var(--status-neutral-bg); border-color: var(--status-neutral); }

.plan-entry-row {
  display: flex; align-items: center; justify-content: space-between;
  padding: 7px 10px;
  background: var(--page-plane);
  border: 1px solid var(--border);
  border-radius: 6px;
  margin-bottom: 6px;
  font-size: 12.5px;
}
.plan-entry-remove { cursor: pointer; color: var(--text-muted); font-weight: 700; padding: 0 4px; }
.plan-entry-remove:hover { color: var(--status-critical); }

@media (max-width: 640px) {
  .calendar-day { min-height: 56px; }
  .calendar-badge { font-size: 9px; padding: 0 3px; }
}

/* Login page, design option 2 (2026-09-09) -- explicit feedback against
   the split-panel version. Full-bleed branded backdrop (dark gradient +
   one huge faint copy of the icon mark) instead of confining the brand
   treatment to one side; a single centered white card floats on it, with
   the logo sitting directly on the backdrop above the card rather than
   inside it. One focal point instead of two side-by-side ones.

   The whole page is a deliberately fixed design (dark backdrop, always-
   white card) rather than reactive to the viewer's light/dark app
   preference -- a light card on an already-dark backdrop is what gives
   this its contrast/pop; flipping the card dark too would sit flush
   against the backdrop and lose that. Same "fixed design surface, not
   themed app content" reasoning as the split version's brand panel, just
   extended to the whole page this time. Hardcoded light-mode hex values
   below (not var(--text-primary) etc, which flip with the app's theme)
   are intentional for exactly this reason. */
.login-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: hidden;
  /* Real warehouse photo (app/static/img/login-bg.jpg -- Brayden Prato via
     Unsplash, Unsplash License) instead of drawn shapes, per explicit
     feedback against the scattered-icon backdrop. The gradient is a scrim
     ON TOP of the photo (first color stop, not a separate layer under
     it), darkened enough that it reads as moody warehouse atmosphere
     behind the card rather than a bright photo competing with it --
     still recognizably a real warehouse, just dimmed to this app's
     charcoal register. */
  background:
    linear-gradient(160deg, rgba(24,24,23,0.86), rgba(30,30,28,0.90) 55%, rgba(20,20,19,0.88)),
    url("/static/img/login-bg.jpg") center / cover no-repeat;
  padding: 40px 20px;
}
.login-stack {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  max-width: 470px;
}
.login-hero-logo { height: 58px; width: auto; display: block; margin-bottom: 40px; filter: brightness(0) invert(1); }

.login-card {
  width: 100%;
  background: #ffffff;
  border-radius: var(--radius);
  box-shadow: var(--shadow-modal);
  padding: 46px 42px;
}
.login-heading { font-size: 28px; font-weight: 700; letter-spacing: -0.01em; margin: 0 0 8px; color: #0b0b0b; }
.login-sub { font-size: 14.5px; color: #52514e; margin: 0 0 30px; }
.login-error {
  background: rgba(208,59,59,0.10); color: #d03b3b;
  font-size: 13px; padding: 11px 14px; border-radius: 8px; margin-bottom: 20px;
}
.login-form { display: flex; flex-direction: column; gap: 9px; }
.login-label { font-size: 13px; font-weight: 600; color: #52514e; }
.login-input {
  width: 100%; padding: 14px 16px; font-size: 15.5px; box-sizing: border-box;
  border-radius: 9px; border: 1px solid rgba(11,11,11,0.16); background: #ffffff;
  color: #0b0b0b;
}
.login-input:focus { outline: none; border-color: var(--series-1); box-shadow: 0 0 0 3px var(--series-1-fill); }
.login-submit { width: 100%; justify-content: center; margin-top: 12px; padding: 14px; font-size: 15.5px; }

@media (max-width: 560px) {
  .login-card { padding: 34px 26px; }
}
