/* ==========================================================================
   PriceSync design tokens + component layer
   --------------------------------------------------------------------------
   GLOBALLY AVAILABLE, SELECTIVELY APPLIED.

   The :root custom properties below are loaded on every page, but they are
   inert on their own — nothing changes appearance until an element opts in by
   sitting inside `.ps-scope`. Today only /competitors carries that class.
   This is deliberate: MainLayout is the DefaultLayout for all 21 pages, so an
   unscoped `body { font: ... }` here would restyle the entire app in one go.

   To adopt this layer on another page, wrap its markup in `.ps-scope`.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. FONTS — self-hosted, no CDN.
   The droplet must render without reaching fonts.gstatic.com: an external
   render-blocking request is a real cost on a 405-row grid, and an outage
   there should never mean an unstyled pricing page.
   Both families are OFL. Latin subset only.
   -------------------------------------------------------------------------- */
@font-face { font-family: 'Plus Jakarta Sans'; font-style: normal; font-weight: 400;
             font-display: swap; src: url('fonts/jakarta-400.woff2') format('woff2'); }
@font-face { font-family: 'Plus Jakarta Sans'; font-style: normal; font-weight: 500;
             font-display: swap; src: url('fonts/jakarta-500.woff2') format('woff2'); }
@font-face { font-family: 'Plus Jakarta Sans'; font-style: normal; font-weight: 600;
             font-display: swap; src: url('fonts/jakarta-600.woff2') format('woff2'); }
@font-face { font-family: 'Plus Jakarta Sans'; font-style: normal; font-weight: 700;
             font-display: swap; src: url('fonts/jakarta-700.woff2') format('woff2'); }
@font-face { font-family: 'Plus Jakarta Sans'; font-style: normal; font-weight: 800;
             font-display: swap; src: url('fonts/jakarta-800.woff2') format('woff2'); }

@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400;
             font-display: swap; src: url('fonts/jetbrains-400.woff2') format('woff2'); }
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 500;
             font-display: swap; src: url('fonts/jetbrains-500.woff2') format('woff2'); }
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 600;
             font-display: swap; src: url('fonts/jetbrains-600.woff2') format('woff2'); }

/* --------------------------------------------------------------------------
   2. TOKENS — named for ROLE, never for value.
   `--ink-2` stays correct when the grey changes; `--gray-400` does not.
   -------------------------------------------------------------------------- */
:root {
  /* Ink — text, darkest to lightest */
  --ink-1: #0f172a;   /* headings, primary figures */
  --ink-2: #334155;   /* body text */
  --ink-3: #64748b;   /* secondary / meta */
  --ink-4: #94a3b8;   /* de-emphasised, placeholder */

  /* Surface — backgrounds, lowest to highest elevation */
  --surface-page:  #f6f7f9;
  --surface-card:  #ffffff;
  --surface-sunk:  #f1f5f9;  /* inset wells, sticky header fill */
  --surface-hover: #eff6ff;

  /* Line */
  --line-soft:   #e8ecf1;   /* interior grid rules */
  --line:        #d8dee7;   /* component borders */
  --line-strong: #b9c2ce;   /* zone dividers */

  /* Provenance — the semantic core of the observation model.
     Feed and manual must be distinguishable at a glance and in the hover. */
  --feed:        #7c3aed;   /* SKUlytics purple */
  --feed-soft:   #f3effe;
  --feed-line:   #ddd0fb;
  --manual:      #c2620a;   /* human-entered orange */
  --manual-soft: #fdf3e7;
  --manual-line: #f5ddba;

  /* Status */
  --good:      #15803d;
  --good-soft: #eaf6ee;
  --warn:      #b45309;
  --bad:       #b91c1c;

  /* Column-zone bands. The zone header carries the colour so the table's own
     header can stay light — a dark header competes with the dark sidebar. */
  --zone-week:  #eef4ff;
  --zone-week-line: #c7d9f7;
  --zone-hist:  #f4f2f7;
  --zone-hist-line: #d9d2e4;
  --zone-calc:  #edf6f0;
  --zone-calc-line: #c4e2ce;

  /* Spacing scale — 4px base */
  --s-1: 4px;   --s-2: 8px;   --s-3: 12px;  --s-4: 16px;
  --s-5: 20px;  --s-6: 24px;  --s-7: 32px;  --s-8: 40px;

  /* Radii */
  --r-sm: 5px;  --r-md: 8px;  --r-lg: 12px; --r-pill: 999px;

  /* Shadows — exactly two */
  --shadow-raise: 0 1px 2px rgba(15, 23, 42, .06), 0 1px 3px rgba(15, 23, 42, .04);
  --shadow-float: 0 4px 12px rgba(15, 23, 42, .10), 0 2px 4px rgba(15, 23, 42, .06);

  /* ------------------------------------------------------------------------
     TYPOGRAPHY

     Fleet standard is 18px body with a 14px hard floor: h1 32 / h2 28 / h3 24.
     That holds for ALL page chrome — buttons, labels, filters, tabs, headers.

     ===== DOCUMENTED EXCEPTION — DO NOT "FIX" THIS =====
     18px does not survive a 15-column grid. Grid DATA sits at 15px in
     JetBrains Mono with font-variant-numeric: tabular-nums, so price columns
     align digit-for-digit and a 4-figure price cannot shift its neighbours.
     The smallest meta text (week sublabels, provenance captions) sits at 14px
     and NEVER goes below.

     This is a deliberate, bounded exception for tabular data only. Raising
     --fs-grid to 18px will reintroduce the horizontal scroll this layout was
     built to control. If you are here to "make the fonts consistent",
     the answer is no — the chrome is already consistent at 18px.
     ==================================================== */
  --fs-h1:    32px;
  --fs-h2:    28px;
  --fs-h3:    24px;
  --fs-body:  18px;   /* all chrome */
  --fs-grid:  15px;   /* EXCEPTION: tabular data only */
  --fs-meta:  14px;   /* HARD FLOOR — nothing smaller, anywhere */

  --font-sans: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Control metrics — buttons and inputs share these so a toolbar row lines up */
  --control-h:  38px;
  --control-px: 14px;
}

/* --------------------------------------------------------------------------
   3. SCOPE — opt-in application
   -------------------------------------------------------------------------- */
.ps-scope {
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  color: var(--ink-2);
  line-height: 1.45;
}
.ps-scope h1 { font-size: var(--fs-h1); font-weight: 800; color: var(--ink-1); letter-spacing: -.02em; }
.ps-scope h2 { font-size: var(--fs-h2); font-weight: 700; color: var(--ink-1); letter-spacing: -.01em; }
.ps-scope h3 { font-size: var(--fs-h3); font-weight: 700; color: var(--ink-1); }
.ps-scope .ps-meta { font-size: var(--fs-meta); color: var(--ink-3); }

/* --------------------------------------------------------------------------
   4. COMPONENTS — buttons in three weights, inputs, panels
   -------------------------------------------------------------------------- */
.ps-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  height: var(--control-h); padding: 0 var(--control-px);
  font-family: var(--font-sans); font-size: var(--fs-body); font-weight: 600;
  line-height: 1; white-space: nowrap;
  border-radius: var(--r-md); border: 1px solid transparent;
  cursor: pointer; text-decoration: none;
  transition: background-color .12s ease, border-color .12s ease, color .12s ease;
}
.ps-btn:disabled, .ps-btn[disabled] { opacity: .45; cursor: not-allowed; }
.ps-btn:focus-visible { outline: 2px solid var(--feed); outline-offset: 2px; }

/* SOLID — reserved for the one irreversible action on the page (Push to Planner).
   Exactly one solid button per screen; if a second appears, one of them is wrong. */
.ps-btn-solid { background: var(--ink-1); color: #fff; border-color: var(--ink-1); }
.ps-btn-solid:hover:not(:disabled) { background: #1e293b; }

/* GHOST — repeatable, reversible actions (Sync from Planner, Check prices). */
.ps-btn-ghost { background: var(--surface-card); color: var(--ink-2); border-color: var(--line); }
.ps-btn-ghost:hover:not(:disabled) { background: var(--surface-sunk); border-color: var(--line-strong); }

/* CHIP — filter pills. Same height as the others so the toolbar row is flat. */
.ps-btn-chip {
  height: var(--control-h); padding: 0 var(--s-3);
  border-radius: var(--r-pill); background: var(--surface-card);
  color: var(--ink-3); border-color: var(--line); font-weight: 500;
}
.ps-btn-chip:hover:not(:disabled) { background: var(--surface-sunk); color: var(--ink-2); }
.ps-btn-chip.is-on { background: var(--ink-1); color: #fff; border-color: var(--ink-1); font-weight: 600; }

.ps-input {
  height: var(--control-h); padding: 0 var(--s-3);
  font-family: var(--font-sans); font-size: var(--fs-body); color: var(--ink-2);
  background: var(--surface-card);
  border: 1px solid var(--line); border-radius: var(--r-md);
}
.ps-input::placeholder { color: var(--ink-4); }
.ps-input:focus { outline: none; border-color: var(--feed); box-shadow: 0 0 0 3px var(--feed-soft); }

.ps-panel {
  background: var(--surface-card); border: 1px solid var(--line);
  border-radius: var(--r-lg); box-shadow: var(--shadow-raise);
}

.ps-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
.ps-toolbar .form-check-input { width: 17px; height: 17px; margin-top: 0; }
.ps-scope .ps-toolbar label { font-size: var(--fs-body); color: var(--ink-2); margin: 0; }

/* --------------------------------------------------------------------------
   5. WEEK RAIL — the panel above the grid

   NOTE: every selector below is written scoped BY HAND. An earlier automated
   pass that prefixed selectors with a regex also prefixed the continuation
   lines of multi-line rules, producing declarations like
   `.ps-scope overflow: auto;`. CSS error recovery silently drops the first
   declaration on such a line, which is how .cpt-wrap lost `overflow` (the page
   scrolled instead of the grid) and .cp-input lost `width: 100%` (inputs fell
   back to the browser's 20-character default, giving ~260px retailer columns).
   Do not re-scope this file mechanically.
   -------------------------------------------------------------------------- */
.ps-scope .wk-rail { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-6); padding: var(--s-4) var(--s-5); margin-bottom: var(--s-3); }
.ps-scope .wk-rail-window { font-size: var(--fs-h3); font-weight: 700; color: var(--ink-1); }
.ps-scope .wk-rail-sub { font-size: var(--fs-meta); color: var(--ink-3); }
.ps-scope .wk-meters { display: flex; gap: var(--s-5); flex-wrap: wrap; }
.ps-scope .wk-meter { min-width: 148px; }
/* gap: the label and value are the two ends of a space-between row; without an
   explicit gap a long name butts straight into the figure ("Home Depot225/405"). */
.ps-scope .wk-meter-top { display: flex; justify-content: space-between; align-items: baseline; gap: var(--s-3); font-size: var(--fs-meta); margin-bottom: var(--s-1); }
.ps-scope .wk-meter-name { font-weight: 600; color: var(--ink-2); white-space: nowrap; }
.ps-scope .wk-meter-val { color: var(--ink-3); font-family: var(--font-mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
.ps-scope .wk-meter-track { height: 6px; border-radius: var(--r-pill); background: var(--surface-sunk); overflow: hidden; }
.ps-scope .wk-meter-fill { height: 100%; border-radius: var(--r-pill); background: var(--feed); }
.ps-scope .wk-gap { margin-left: auto; text-align: right; }
.ps-scope .wk-gap-n { font-size: var(--fs-h3); font-weight: 700; font-family: var(--font-mono); color: var(--bad); }

/* --------------------------------------------------------------------------
   6. THE GRID

   CONTAINMENT CONTRACT: the horizontal scroll belongs to .cpt-wrap and nothing
   above it. .cpt is `width: max-content` (wider than the viewport by design —
   Description is not truncated to force a fit), so .cpt-wrap MUST be both
   `overflow: auto` AND width-limited. An `overflow: auto` box with no width
   constraint just grows to its content and the PAGE scrolls instead, dragging
   the toolbar and week rail off-screen.
   `min-width: 0` on the wrap defeats the flex/grid `min-width: auto` default,
   which would otherwise let the table push its ancestor wider.
   -------------------------------------------------------------------------- */
.ps-scope .cpt-wrap {
  overflow: auto;
  width: 100%;
  max-width: 100%;
  min-width: 0;
  max-height: calc(100vh - 320px);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface-card);
}
.ps-scope .cpt { border-collapse: separate; border-spacing: 0; }
/* fixed layout + max-content belong ONLY to the main grid. The Price Changes tab
   reuses .cpt with an inline width:100% and no colgroup; giving it fixed layout would
   split its 11 columns evenly and wreck that table. */
/* AUTO: fill the wrap exactly (no scrollbar when there is room), never shrink past
   the fixed columns + the Description floor. */
.ps-scope .cpt-grid {
  table-layout: fixed;
  width: 100%;
  min-width: calc(var(--grid-fixed-sum) + var(--desc-floor));
}
/* PINNED: the user dragged Description, so the table is content-sized again and no
   leftover exists for fixed layout to distribute across the price columns. */
.ps-scope.desc-pinned .cpt-grid { width: max-content; min-width: 0; }

.ps-scope .cpt th,
.ps-scope .cpt td {
  font-size: var(--fs-grid);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;   /* digit-for-digit price alignment */
  padding: 3px 8px;
  white-space: nowrap;
  overflow: hidden;
  border-right: 1px solid var(--line-soft);
  border-bottom: 1px solid var(--line-soft);
}
/* Chrome inside the grid (headers and labels) stays in the sans face. */
.ps-scope .cpt thead th { font-family: var(--font-sans); font-weight: 600; }

/* Zone band row — the colour lives here so the column header can stay light. */
.ps-scope .cpt thead tr.zone-row th {
  font-size: var(--fs-meta);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  padding: 6px 8px;
  text-align: center;
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  z-index: 4;
}
.ps-scope .zone-week { background: var(--zone-week); color: #1e40af; border-top: 3px solid var(--zone-week-line); }
.ps-scope .zone-hist { background: var(--zone-hist); color: #5b3f86; border-top: 3px solid var(--zone-hist-line); }
.ps-scope .zone-calc { background: var(--zone-calc); color: #14653a; border-top: 3px solid var(--zone-calc-line); }
.ps-scope .zone-id   { background: var(--surface-sunk); color: var(--ink-3); border-top: 3px solid var(--line); }

/* Light sticky column header — replaces the old dark bar, which competed with
   the dark sidebar. Sits below the zone band, hence top: 30px. */
.ps-scope .cpt thead tr.col-row th {
  position: sticky;
  top: 30px;
  z-index: 3;
  background: var(--surface-sunk);
  color: var(--ink-2);
  border-bottom: 1px solid var(--line-strong);
  vertical-align: bottom;
}
/* Two-line header box. Every retailer/history head reserves the same height so
   the row stays even whether the label wraps ("Home / Depot") or not ("Lowe's"). */
.ps-scope .col-stack { display: block; min-height: 2.5em; line-height: 1.25; }
.ps-scope .col-sub { display: block; font-size: var(--fs-meta); font-weight: 400; color: var(--ink-3); }

/* ---- COLUMN WIDTHS ----------------------------------------------------------
   table-layout: fixed, so these are authoritative and a long Description can no
   longer stretch a column. Retailer/history widths are sized to the widest
   realistic value: "1,049.00" is 8 glyphs; JetBrains Mono advance is 0.6em, so
   at 15px that is 8 x 9px = 72px, + 16px cell padding + 1px border = 89px. 92px
   gives a little headroom without wasting a column.
   ---------------------------------------------------------------------------- */
:root {
  --col-retailer: 92px; --col-hist: 92px;
  --col-cat: 92px; --col-decided: 96px; --col-msrp: 96px;
  --col-lowest: 110px; --col-where: 64px; --col-margin: 84px;
}
.ps-scope .cpt-grid .col-retailer { width: var(--col-retailer); min-width: var(--col-retailer); max-width: var(--col-retailer); }
.ps-scope .cpt-grid .col-hist     { width: var(--col-hist);     min-width: var(--col-hist);     max-width: var(--col-hist); }
.ps-scope .cpt-grid .col-cat      { width: var(--col-cat); }
.ps-scope .cpt-grid .col-decided  { width: var(--col-decided); }
.ps-scope .cpt-grid .col-msrp     { width: var(--col-msrp); }
.ps-scope .cpt-grid .col-lowest   { width: var(--col-lowest); }
.ps-scope .cpt-grid .col-where    { width: var(--col-where); }
.ps-scope .cpt-grid .col-margin   { width: var(--col-margin); }

/* Sticky left context columns: Brand / Model / Description never scroll away.
   Widths are pinned and the left offsets derive from the same variables — never
   hand-type a `left`, that is how sticky columns drift into overlapping. */
:root { --stick-1-w: 74px; --stick-2-w: 132px; --stick-3-w: 240px; }
.ps-scope .cpt-grid .stick { position: sticky; background: var(--surface-card); z-index: 2; }
.ps-scope .cpt-grid thead .stick { background: var(--surface-sunk); }
/* Sticky header cells must out-stack the ORDINARY header cells, which scroll
   underneath them. `.col-row th` is specificity 0-3-3, so a plain
   `thead .stick { z-index }` (0-3-1) loses and the scrolled Cat/Decided/MSRP
   headers paint over the pinned Brand/Model/Description. Match the specificity. */
.ps-scope .cpt-grid thead tr.col-row th.stick  { z-index: 6; }
.ps-scope .cpt-grid thead tr.zone-row th.stick { z-index: 7; }
.ps-scope .cpt-grid .stick-1 { left: 0; width: var(--stick-1-w); min-width: var(--stick-1-w); max-width: var(--stick-1-w); }
.ps-scope .cpt-grid .stick-2 { left: var(--stick-1-w); width: var(--stick-2-w); min-width: var(--stick-2-w); max-width: var(--stick-2-w); }
/* Description is the flexible column: no declared width in auto mode, so the table
   hands it the slack. Its own `left` depends only on stick-1/stick-2, and NOTHING to
   its right is sticky — so a changing Description width cannot desync any offset. */
.ps-scope .cpt-grid .stick-3 { left: calc(var(--stick-1-w) + var(--stick-2-w)); min-width: var(--desc-floor); box-shadow: 1px 0 0 var(--line-strong); }
.ps-scope.desc-pinned .cpt-grid .stick-3 { width: var(--stick-3-w); min-width: var(--stick-3-w); max-width: var(--stick-3-w); }

.ps-scope .cpt tbody tr:hover td { background: var(--surface-hover); }
.ps-scope .cpt tbody tr:hover td.stick { background: var(--surface-hover); }

.ps-scope .num { text-align: right; }
.ps-scope .desc-cell { max-width: 0; overflow: hidden; text-overflow: ellipsis; font-family: var(--font-sans); font-size: var(--fs-meta); color: var(--ink-3); }
.ps-scope .cat-cell { font-family: var(--font-sans); font-size: var(--fs-meta); color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; }

/* Cell provenance — feed vs manual must read at a glance. */
.ps-scope .cell-feed { background: var(--feed-soft); }
.ps-scope .cell-feed .cp-input { color: var(--feed); font-weight: 600; }
.ps-scope .cell-manual { background: var(--manual-soft); box-shadow: inset 3px 0 0 var(--manual); }
/* A person checked the retailer's own site and the feed was right: the FEED's number,
   endorsed by a human. So it keeps the feed's purple field and gains the human's orange
   rule and tick -- "confirmed" is a fact about the feed value, not a separate value.
   Deliberately NOT green: .lp (lowest price) is green with !important, so a green
   confirmed cell would read as "this is the low" on every cell that isn't. */
.ps-scope .cell-confirmed { background: var(--feed-soft); box-shadow: inset 3px 0 0 var(--manual); position: relative; }
.ps-scope .cell-confirmed .cp-input { color: var(--feed); font-weight: 700; }
.ps-scope .cell-confirmed::before {
  content: '✓'; position: absolute; left: 5px; top: 50%; transform: translateY(-50%);
  font-size: 11px; font-weight: 700; color: var(--manual); pointer-events: none;
}
.ps-scope .cell-manual .cp-input { color: var(--manual); font-weight: 700; }
.ps-scope .cell-both::after { content: ''; position: absolute; top: 2px; right: 2px; width: 5px; height: 5px; border-radius: 50%; background: var(--feed); }
.ps-scope .cpt td.has-dot { position: relative; }

/* width: 100% is load-bearing. Without it an <input> falls back to its default
   size=20 (~20 characters) and drags the whole column out to ~260px. */
.ps-scope .cp-input {
  width: 100%;
  min-width: 0;
  border: none;
  background: transparent;
  text-align: right;
  font-family: var(--font-mono);
  font-size: var(--fs-grid);
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
  padding: 1px 2px;
}
.ps-scope .cp-input:focus { outline: 2px solid var(--feed); outline-offset: -2px; background: var(--surface-card); }
.ps-scope .cp-input.stale { color: var(--ink-4); font-style: italic; }

.ps-scope .lp { background: var(--good-soft) !important; font-weight: 700; color: var(--good); }
.ps-scope .rc { border-bottom: 2px dashed var(--warn) !important; }   /* price only in cart */
.ps-scope .wk-low { text-align: right; color: var(--ink-3); }
.ps-scope .src-cell { text-align: center; font-weight: 700; color: var(--good); font-family: var(--font-sans); }

/* Price Changes tab — stat cards. Scoped: .stat-card is also used by Claims,
   Snapshots and SalespersonView and is defined in app.css. */
.ps-scope .stat-card { background: var(--surface-card); border: 1px solid var(--line); border-left-width: 4px; border-radius: var(--r-md); padding: var(--s-3) var(--s-4); cursor: pointer; min-width: 128px; transition: box-shadow .12s ease, border-color .12s ease; }
.ps-scope .stat-card:hover { box-shadow: var(--shadow-raise); }
.ps-scope .stat-card-active { box-shadow: var(--shadow-float); border-color: var(--line-strong); }
.ps-scope .stat-card .sc-n { font-size: var(--fs-h3); font-weight: 700; font-family: var(--font-mono); color: var(--ink-1); }
.ps-scope .stat-card .sc-l { font-size: var(--fs-meta); color: var(--ink-3); }

/* --------------------------------------------------------------------------
   7. HEADER WIDTH — one column

   The grid's width is the sum of its column widths (table-layout: fixed), so
   it is computable in CSS from the SAME variables the <colgroup> uses. The
   toolbar, search row and week rail cap at that width, so the page reads as a
   single column instead of the header running past the grid's right edge.

   --grid-w tracks BOTH moving parts automatically:
     * --n-retailers  changes when a retailer checkbox is toggled
     * --stick-3-w    changes when Description is dragged
   Both are set on .ps-scope, which is an ancestor of the header blocks AND of
   the table, so one variable drives the colgroup, the sticky offsets and the
   header cap together. Nothing is hardcoded to 1543.

   The constant is the sum of the fixed columns:
     brand 74 + model 132 + cat 92 + decided 96 + msrp 96
     + history 3x92=276 + lowest 110 + where 64 + margin 84   = 1024
     + 2px for the .cpt-wrap border                            = 1026
   -------------------------------------------------------------------------- */
.ps-scope {
  /* --------------------------------------------------------------------------
     RESPONSIVE MODEL — Description absorbs the slack, prices never move.

     THE TENSION, resolved: table-layout:fixed plus an explicit <col> width for
     Description would pin it. Under fixed layout with `width: 100%`, columns
     that DO declare a width get exactly that width, and any leftover is shared
     among the columns that DON'T. Description is the only column without a
     declared width, so it receives all the slack — and every price column stays
     nailed to its variable. No calc() against the viewport, no guessing at the
     sidebar, and no --grid-w arithmetic that can under-compute and clip.

     The FLOOR is enforced by the table's min-width, not by the column: below
     fixed-sum + 160px the table stops shrinking and the wrap scrolls, which is
     the honest fallback rather than squeezing prices.
     -------------------------------------------------------------------------- */
  --desc-floor: 160px;
  --grid-chrome: 8px;

  /* Everything except Description, summed from the SAME variables the colgroup
     uses so it cannot drift. */
  --grid-fixed-sum: calc(
      var(--stick-1-w) + var(--stick-2-w)
    + var(--col-cat) + var(--col-decided) + var(--col-msrp)
    + (var(--n-retailers, 3) * var(--col-retailer))
    + (3 * var(--col-hist))
    + var(--col-lowest) + var(--col-where) + var(--col-margin)
    + var(--grid-chrome));

  /* Only meaningful when the user has PINNED a Description width. */
  --grid-w: calc(var(--grid-fixed-sum) + var(--stick-3-w));
}

/* width:100% + max-width means: fill the viewport when it is NARROWER than the
   grid (never overflow), cap at the grid when it is wider (no dead band). */
.ps-scope .ps-toolbar,
.ps-scope .wk-rail,
.ps-scope .cpt-wrap,
.ps-scope .row-cap { width: 100%; max-width: 100%; }
/* Pinned: the grid is max-content, so the header blocks cap at it to avoid a dead band. */
.ps-scope.desc-pinned .ps-toolbar,
.ps-scope.desc-pinned .wk-rail,
.ps-scope.desc-pinned .cpt-wrap,
.ps-scope.desc-pinned .row-cap { max-width: var(--grid-w); }

/* --------------------------------------------------------------------------
   8. RESIZABLE DESCRIPTION COLUMN

   The handle sets --stick-3-w on .ps-scope. It must NOT set the <col> width
   directly: --stick-3-w feeds the colgroup width, the cell min/max-width AND
   the `left` offset of every sticky column to its right. Writing the col width
   alone desyncs the sticky columns from the cells they are supposed to pin.
   One variable, one source of truth.
   -------------------------------------------------------------------------- */
.ps-scope .cpt-grid thead th.stick-3 { position: sticky; }   /* containing block for the handle */
.ps-scope .col-resize {
  position: absolute;
  top: 0; right: 0;
  width: 9px; height: 100%;
  cursor: col-resize;
  z-index: 8;                 /* above the sticky header, below nothing that matters */
  user-select: none;
  background: transparent;
  border-right: 2px solid transparent;
}
.ps-scope .col-resize:hover,
.ps-scope .col-resize.is-dragging { border-right-color: var(--feed); }
/* While dragging, kill pointer events on the grid so the drag can't land in a
   cell or start a text selection. */
.ps-scope.is-resizing .cpt-grid tbody { pointer-events: none; }
.ps-scope.is-resizing { cursor: col-resize; user-select: none; }

/* --------------------------------------------------------------------------
   9. RAIL CARDS — coverage display AND the filter control, in one object.

   The card SHOWS coverage (225/405) but CLICKING gives the inverse (the 180
   models with no price there). That inversion is the whole risk of this
   control, so the actionable number is printed under the bar in the same red
   the gap card uses: you click the number you can see.
   -------------------------------------------------------------------------- */
.ps-scope .wk-card {
  flex: 0 0 auto; min-width: 168px;
  padding: var(--s-3) var(--s-4);
  background: var(--surface-card);
  border: 1px solid var(--line); border-radius: var(--r-md);
  cursor: pointer; text-align: left;
  transition: border-color .12s ease, box-shadow .12s ease, background-color .12s ease;
}
.ps-scope .wk-card:hover { border-color: var(--line-strong); box-shadow: var(--shadow-raise); }
.ps-scope .wk-card:focus-visible { outline: 2px solid var(--feed); outline-offset: 2px; }
.ps-scope .wk-card.is-on {
  border-color: var(--ink-1); box-shadow: var(--shadow-float);
  background: var(--surface-sunk);
}
.ps-scope .wk-card.is-on .wk-card-name { color: var(--ink-1); }

.ps-scope .wk-card-top { display: flex; align-items: center; gap: var(--s-2); margin-bottom: var(--s-1); }
.ps-scope .wk-card-name { font-size: var(--fs-meta); font-weight: 700; color: var(--ink-2); white-space: nowrap; }
.ps-scope .wk-badge {
  font-size: 11px; font-weight: 700; letter-spacing: .06em;
  padding: 1px 6px; border-radius: var(--r-pill); white-space: nowrap;
}
.ps-scope .wk-badge-auto { background: var(--feed-soft); color: var(--feed); border: 1px solid var(--feed-line); }
.ps-scope .wk-badge-you  { background: var(--manual-soft); color: var(--manual); border: 1px solid var(--manual-line); }

.ps-scope .wk-card-count {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: 26px; font-weight: 700; color: var(--ink-1); line-height: 1.1;
}
.ps-scope .wk-card-of { font-size: var(--fs-meta); font-weight: 500; color: var(--ink-3); }
.ps-scope .wk-card-missing { font-size: var(--fs-meta); font-weight: 600; color: var(--bad); margin-top: 2px; }

/* Gap card — alarm styling. Same object, different temperature. */
.ps-scope .wk-card-alarm { background: #fdf2f2; border-color: #f0c2c2; }
.ps-scope .wk-card-alarm:hover { border-color: #dc8f8f; }
.ps-scope .wk-card-alarm.is-on { border-color: var(--bad); background: #fbe9e9; }
.ps-scope .wk-card-alarm .wk-card-count { color: var(--bad); }
.ps-scope .wk-card-alarm .wk-card-name { color: #8f2727; }

/* --------------------------------------------------------------------------
   10. RAIL SECOND ROW — chips, run status, reset
   -------------------------------------------------------------------------- */
.ps-scope .wk-rail-2 {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2);
  padding-top: var(--s-3); margin-top: var(--s-3);
  border-top: 1px solid var(--line-soft);
}
.ps-scope .chip {
  display: inline-flex; align-items: center; gap: var(--s-2);
  height: 32px; padding: 0 var(--s-3);
  font-size: var(--fs-meta); font-weight: 600;
  border: 1px solid var(--line); border-radius: var(--r-pill);
  background: var(--surface-card); color: var(--ink-2);
  cursor: pointer; white-space: nowrap;
}
.ps-scope .chip-n { font-family: var(--font-mono); font-variant-numeric: tabular-nums; color: var(--ink-3); }
.ps-scope .chip.is-on { background: var(--ink-1); color: #fff; border-color: var(--ink-1); }
.ps-scope .chip.is-on .chip-n { color: #fff; }
/* Muted = visible but inert. A chip reading 0 is information ("you and the feed
   agree"), so it stays on screen rather than materialising later. */
.ps-scope .chip-muted { opacity: .55; cursor: default; background: var(--surface-sunk); }
.ps-scope .chip-static { cursor: default; background: var(--surface-sunk); color: var(--ink-3); font-weight: 500; }
.ps-scope .chip-reset { margin-left: auto; border-color: var(--line-strong); color: var(--ink-1); }
.ps-scope .chip-reset:disabled { opacity: .35; cursor: default; }
.ps-scope .wk-runstatus { font-size: var(--fs-meta); color: var(--ink-3); white-space: nowrap; }

/* Filter summary — must never let the count be mistaken for the row count. */
.ps-scope .filter-note {
  font-size: var(--fs-meta); color: var(--ink-3);
  padding: var(--s-2) 0;
}
.ps-scope .filter-note strong { color: var(--ink-1); }

/* A row present ONLY because its laundry mate matched. Quieter, marked, uncounted. */
.ps-scope .cpt-grid tbody tr.is-mate td { background: #fbfbfc; color: var(--ink-4); }
.ps-scope .cpt-grid tbody tr.is-mate td.stick { background: #fbfbfc; }
.ps-scope .cpt-grid tbody tr.is-mate:hover td { background: var(--surface-hover); }
/* The marker must not sit in the model-number text — that breaks the column's scan line.
   Instead: a quiet left rule on the row, plus a glyph parked at the cell's right edge in
   space no text uses. Both are hoverable for the explanation. */
.ps-scope .cpt-grid tbody tr.is-mate td.stick-1 { box-shadow: inset 3px 0 0 var(--ink-4); }
.ps-scope .cpt-grid tbody tr.is-mate td.stick-2 { position: sticky; }
.ps-scope .mate-marker {
  position: absolute; right: 5px; top: 50%; transform: translateY(-50%);
  width: 14px; height: 14px; line-height: 13px; text-align: center;
  font-size: 10px; font-weight: 700; cursor: help;
  border-radius: 50%; background: var(--surface-sunk);
  color: var(--ink-3); border: 1px solid var(--line);
}
