/* ===== PRODUCTS — the Sage catalogue ======================================
   88,008 products, 78,907 of which have never sold. The page leads with that
   ratio because a list of 88,008 rows cannot say it.

   Prefix .pd-*. A single series is not encoding identity, so every bar here is
   gold as chrome; a category is not a brand and must never borrow a brand
   colour.                                                                  */

.pd-src { margin-left: auto; font-size: var(--fs-xs); color: var(--text-dim); font-weight: var(--fw-medium); }
.pd-dim { color: var(--text-dim); font-weight: var(--fw-normal); }
.pd-strong { font-weight: var(--fw-bold); }
.pd-empty {
  margin: 0; padding: 1.6rem 1rem; text-align: center;
  font-size: var(--fs-md); color: var(--text-dim);
}
.pd-empty i { display: block; font-size: 1.3rem; margin-bottom: .4rem; opacity: .55; }
.pd-note { margin: .5rem 0 0; font-size: var(--fs-xs); color: var(--text-dim); line-height: 1.5; }

/* ---- tables -------------------------------------------------------------- */
.pd-tbl { font-size: var(--fs-sm); overflow-x: auto; }
.pd-tr {
  display: grid; align-items: center; gap: .7rem;
  padding: .42rem .1rem; border-bottom: 1px solid var(--border-soft);
}
.pd-tr:last-child { border-bottom: 0; }
.pd-tr > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pd-tr .n { text-align: right; font-variant-numeric: tabular-nums; }
.pd-th {
  font-size: var(--fs-2xs); font-weight: var(--fw-bold); text-transform: uppercase;
  letter-spacing: .05em; color: var(--text-dim); border-bottom-color: var(--border);
}
/* A CREAM FILL ON A NEAR-BLACK BAND. This rule was `.pd-click:hover
   { background: var(--accent-weak) }` — unscoped, so it matched every carrier of
   .pd-click including the two that sit ON THE DARK LEAD BAND: .pd-bk (the five
   headline figures) and .pd-seg (the sold / never-sold bar). --accent-weak is
   --gold-100, #F8F1E2, so hovering a headline figure painted a near-white slab
   over --shell-800 and the white figure on it became unreadable. Reported from
   a screenshot, 2026-09-09.

   IT WAS ALSO REDUNDANT EVERYWHERE IT WAS CORRECT. Every element that carries
   .pd-click already declares its own hover: .pd-fact lifts (line ~500),
   .pd-rk and .pd-tr take --accent-weak in their own rules, .pd-seg brightens
   and lifts. So the fill is deleted rather than scoped — there is nothing left
   for it to do.

   THE GENERAL RULE, and it is why this recurred: a hover that names a LIGHT
   token is only correct on a light ground. Inside a dark band the idiom is a
   white wash at low alpha — `rgba(255,255,255,.08)`, which is what the dark
   rail already uses (layout.css) — because it composites over whatever the band
   is doing instead of replacing it. Scope any such rule to the ground it was
   written for. */
.pd-click { cursor: pointer; }

/* The slack goes to the column that is a PICTURE, never to a run of numerics. */
.pd-tbl-cat .pd-tr {
  /* THE BAR AND THE NUMBER IT WAS DRAWN FROM MUST STAY WITHIN SIGHT OF EACH
     OTHER - the rule the ranked panels have now been brought into line with
     too. What is left here is a meter, and a meter is a share of a whole, so
     it keeps a track: the row's own count is the whole and it is one cell to
     the left. It sits UNDER its figure and shares that figure's right edge.

     EVERY TRACK IS FLEXIBLE AND THE TABLE FILLS ITS CELL. Bounding all six
     stopped a 540px grey rail opening in the middle of the row, and then left
     390px of empty white on the right instead - measured at 1600px, the
     revenue column ended at x=1169 inside a cell running to x=1557. Even
     tracks put the total on the panel's right edge, where the eye already
     looks for one, and nothing has to absorb the slack alone. */
  grid-template-columns:
    minmax(92px, .8fr) minmax(84px, .85fr) minmax(120px, 1fr)
    minmax(88px, .9fr) minmax(100px, .95fr) minmax(112px, 1.05fr);
}
/* THE COLUMNS FOLLOW THE SET. `--pd-cols` is written once per table by
   _fitCols, not as an inline style on each of the fifty rows, and the fallback
   is the full seven-column layout - so a table that never gets the property
   renders exactly as it did before rather than collapsing to one column. */
/* The DEFAULT set, defined here rather than left to a fallback inside var() -
   so the token is a real declaration a sweep can see, and a table that carries
   no override still lays out as the full seven columns.
   TWO TRACKS ARE rem, NOT px. Fixed at the type floor (D7) and the TV tier
   (D6) both raised what these hold without raising the track: "Customers"
   clipped at 90px (needs 103) and a price range at 132px (needs 137-148 on 19
   rows), at 2560px both worse again because the figure grows and the track
   does not. 90px -> 5.625rem and 132px -> 8.25rem are the same width at a
   16px root and scale with everything else in the row above 1920px. The
   other four numbers are unaffected by either change and stay px. */
.pd-tbl-list {
  --pd-cols: minmax(240px,1.7fr) minmax(160px,1.1fr) 5.625rem 96px 8.25rem 112px;
}
.pd-tbl-list .pd-tr { grid-template-columns: var(--pd-cols); }
/* The routing code, where the money columns left room for it. */
.pd-catcode { font-size: var(--fs-xs); color: var(--text-soft); }
.pd-tbl-cust .pd-tr { grid-template-columns: minmax(180px,1.8fr) 116px 84px 128px 104px; }
.pd-tbl-ord  .pd-tr { grid-template-columns: minmax(120px,1fr) 92px 116px 80px 104px minmax(120px,1fr); }
/* Every order the same kind, so the column is gone and its width goes to the
   reference and the invoice number, which are what a reader actually looks up. */
.pd-tbl-ord.is-onekind .pd-tr { grid-template-columns: minmax(130px,1fr) 116px 80px 104px minmax(130px,1fr); }
/* A RIGHT-ALIGNED FIGURE AGAINST A LEFT-ALIGNED CODE. Value ends where Invoice
   begins, so the gap between them was the grid's .7rem and "$81 INV260217326"
   read as one token - the header "VALUE INVOICE" likewise. The invoice column
   takes its own lead so the two halves separate. */
.pd-tbl-ord .pd-tr > span:last-child { padding-left: 1.1rem; }

.pd-bar {
  height: 8px; border-radius: 4px; background: var(--inset);
  border: 1px solid var(--border-soft); overflow: hidden;
}
.pd-bar > span {
  display: block; height: 100%; border-radius: 3px;
  background: linear-gradient(90deg, var(--gold-400), var(--gold-600));
}

/* ---- a product in the list ---------------------------------------------- */
.pd-p, .pd-f, .pd-cu { display: flex; flex-direction: column; gap: .04rem; min-width: 0; }
.pd-p-n { font-weight: var(--fw-medium); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pd-p-m, .pd-f > span:last-child {
  font-size: var(--fs-xs); color: var(--text-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pd-tags { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: var(--fs-2xs); opacity: .9; }

.pd-pill {
  display: inline-block; padding: .06rem .4rem; border-radius: 999px;
  font-size: var(--fs-2xs); font-weight: var(--fw-bold); line-height: 1.5; white-space: nowrap;
  border: 1px solid transparent;
}
/* COLOUR MARKS THE EXCEPTION, NOT THE NORM. `Active` is the state of nine rows
   in ten, so a filled green pill on every one of them was fifty identical
   lozenges competing for attention and pointing at nothing - the reader has to
   scan past all of them to find the one row that differs. Active is quiet;
   discontinued, still-selling and never-sold keep their colour, and every one
   still prints its state in words. */
.pd-pill.ok   { background: transparent; color: var(--text-dim); border-color: var(--border); font-weight: var(--fw-medium); }
.pd-pill.warn { background: var(--warn-weak); color: var(--warn-ink); border-color: var(--warn-line); }
.pd-pill.bad  { background: var(--bad-weak);  color: var(--bad-ink);  border-color: var(--bad-line); }
.pd-pill.alt  { background: var(--inset);     color: var(--text-dim); border-color: var(--border); }

/* THE RATIO REPLACED THE COLOUR, and the reason is arithmetic. 2x is genuinely
   the exception over the book - 566 of the 3,253 products that have any range
   at all, 17%, against a median spread of 1.32 - but the default sort is
   most-revenue-first and the widest spreads cluster at the top of it: 22 of the
   39 ranged rows on page one came out gold. A mark carried by the majority
   marks nothing, and this stylesheet's own rule two blocks up is that colour
   marks the exception.
   A figure is not a claim about exceptionality, it is the measurement - and it
   says strictly more than the colour did, which painted 35,600x and 2.05x
   identically. The 566 and the 2x threshold keep their home on the fact card
   and the sort, where they are statements about the whole catalogue and true. */
.pd-px { display: flex; flex-direction: column; align-items: flex-end; gap: .02rem; min-width: 0; }
.pd-pxr { font-weight: var(--fw-bold); font-variant-numeric: tabular-nums; max-width: 100%;
          overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pd-pxx { font-size: var(--fs-xs); color: var(--text-dim); font-variant-numeric: tabular-nums; }
/* The second word of a two-value header, sitting directly over the second line
   of the cell. --text-dim on --surface is 3.76:1 light and 5.07:1 dark; it is a
   label, which is the only thing that token is held to 3:1 for.
   FLOORED, NOT FLAT (D7). .92em of --fs-xs is 10.67px, under the 11px floor.
   A flat token would make it bigger than its neighbours wherever .92em
   already clears the floor on its own, so this is the same `max()` guard
   base.css uses for <code>: relative where the relative size already reads,
   the floor only where it would not. Both terms follow the root. */
.pd-th-s {
  display: block; font-style: normal; font-size: max(.92em, var(--fs-3xs));
  font-weight: var(--fw-medium); opacity: .78; letter-spacing: .04em;
}

/* ---- one product, opened ------------------------------------------------- */
.pd-wide { max-width: 1400px; }
.pd-d { background: var(--inset); }
.pd-d-h {
  display: flex; align-items: flex-start; gap: 1rem; flex-wrap: wrap;
  padding: 1rem 1.2rem .8rem; background: var(--surface);
  border-bottom: 1px solid var(--border-soft);
}
.pd-d-t { min-width: 0; flex: 1 1 320px; }
/* No modal-title above this: the header IS the title, and printing the name
   twice is a fault this project already records on the sign-in card. */
.pd-d-t h3 { margin: 0; font-size: 1.16rem; font-weight: var(--fw-bold); line-height: 1.25; }
.pd-d-m { font-size: var(--fs-sm); color: var(--text-dim); margin-top: .2rem; }
/* The window every figure in this dialog is bounded by, stated where the
   reader lands rather than left behind on the band they came from.
   --text-soft #55555C on --inset #ECEDF1 is 6.32:1 light, 10.25:1 dark. */
.pd-d-win {
  display: inline-flex; align-items: center; gap: .35rem; margin-top: .45rem;
  padding: .14rem .5rem; border-radius: 999px; cursor: help;
  background: var(--inset); border: 1px solid var(--border);
  font-size: var(--fs-xs); font-weight: var(--fw-medium); color: var(--text-soft);
}
.pd-d-win i { font-size: var(--fs-xs); opacity: .75; }
.pd-d-st { display: flex; align-items: center; gap: .35rem; flex-wrap: wrap; }
/* The seller's lockup, at the chip's own 28px - a 15px cap here lost to the
   --brand-logo-min floor anyway, and was the reason for the badge-plus-name. */
.pd-d-co { display: inline-flex; align-items: center; }
.pd-co { font-size: var(--fs-xs); color: var(--text-dim); font-weight: var(--fw-medium); }

.pd-d-k {
  display: grid; gap: .7rem; padding: .9rem 1.2rem;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  background: var(--surface); border-bottom: 1px solid var(--border-soft);
}
.pd-k { display: flex; flex-direction: column; gap: .05rem; min-width: 0; }
.pd-k-l {
  font-size: var(--fs-2xs); font-weight: var(--fw-bold); text-transform: uppercase;
  letter-spacing: .06em; color: var(--text-dim);
}
.pd-k-v { font-size: 1.24rem; font-weight: var(--fw-bold); font-variant-numeric: tabular-nums; }
.pd-k-s { font-size: var(--fs-xs); color: var(--text-dim); }

.pd-d-grid { display: grid; gap: .9rem; padding: .9rem 1.2rem 1.1rem; grid-template-columns: 1fr; }
/* 1200, NOT 1000. Between the two the 320px aside left the main column 566px,
   and the customers and orders tables - 659px and 607px of tracks - scrolled
   sideways inside it at 1024. Below 1200 the aside drops under the tables. */
@media (min-width: 1200px) { .pd-d-grid { grid-template-columns: minmax(0,1fr) 320px; } }
.pd-d-main, .pd-d-side { display: flex; flex-direction: column; gap: .9rem; min-width: 0; }
/* Not stretching is what lets it sit at the top rather than becoming a tall
   box holding one card - and STICKY is what stops it scrolling away while the
   reader is 25 orders down the main column, which is where the catalogue facts
   and the price range are most wanted. */
.pd-d-side { align-self: start; }
@media (min-width: 1200px) {
  .pd-d-side { position: sticky; top: .9rem; }
}

/* The dialog had a full-width footer holding one Close button - forty pixels
   of chrome for something the header already provides on every other dialog in
   this app. The X floats over the head band instead. */
.pd-d-x {
  position: absolute; top: .85rem; right: 1rem; z-index: 3;
  opacity: .55;
}
/* ...and the head has to make room for it, or it lands on top of the status
   pill and the brand name, which is where it was sitting. */
.pd-d-h { padding-right: 3.3rem; }
.pd-d-x:hover { opacity: 1; }

.pd-card {
  background: var(--surface); border: 1px solid var(--border-soft);
  border-radius: 10px; padding: .8rem .95rem; box-shadow: var(--shadow);
}
.pd-card-h {
  display: flex; align-items: baseline; gap: .5rem;
  font-size: var(--fs-lg); font-weight: var(--fw-bold); margin-bottom: .5rem;
}

/* Usage. Two months of data drawn across a wide plot reads as a broken chart,
   so a short series is drawn short. */
/* TWO BARS ARE NOT A CHART. Under four buckets the months are stated as
   figures instead - see _detUsage. `.is-short` went with that: it capped the
   plot at 220px, which made two fat rectangles in a full-width card rather
   than fixing what was wrong with drawing them at all. */
.pd-mocells {
  display: grid; gap: .5rem;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.pd-mocell {
  display: flex; flex-direction: column; gap: .1rem; min-width: 0;
  padding: .5rem .65rem; border-radius: 9px;
  background: var(--inset); border: 1px solid var(--border-soft);
}
.pd-mocell-m {
  font-size: var(--fs-2xs); font-weight: var(--fw-bold); letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-dim);
}
.pd-mocell-v {
  font-size: var(--fs-xl); font-weight: var(--fw-bold); line-height: 1.15;
  font-variant-numeric: tabular-nums; color: var(--text);
}
.pd-mocell-s { font-size: var(--fs-xs); color: var(--text-soft); }

/* One route at 100% is a sentence, not a bar. */
.pd-one { margin: 0; font-size: var(--fs-sm); line-height: 1.5; color: var(--text-soft); }
.pd-one b { color: var(--text); font-weight: var(--fw-bold); }

.pd-months { display: flex; align-items: flex-end; gap: .35rem; height: 116px; }
.pd-mo { flex: 1 1 0; display: flex; flex-direction: column; align-items: center;
         justify-content: flex-end; height: 100%; min-width: 0; cursor: help; }
.pd-mo-b {
  width: 100%; max-width: 52px; border-radius: 4px 4px 0 0;
  background: linear-gradient(180deg, var(--gold-400), var(--gold-600));
}
.pd-mo-l { font-size: var(--fs-2xs); color: var(--text-dim); margin-top: .25rem; white-space: nowrap; }

/* The one fact worth having about a product that has never sold: whether its
   whole category is dead or only it. --text-soft #55555C on --surface #FFFFFF
   is 7.39:1 light, and #C4BDAE on #171613 is 9.68:1 dark. MEASURED - 11.19
   was written here first, from memory, and was wrong by a point and a half. */
.pd-catctx {
  margin: 0; padding: .6rem .75rem; border-radius: 9px;
  background: var(--inset); border: 1px solid var(--border-soft);
  font-size: var(--fs-sm); line-height: 1.55; color: var(--text-soft); text-align: center;
}
.pd-catctx b { color: var(--text); font-weight: var(--fw-bold); }
.pd-catctx code {
  font-size: .95em; padding: .05rem .3rem; border-radius: 4px;
  background: var(--surface); border: 1px solid var(--border); color: var(--text);
}

.pd-fields { display: flex; flex-direction: column; gap: .4rem; }
.pd-fd { display: flex; flex-direction: column; gap: .02rem; min-width: 0; }
.pd-fd-k { font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: .05em; color: var(--text-dim); }
.pd-fd-v { font-size: var(--fs-sm); font-weight: var(--fw-medium); overflow-wrap: anywhere; }

/* The spread, held at one quantity. */
.pd-price { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: .5rem; }
.pd-pr-lo, .pd-pr-hi { font-size: var(--fs-md); font-weight: var(--fw-bold); font-variant-numeric: tabular-nums; }
.pd-pr-track {
  height: 6px; border-radius: 3px; background: var(--inset);
  border: 1px solid var(--border-soft); overflow: hidden;
}
.pd-pr-fill { display: block; height: 100%; width: 100%; background: var(--border); }
/* A PRICE RANGE IS NOT A STATUS. This was a green-amber-red ramp, which says
   the low price is good and the high one is bad - a judgement the data does not
   support, since the high price may be the list price and the low one the
   unjustified discount. ok/warn/bad are reserved for state in this app and are
   never borrowed for a measure. One hue, running light to dark across the
   range; the sentence underneath carries the actual finding. */
.pd-price.is-wide .pd-pr-fill {
  background: linear-gradient(90deg, var(--gold-200), var(--gold-600));
}
.pd-price.is-wide .pd-pr-hi { color: var(--accent-ink); font-weight: var(--fw-bold); }
/* A PRICE SPREAD IS NOT A WARNING. This was --warn-ink behind a warning
   triangle, at a 1.25 threshold that sits BELOW the book's own median spread of
   1.32 - so the app's state colour appeared on more than half of everything
   that has ever sold twice. ok/warn/bad mean state here and are never borrowed
   for a measure; the ratio is set in gold, which is chrome, and the sentence
   around it carries the finding.
   MEASURED: --accent-ink is --gold-700 #75551B on --surface #FFFFFF = 6.84:1
   light, and --gold-300 #DFC593 on #171613 = 10.81:1 dark. */
.pd-spreadline {
  margin: .5rem 0 0; font-size: var(--fs-sm); line-height: 1.45; color: var(--text-soft);
}
.pd-spreadline b {
  color: var(--accent-ink); font-weight: var(--fw-bold); font-variant-numeric: tabular-nums;
}

.pd-routes { display: flex; flex-direction: column; gap: .35rem; }
.pd-rt { display: grid; grid-template-columns: minmax(88px,1fr) minmax(60px,1.1fr) 44px;
         align-items: center; gap: .5rem; font-size: var(--fs-sm); }
.pd-rt-k { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pd-rt .n { text-align: right; font-variant-numeric: tabular-nums; font-weight: var(--fw-bold); }

@media (max-width: 1000px) {
  /* Scoped to `.is-full`: these nth-child positions only mean what they say
     when all seven columns are present, and the set now decides that. */
  /* The full set is SIX columns since Status stopped being one - Product,
     Family, Customers, Units, Unit price, Revenue - so the positions hidden
     here had to move with it. Family and Customers give way; the price stays,
     because it is the column this page rebuilt itself around. */
  .pd-tbl-list.is-full .pd-tr {
    grid-template-columns: minmax(160px,2fr) 92px 136px 100px;
  }
  .pd-tbl-list.is-full .pd-tr > span:nth-child(2),
  .pd-tbl-list.is-full .pd-tr > span:nth-child(3) { display: none; }
  /* A PERCENTAGE WHOSE BASE IS OFF SCREEN. This dropped nth-child(2) - the
     products count AND the bar drawn from it - so at 1000px "4,527 (14%)" sat
     with nothing on the row to be 14% of, and the panel lost its only chart.
     Never sold is products minus sold and Discontinued is the least-read
     column here, so those two give way instead. */
  .pd-tbl-cat .pd-tr { grid-template-columns: minmax(74px,92px) minmax(90px,1fr) 140px 116px; }
  .pd-tbl-cat .pd-tr > span:nth-child(4), .pd-tbl-cat .pd-tr > span:nth-child(5) { display: none; }
}

/* ===== THE LEAD IS A DARK BAND =====
   The page was four white panels of equal weight, so nothing was dominant and
   the eye had nowhere to land. --shell-800 is the app's own dark surface - the
   navigation rail, the tooltips, the toasts and the sign-in canvas all sit on
   it, in BOTH themes - so this is the house voice rather than a new one, and it
   is the one ground on which this app's gold actually reads as gold.

   THE COMPONENT LIVES IN base.css NOW - the ground, the gold wash, the
   stacking context, the top row and the dark-mode lift are shared with the
   four other pages that lead with this band. What is left here is what is
   genuinely different about the Products one. */
.pd-band {
  margin: 0 0 1rem; padding: 1.15rem 1.35rem 1.3rem;
  /* The one band whose ink is --shell-ink rather than plain white: the segment
     labels below inherit it, and this warm grey is what they were measured on
     (10.14:1 at the light end of the sold gradient, 5.47:1 at the dark). */
  color: var(--shell-ink);
}

/* Baseline, not centre: the eyebrow and the window chip are set at different
   sizes and this is the only band whose top row reads as one line of type. */
.pd-band-top {
  align-items: flex-start;
  margin-bottom: 1.15rem;
}
/* WHITE, NOT GOLD. A gold label directly above the gold headline figure gives
   the band two focal points and therefore none - Entities settled this and its
   comment says so. This one survived because the harness asserted "exactly one
   gold figure" by counting the markup class, which cannot fail when a
   different rule is painted gold. See scripts/lib/band-gold.js. 9.88:1 on
   #141310. The one gold ink left on this band besides the figure is
   `.pd-band-more`, which is an underlined text LINK inside the note - an
   affordance, which this design system calls chrome, not a second figure. */
.pd-band-ttl { min-width: 0; }
.pd-band-h {
  margin: 0; font-size: var(--fs-2xl); font-weight: var(--fw-bold); letter-spacing: -.02em;
  line-height: 1.15; color: #FFFFFF;
}
/* The Home page's flow subtitle: uppercase, tracked, a gold UNDERLINE. The
   gold is a border, not ink, so the one-gold-figure count is unchanged. */
.pd-band-eyebrow {
  display: inline-block; margin-top: .3rem; padding-bottom: .3rem;
  font-size: var(--fs-2xs); font-weight: var(--fw-medium); letter-spacing: .16em;
  text-transform: uppercase; color: rgba(255, 255, 255, .78);
  border-bottom: 2px solid var(--gold-400);
}
.pd-band-win {
  margin-left: auto; display: inline-flex; align-items: center; gap: .35rem;
  padding: .16rem .55rem; border-radius: 999px;
  background: rgba(255,255,255,.06); border: 1px solid var(--shell-line);
  font-size: var(--fs-xs); font-weight: var(--fw-medium); color: rgba(255,255,255,.72);
  cursor: help; white-space: nowrap;
}
.pd-band-win i { font-size: var(--fs-xs); opacity: .8; }

/* Five figures across. auto-fit rather than a fixed five so the row reflows
   instead of overflowing on a laptop, and 132px is the width at which the
   longest of them ($11,045,605) still fits on one line. */
.pd-band-kpis {
  display: grid; gap: .5rem 1.5rem;
  grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
  padding-bottom: 1.05rem;
}
.pd-bk { min-width: 0; cursor: help; }
.pd-bk-v {
  /* line-height 1.1 shaved 4px off the 2.5rem lead figure: `overflow: hidden`
     is here for the ellipsis on a long figure, so the LINE BOX has to be the
     thing that grows. Measured, not guessed - the browser reported the figure
     needing 48px in a 44px box. */
  font-size: 1.42rem; font-weight: var(--fw-bold); line-height: 1.22;
  letter-spacing: -.015em; font-variant-numeric: tabular-nums;
  color: #FFFFFF;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pd-bk-v em { font-style: normal; font-size: .62em; font-weight: var(--fw-bold); opacity: .8; }
/* ONE figure is gold. Two would leave the band with no focal point, which is
   the whole fault this rebuild exists to fix. */
.pd-bk.is-lead .pd-bk-v { font-size: 2.5rem; color: var(--gold-300); }
.pd-bk-l {
  margin-top: .18rem; font-size: var(--fs-xs); line-height: 1.35;
  color: rgba(255,255,255,.58);
}
.pd-bk.is-lead .pd-bk-l { color: rgba(255,255,255,.78); font-weight: var(--fw-medium); }
/* THE BASE, ON THE CARD. A ratio whose denominator can only be reached by
   hovering has no denominator for anybody who is reading rather than pointing -
   and "10.3% has ever sold" was exactly that, with its 88,008 one chip away and
   nothing joining them. Smaller than the label above it, because it is the
   label's evidence rather than a second label.
   White at .5 on #141310 measures 5.42:1. MEASURED, not assumed. */
.pd-bk-s {
  margin-top: .1rem; font-size: var(--fs-2xs); line-height: 1.35;
  color: rgba(255,255,255,.5); font-variant-numeric: tabular-nums;
}
.pd-bk.is-lead .pd-bk-s { color: rgba(255,255,255,.62); }

/* THE EXCLUSION AND ITS COUNT, STATED ON THE CARD. The page led with 10.3% and
   the category panel one screen lower said 23,336 products sit in categories
   that have never sold anything - two facts a reader could not reconcile and no
   way to ask where 10.3 comes from.

   It is SMALL PRINT and must stay small print: it is the argument behind the
   figure, not a fifth figure. At body size across the full band it competed
   with the five numbers above it, which is the opposite of what a hero is for.
   White at .58 on #141310 measures 6.76:1; the emphasised figures inside it sit
   at .88 (13.9:1). */
.pd-band-note {
  margin: .85rem 0 0; padding-top: .8rem;
  border-top: 1px solid var(--shell-line);
  font-size: var(--fs-xs); line-height: 1.6; max-width: 108ch;
  color: rgba(255,255,255,.58);
}
.pd-band-note b { color: rgba(255,255,255,.88); font-weight: var(--fw-bold);
                  font-variant-numeric: tabular-nums; }
/* A TEXT BUTTON, NOT A <BUTTON>. Left to the browser it drew a system-chrome
   push button in the middle of a sentence on a dark band - a control that
   belongs to the operating system rather than to this page. */
.pd-band-more {
  padding: 0; border: 0; background: none; font: inherit;
  color: var(--gold-300); font-weight: var(--fw-bold); cursor: pointer;
  text-decoration: underline; text-underline-offset: 3px;
  text-decoration-color: rgba(223,197,147,.4);
}
.pd-band-more:hover { color: var(--gold-200); text-decoration-color: currentColor; }
.pd-band-more::after { content: ' \203A'; text-decoration: none; }
/* Neutral white at varying alpha, never --shell-dim: that token is a warm grey
   tuned for small amounts of text, and across a whole row it reads as coffee.
   Same reason the navigation rail carries plain white.

   MEASURED on #141310, not assumed - every one clears AA:
     white          18.58:1     gold-300 (lead figure, eyebrow)  11.10:1
     white .78      11.44:1     white .72 (window chip)           9.88:1
     white .62       7.55:1     white .58 (kpi labels)            6.76:1
   The sold segment's near-black ink measures 10.14:1 at the light end of its
   gradient and 5.47:1 at the dark end. The gradient ENDS at --gold-500 for
   exactly that reason: --gold-550 was the first choice and measures 4.49:1,
   which is below AA for the .68rem label sitting on it. White on that gold
   would be 3.1:1 - the same trap the gold button in the emails records. */

.pd-band-bar {
  display: flex; gap: 3px; height: 54px;
  padding: 3px; border-radius: 11px;
  background: rgba(0,0,0,.28); border: 1px solid var(--shell-line);
}
/* A 2px surface gap between adjacent fills is the mark spec; here it is 3px of
   the band showing through, which does the same job on a dark ground. */
.pd-seg {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  min-width: 0; overflow: hidden; border-radius: 8px;
  transition: filter .16s ease, transform .16s ease;
  cursor: help;
}
.pd-seg:hover { filter: brightness(1.09); transform: translateY(-1px); }
.pd-seg b {
  font-size: var(--fs-xl); font-weight: var(--fw-black); line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.pd-seg i { font-style: normal; font-size: var(--fs-2xs); opacity: .88; white-space: nowrap; }
/* Sold takes the gold. Never-sold is deliberately quiet - it is nine tenths of
   the bar, and colouring it loudly would make the page read as an alarm rather
   than as a position. */
.pd-seg.is-sold {
  background: linear-gradient(135deg, var(--gold-300), var(--gold-500));
  color: #241B0C;
}
/* NINE TENTHS OF THE BAR, so a centred label floats a thousand pixels from
   the boundary it describes and reads as unattached to anything. It sits just
   past the divide instead, where the eye already is. */
.pd-seg.is-never {
  background: rgba(255,255,255,.07); color: rgba(255,255,255,.62);
  align-items: flex-start; padding-left: .9rem;
}

@media (prefers-reduced-motion: reduce) {
  .pd-seg { transition: none; }
  .pd-seg:hover { transform: none; }
}

/* Three findings, as cards rather than grey chips. */
.pd-facts {
  display: grid; gap: 1.1rem; margin: 0 0 1.1rem;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
/* THE HOME PAGE'S KPI CARD: icon tile and label, the figure, the sentence it
   completes, and the method as the footer. Tone colours the ICON TILE only -
   a warning is a state, so it takes warn; the others are neutral chrome. */
.pd-fact {
  --pd-weak: var(--accent-weak); --pd-ink: var(--accent-ink);
  display: flex; flex-direction: column; cursor: help; min-width: 0;
  padding: 1rem 1.15rem 1rem; border-radius: var(--r); line-height: 1.4;
  background: var(--surface); border: 1px solid var(--border);
  box-shadow: var(--shadow);
  transition: transform .25s var(--ease), box-shadow .25s var(--ease), border-color .2s var(--ease);
  animation: pd-rise .5s var(--ease-out) backwards;
}
.pd-fact:nth-child(2) { animation-delay: 60ms; }
.pd-fact:nth-child(3) { animation-delay: 120ms; }
.pd-fact:hover { transform: translateY(-3px); box-shadow: var(--lift-2); border-color: var(--accent-line); }
.pd-fact.is-warn { --pd-weak: var(--warn-weak); --pd-ink: var(--warn-ink); }
.pd-fact.is-bad  { --pd-weak: var(--bad-weak);  --pd-ink: var(--bad-ink); }
.pd-fact-h { display: flex; align-items: center; gap: .6rem; min-width: 0; }
.pd-fact-ico {
  width: 36px; height: 36px; border-radius: 10px; flex: 0 0 36px;
  display: grid; place-items: center; font-size: 1.05rem;
  background: var(--pd-weak); color: var(--pd-ink);
}
.pd-fact-l { font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--text-soft); min-width: 0; }
.pd-fact-go { margin-left: auto; color: var(--text-dim); font-size: var(--fs-sm); transition: color .2s var(--ease), transform .2s var(--ease); }
.pd-fact:hover .pd-fact-go { color: var(--accent-ink); transform: translate(2px, -2px); }
.pd-fact-t { display: block; font-size: var(--fs-sm); color: var(--text-soft); min-width: 0; margin-top: .6rem; }
.pd-fact-t b {
  display: block; font-size: var(--fs-3xl); font-weight: var(--fw-bold); color: var(--text);
  font-variant-numeric: tabular-nums; line-height: 1.05; letter-spacing: -.02em;
}
.pd-fact-t em { display: block; font-style: normal; margin-top: .3rem; color: var(--text); font-weight: var(--fw-medium); }
.pd-fact-n {
  display: block; margin-top: .55rem; padding-top: .55rem; border-top: 1px solid var(--border-soft);
  font-size: var(--fs-xs); line-height: 1.45; color: var(--text-soft);
}
@media (prefers-reduced-motion: reduce) {
  .pd-fact { transition: none; animation: none; }
  .pd-fact:hover { transform: none; }
}
.pd-rank { display: flex; flex-direction: column; gap: 2px; min-width: 0; }

.pd-rk {
  position: relative; display: grid; align-items: center;
  /* FOUR TRACKS, and the fourth holds the bar AND the figure it measures. The
     bar used to be the row's own background and the value track was 104px; the
     value track is 232px now and the bar lives in it, so length and number are
     read together instead of 1,350px apart. */
  /* THE SLACK GOES TO THE BAR, which is this row's picture - the rule this
     stylesheet already applies to the category table. Given to the name
     instead, the name track ran to 900px against names that end around 200,
     and the % column floated alone in the middle of the white. */
  /* REVENUE THIRD, % INVOICED LAST. The percentage used to sit between the
     name and the bar, directly under which the bar began - so "9%" and the
     longest revenue bar on the panel read as one cell. The bar now runs from
     the name to the figure it was drawn from, and the share-of-range column
     closes the row as its own, quieter fact. */
  /* NAME AND % TRACKS ARE rem, NOT px (D6/D7). At the 11px type floor a name
     needs 257px against a 240px track, and "% invoiced" needs 91px against
     76 - both worse at the TV tier, where the figure grows and a px track
     does not. 15rem and 4.75rem are the same width at a 16px root and follow
     the scaled root above 1920px. The rank number (22px) is untouched: it is
     never more than two digits and was never reported clipped. */
  grid-template-columns: 22px minmax(0, 15rem) minmax(0, 1fr) 4.75rem;
  gap: .55rem; padding: .5rem .65rem; border-radius: 8px;
  font-size: var(--fs-md); cursor: help;
  transition: background .12s ease;
}
/* No lift and no nudge. Ten rows that each move on hover is a lot of motion for
   a list somebody reads down; a quiet tint says the row is live and nothing
   under the pointer shifts. */
.pd-rk.pd-click:hover { background: var(--accent-weak); }

/* THE BAR, IN ITS OWN TRACK.
   It was `.pd-rk-fill` absolutely positioned across the whole row - a cream
   plate behind the text - and that cost three things at once:

     - the fill ended mid-word on 9 of the 10 rows, because its length is the
       value and the value has nothing to do with where a name ends;
     - ten of them stacked made a wall of beige, which is most of what "all the
       gray" was pointing at;
     - rounded on all four corners with a 2px gold cap, each row read as a
       lozenge - a discrete object - rather than as a measured length.

   In its own track it is an ordinary bar: one hue, square right end, on a
   hairline track so the row still shows the full extent it is measured against.
   The name is on the card in every row, at full contrast, always. */
.pd-rk-bar {
  display: block; height: 8px; border-radius: 4px;
  background: var(--inset); border: 1px solid var(--border-soft);
  overflow: hidden; min-width: 0;
}
.pd-rk-fill {
  display: block; height: 100%; min-width: 3px;
  border-radius: 3px 2px 2px 3px;
  background: var(--gold-500);
}
/* `transition: width .45s` LIVED HERE and is gone. Width is layout: it cannot
   be composited, and it re-ran on every repaint of the panel - which is the
   thing the .pd-gx gate above exists to stop. The arrival is the transform
   grow on .pd-rk-bar.pd-gx; there is no second animation of the same bar. */
.pd-rk.pd-click:hover .pd-rk-fill { background: var(--gold-600); }

/* A COLUMN OF PERCENTAGES WITH NO LABEL. The rows carried 9% / 40% / 23%
   between the name and the money and nothing on screen said what they were -
   the families panel explained it in its lede, the types panel never mentioned
   it. The header shares the row's own track list, which is the house rule: a
   header that is not given every track change stops lining up with its rows. */
.pd-rk-h {
  padding: .1rem .65rem .35rem; cursor: default;
  border-bottom: 1px solid var(--border-soft); border-radius: 0;
  font-size: var(--fs-2xs); font-weight: var(--fw-bold); text-transform: uppercase;
  letter-spacing: .05em; color: var(--text-dim);
}
.pd-rk-h .pd-rk-nm, .pd-rk-h .pd-rk-v, .pd-rk-h .pd-rk-p, .pd-rk-h .pd-rk-n {
  font-weight: var(--fw-bold); font-size: inherit; color: inherit;
}

.pd-rk-n {
  font-size: var(--fs-2xs); font-weight: var(--fw-black); color: var(--text-dim);
  text-align: right; font-variant-numeric: tabular-nums;
}
.pd-rk-nm {
  font-weight: var(--fw-medium); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pd-rk-p {
  font-size: var(--fs-xs); color: var(--text-dim); text-align: right;
  font-variant-numeric: tabular-nums;
  padding-left: .55rem; border-left: 1px solid var(--border-soft);
}
.pd-rk-h .pd-rk-p { border-left-color: transparent; }
/* The bar takes the slack and the figure sits on the row's right edge, where
   the eye already expects a total. */
.pd-rk-v {
  display: flex; align-items: center; gap: .6rem; min-width: 0;
  font-weight: var(--fw-bold); text-align: right; font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.pd-rk-v > .pd-rk-bar { flex: 1 1 auto; }
.pd-rk-v > b { flex: 0 0 auto; min-width: 84px; }
/* The header is text, so it right-aligns like the figures under it. */
.pd-rk-h .pd-rk-v { display: block; }

@media (prefers-reduced-motion: reduce) {
  .pd-rk, .pd-rk-fill { transition: none; }
}

/* Two panels side by side must each be allowed to shrink, or the second
   overflows its column - which is what pushed the revenue figures off-screen. */
.row > [class*="col-"] > .card-panel { min-width: 0; }
.card-panel > div { min-width: 0; }

/* The % column goes first on a narrow screen; the bar and its figure stay
   together, because that pairing is the chart. */
@media (max-width: 620px) {
  .pd-rk { grid-template-columns: 18px minmax(0, 1fr) 150px; }
  .pd-rk-p { display: none; }
  .pd-rk-v > b { min-width: 74px; }
}

/* ===== THE CATEGORY TABLE IS A CHART TOO =====
   It carried the same flat slab in one column. The bar is the row's own
   background fill now, so the table reads as ranked magnitude rather than as a
   table with a decoration in it. The narrow bar stays a real mark because it
   measures a DIFFERENT thing (share of the catalogue, not revenue) and two
   measures must never share one encoding. */
/* The row keeps its hover and its rounding; it no longer draws a background
   fill, because the bar in the "products in it" column already encodes exactly
   the same value and a measure must be drawn once. */
.pd-catrow { border-radius: 8px; }

/* Notes on the left, table on the right. Measured at 1600px before this: the
   lede was 467px against a 1514px row, so a thousand pixels of white sat beside
   a paragraph while the table stretched ONE column to 849px to fill the same
   space. Below 1200px it stacks, because a 300px column of prose beside a
   six-column table is worse than either. */
.pd-cat-wrap { display: grid; gap: 1rem; grid-template-columns: 1fr; }
@media (min-width: 1200px) {
  .pd-cat-wrap { grid-template-columns: 290px minmax(0, 1fr); align-items: start; }
  .pd-cat-note { position: sticky; top: .6rem; }
}
.pd-cat-note .pd-lede { margin-bottom: .6rem; }
.pd-cat-sum {
  margin: 0; padding: .55rem .7rem; border-radius: 9px;
  background: var(--inset); border: 1px solid var(--border-soft);
  font-size: var(--fs-sm); line-height: 1.5; color: var(--text-soft);
}
.pd-cat-sum b { color: var(--text); font-weight: var(--fw-bold); }

/* THE NINE THAT SOLD NOTHING, as a group rather than as nine table rows whose
   four money columns are empty by construction. They printed "0 (0%)" beside a
   never-sold figure identical to their own product count beside the word
   "none" - three cells restating one fact - and the fact is the GROUP, not any
   one of them. Each still names itself, still carries its count and still
   filters the list, so the tail is counted rather than dropped. */
.pd-dead { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .7rem; }
.pd-deadchip {
  display: inline-flex; align-items: baseline; gap: .4rem;
  padding: .22rem .5rem; border-radius: 7px; cursor: pointer;
  background: var(--inset); border: 1px solid var(--border);
  color: var(--text-soft); font: inherit; font-size: var(--fs-xs); line-height: 1.35;
  transition: border-color .14s ease, background .14s ease;
}
.pd-deadchip:hover { background: var(--accent-weak); border-color: var(--accent-line); }
.pd-deadchip > code {
  font-size: var(--fs-sm); color: var(--text); background: none; border: 0; padding: 0;
}
.pd-deadchip > b { font-weight: var(--fw-bold); font-variant-numeric: tabular-nums; }

/* The eight that did sell, and what "sold" counts. */
.pd-cat-h {
  margin: 0 0 .6rem; font-size: var(--fs-sm); line-height: 1.5; color: var(--text-dim);
}
.pd-cat-h b { color: var(--text); font-weight: var(--fw-medium); }
.pd-cat-live { min-width: 0; }

/* THE BAR TAKES THE SLACK AND THE FIGURE SITS IN A FIXED TRACK BESIDE IT, so
   the two are adjacent at every table width AND the figures still line up as a
   column. As a flex row with `margin-left: auto` on the number, the bar for
   HOUSE (2 products) was a 4px dot at x=497 with its own figure at x=958. */
/* Figure first, meter under it. The meter is 5px of gold on a hairline track,
   so the row's ink is the numbers - which is what a reader compares. */
/* ONE LINE, NOT A DIAGONAL. The comment above this one described the opposite
   of what shipped: it said "figure first, meter under it", and the DOM emits the
   meter first - so the meter drew at the top-LEFT of the cell and its figure at
   the bottom-RIGHT, the two ends of a diagonal, in a row 20px taller than it
   needed to be. The header word "Sold" sat over the figure while the bar it
   labels began 250px to the left of it.

   Bar then figure, on one baseline: the bar takes the slack, the figure sits on
   the cell's right edge where the column of numbers lines up, and the header
   sits over both. That is the same shape .pd-rk-v uses one panel over, so the
   two charts on this page are now read the same way. */
.pd-cat-p {
  display: flex; align-items: center; gap: .55rem; min-width: 0;
}
.pd-cat-p > b {
  flex: 0 0 auto; min-width: 74px;
  font-size: var(--fs-sm); font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums; text-align: right;
}
/* A METER, because sell-through is a share of a whole and the whole is on the
   row beside it. It keeps a track for that reason - unlike a magnitude bar,
   where a filled rail is ink the data did not ask for. Thin, and it sits under
   the figure rather than beside it so the number leads. */
.pd-catbar {
  height: 5px; border-radius: 3px; background: var(--inset);
  border: 1px solid var(--border-soft);
  overflow: hidden; display: block; width: 100%;
  /* CAPPED, AND RIGHT-ALIGNED UNDER ITS OWN FIGURE. Full-width in a 190px
     track it was a long pale rail carrying almost nothing - six of the eight
     rows fill under 15% of it - and its left end sat 180px from the right-
     aligned number it belongs to. 96px is enough to separate 1% from 42% and
     small enough that the empty part is not the loudest thing in the row.
     Scoped to this table: the drill dialog reuses .pd-catbar as a full-width
     share-of-revenue bar in its own column, where a 96px cap would be wrong. */
}
/* Beside its figure, not under it, and 7px rather than 5: at 5px on a 96px rail
   CAL's 2% was two pixels of gold and read as a rendering artefact rather than
   as a small number. Capped, because given the whole flexible track it was a
   long pale rail carrying almost nothing - six of the eight rows fill under 15%
   of it. The cap is scoped to this table: the drill dialog reuses .pd-catbar as
   a full-width share-of-revenue bar in a column of its own. */
.pd-tbl-cat .pd-catbar {
  flex: 1 1 auto; width: auto; max-width: 96px; margin: 0; height: 7px;
}
/* FIGURE ON THE RIGHT EDGE, METER BESIDE IT ON THE LEFT. Emitted figure-first
   and laid out left to right, "4,527 (14%)" started at the cell's left edge and
   ran into the Products figure one column over ("32,984 4,527 (14%)" read as a
   single number), while its right-aligned header sat over the empty meter.
   row-reverse puts the figure under its header and in a column with the other
   figures, and the meter keeps its place directly beside it. */
.pd-tbl-cat .pd-cat-p { flex-direction: row-reverse; justify-content: flex-start; }
.pd-catbar > span {
  display: block; height: 100%; border-radius: 3px; min-width: 3px;
  background: var(--gold-500);
  transition: filter .14s ease;
}
.pd-catrow:hover .pd-catbar > span { filter: brightness(1.12); }

/* ===== FOUR CLASSES THIS PAGE EMITTED WITH NO RULE AT ALL =====
   Found by widening check 4 of check-frontend.py, whose PREFIXES list had
   never included `pd-` — so the whole Products page sat outside both directions
   of the class contract while the checker printed FRONTEND-OK. An unknown class
   is not an error, it is just a class: each of these rendered as unstyled body
   text, which is a fair part of why the page read as unfinished. */

/* The sentence above each ranked chart, saying what the chart is measuring and
   over what window. It is prose ABOUT a figure, so it takes the dim label ink
   rather than competing with the figures underneath it. */
.pd-lede {
  margin: 0 0 .75rem; padding: 0 0 .7rem;
  border-bottom: 1px solid var(--border-soft);
  font-size: var(--fs-md); line-height: 1.5; color: var(--text-dim); max-width: 66ch;
}
.pd-lede code {
  font-size: .95em; padding: .05rem .3rem; border-radius: 4px;
  background: var(--inset); border: 1px solid var(--border-soft);
}
.pd-lede b { color: var(--text); font-weight: var(--fw-medium); }
/* TWO PANELS SIDE BY SIDE ARE READ ACROSS, so their first ranked rows have to
   start at the same height. The families lede runs to two lines and the types
   lede to three - Register Books spanning several families is worth the extra
   clause - so the taller of the two sets the floor and the shorter reserves it,
   the same device .ad-chip-k uses to keep a one-line label aligned with a
   two-line one. 3 lines at .82rem / 1.5 plus the rule and its padding. */
/* IN em, BECAUSE THE FONT MOVED AND THE ARITHMETIC DID NOT. This was
   `3 * .82rem * 1.5` from before the type sweep put the lede on --fs-md, so the
   reserve was a line short of three real lines and the two tables' headers sat
   2px apart (815 against 817 at 1600px). 4.5em is three line boxes of the
   lede's OWN font at its own 1.5 line-height, whatever that font becomes.
   AND NO RULE UNDER IT. `.pd-lede`'s border stops at its 66ch max-width, so
   over a full-width chart it drew a hairline two thirds of the way across and
   ended in mid-air; the chart's own header already carries a full-width rule
   one line lower. */
.pd-rank-lede { min-height: 4.5em; box-sizing: content-box; border-bottom: 0; padding-bottom: 0; }
@media (max-width: 1199.98px) { .pd-rank-lede { min-height: 0; } }

/* The category key. A Sage category is a CODE, so it is set in the mono face at
   a readable size rather than left at the browser's default <code> shrink. */
.pd-cat-k { min-width: 0; display: flex; align-items: center; }
.pd-cat-k code {
  font-size: var(--fs-sm); color: var(--text); background: none; border: 0; padding: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The "how this is measured" note under the hero. It answers the question the
   user asked out loud — how is the discontinued number arrived at — so it is
   a standing part of the panel, not a tooltip, and it is set apart from the
   figures above it rather than running on from them. */
/* THE REVENUE BAR IS GONE, AND THE ROW ORDER IS WHY. These rows are ranked by
   PRODUCTS - that is what the panel is about and what its one bar encodes - so
   a second bar drawn against revenue ran non-monotonically down the column:
   $4.37m, $2.64m, $3.19m, $0.61m. A bar whose lengths do not follow the order
   of its rows is not read as a ranking, it is read as a highlight, and the
   400px cream block behind the top figure was the loudest thing in the table.
   Left-anchored in a flexible track it also stranded CAL's $19,180 370px from
   the sliver that encoded it; right-anchored, the sliver vanished under the
   figure. Eight rows is few enough to read as eight numbers. */

/* ===== THE DEFINITIONS ARE A REFERENCE, SO THEY ARE CLOSED ================
   This was ninety words of small print across the full width, restating the
   four definitions the band's own tooltips carry. It is a <details> built from
   the same `_defs` object the band reads, so it cannot drift from them. */
/* IT WAS --inset ON THE CANVAS, WHICH IS A RATIO OF 1.03. Measured in the
   browser: #ECEDF1 composited on #E9EAEE. --inset is the ground INSIDE a card
   and this strip is not inside one - it sits on the page next to three white
   fact cards - so it read as a smear of the canvas rather than as a control.
   On --surface it measures 1.20, the same step every other card on this page
   stands off the page by. */
.pd-basis {
  margin-top: 1rem; padding: 0;
  background: var(--surface); border: 1px solid var(--border-soft);
  box-shadow: var(--shadow);
  border-radius: 12px; font-size: var(--fs-sm); color: var(--text-soft);
}
.pd-basis > summary {
  cursor: pointer; padding: .55rem .9rem; list-style: none;
  display: flex; align-items: center; gap: .5rem;
  font-weight: var(--fw-medium); color: var(--text);
}
.pd-basis > summary::-webkit-details-marker { display: none; }
.pd-basis > summary::after {
  /* THIS RENDERED AS A BOX AND A CAPITAL A next to "How every figure on this
     page is measured" - the bytes here were C2 83 41: U+0083, an undefined C1
     control character, followed by a literal 'A'. Some encoding step ate the
     chevron that was meant to be here and nothing looked at the result.
     It is a CSS escape now, which is pure ASCII in the file and therefore
     cannot be mangled again by anything that rewrites this text. */
  content: '\203A'; margin-left: auto; font-size: 1.1rem; line-height: 1;
  color: var(--text-dim); transition: transform .15s ease;
}
.pd-basis[open] > summary::after { transform: rotate(90deg); }
.pd-basis > summary > i { color: var(--accent-ink); }
.pd-basis > summary:hover { color: var(--accent-ink); }
/* ===== THE THREE BASES ====================================================
   The dialog behind "Compare the three bases". It is an ARGUMENT, so it is set
   as a small table followed by prose rather than as another dashboard: the
   reader has come here to find out why the headline is 10.3 and not 14.1, and
   that is a paragraph with three numbers in it, not a chart.               */
.pd-bases { padding: 1rem 1.1rem 1.2rem; }
.pd-tbl-base .pd-tr {
  grid-template-columns: minmax(220px, 1fr) 100px 108px 84px;
}
.pd-base-k { display: flex; flex-direction: column; gap: .1rem; min-width: 0;
             font-weight: var(--fw-medium); }
.pd-base-k em {
  font-style: normal; font-size: var(--fs-xs); font-weight: var(--fw-medium); color: var(--accent-ink);
}
/* The rung the band actually leads with, marked so the reader can see at once
   which of the three they have been reading. A tint and a gold edge, never
   colour alone: the sub-label says it in words. */
.pd-base-r.is-here {
  /* The text sat hard against its own 3px gold edge - "The whole Sage product
     master" touched the bar. Its padding lives in the ID-scoped rule below,
     because `#page-products .pd-tr` (1-1-0) sets every row's side padding and
     a class-only rule here loses to it - which is exactly how the first
     attempt at this changed nothing. */
  background: var(--accent-weak); border-radius: 8px;
  box-shadow: inset 3px 0 0 var(--accent-fill);
}
#page-products .pd-base-r.is-here { padding-left: .8rem; padding-right: .5rem; }
.pd-base-why { margin-top: 1.1rem; max-width: 78ch; }
.pd-base-why h6 {
  margin: 0 0 .5rem; font-size: var(--fs-lg); font-weight: var(--fw-bold); color: var(--text);
}
.pd-base-why p {
  margin: 0 0 .7rem; font-size: var(--fs-md); line-height: 1.6; color: var(--text-soft);
}
.pd-base-why p b { color: var(--text); font-weight: var(--fw-bold);
                   font-variant-numeric: tabular-nums; }
.pd-base-why .pd-dead { margin: 0 0 1rem; }

.pd-basis-l {
  margin: 0; padding: 0 .9rem .8rem;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: .5rem .9rem;
}
.pd-basis-l > dt {
  font-size: var(--fs-2xs); font-weight: var(--fw-bold); letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-dim); margin-bottom: .1rem;
}
.pd-basis-l > dd { margin: 0 0 .3rem; line-height: 1.5; }

/* ===== RHYTHM =============================================================
   Scoped to this page. base.css owns .card-panel and every other page uses it;
   a feature stylesheet restating a shell rule is a fault this project has paid
   for three times, most recently a hover that silently did nothing.

   AND THE NOTE THAT USED TO SIT HERE SAID "base.css owns .panel", WHICH IS THE
   BELIEF THAT KEPT THIS PAGE GREY FOR A WEEK. There is no `.panel` rule in this
   app - not in base.css, not in layout.css, and Bootstrap 5 dropped the class
   in 2015. The house card is `.card-panel`, and every other page in the repo
   uses it; products.html was the only file carrying the bare word. So all four
   panels here were TRANSPARENT: no background, no border, no radius, no shadow,
   no gold top-rule - four invisible boxes on the --canvas grey, which is
   exactly what "all the gray" was describing. The padding and radius below were
   being applied to a box nobody could see.

   Same family as every other fault in this file: an unknown class is not an
   error, it is just a class. Nothing threw, and check-frontend's class contract
   is scoped to classes JS EMITS, so a dead class sitting in a partial was never
   swept in either direction. It is now (check 4b). */

/* .panel-head brings its own padding and ends in a rule, so the padding belongs
   to the body - which is the structure active-accounts and agent already use. */
#page-products .panel-body { padding: 1rem 1.2rem 1.15rem; }

/* The step number is chrome, not a figure: it orients the reader down the page
   and must never compete with the numbers inside the panel. */
#page-products .panel-num {
  background: var(--gold-100); color: var(--gold-700);
  border: 1px solid var(--gold-200);
}

/* Tables were .8rem with .42rem rows - correct for a dense grid, tiring for a
   page somebody reads rather than scans. */
#page-products .pd-tbl { font-size: var(--fs-md); }
#page-products .pd-tr { padding: .52rem .12rem; }
#page-products .pd-th { font-size: var(--fs-2xs); letter-spacing: .06em; padding-bottom: .5rem; }

/* The list is the page's longest table - 50 rows, 2,785px of a 4,958px page -
   so a row has to be trackable from its name to its revenue across 1,514px.
   IT ALREADY HAD A ZEBRA AND THE ZEBRA WAS INVISIBLE. --surface-2 #FAFAFA on
   --surface #FFFFFF measures 1.044, and 1.064 in dark - the step CLAUDE.md
   calls one nobody can see, and the step that made every panel in this app
   float before --inset existed. Chrome reported the tint present and painted on
   every even row; on screen there were no stripes.
   --surface-3 is 1.118 light and 1.167 dark: quiet, and actually there. Active
   Accounts made exactly this change on exactly this reasoning and wrote the
   note beside it; this is the same fix on the longer table.
   Hover has to beat the tint or a tinted row appears not to respond. */
#page-products .pd-tbl-list .pd-tr:not(.pd-th):nth-child(even) { background: var(--surface-3); }
#page-products .pd-tbl-list .pd-tr.pd-click:hover,
#page-products .pd-tbl-list .pd-tr.pd-click:nth-child(even):hover { background: var(--accent-weak); }
/* The dialog's two long tables get it for the same reason: 25 rows of six
   columns is exactly where a row stops being trackable across its own width. */
#page-products .pd-tbl-ord .pd-tr:not(.pd-th):nth-child(even),
#page-products .pd-tbl-cust .pd-tr:not(.pd-th):nth-child(even) { background: var(--surface-3); }

/* WHAT EVERY ROW IN THIS SET HAS IN COMMON, said once. The Status column read
   "Active" on all fifty rows of the default page and on four of the five pages
   sampled across the book - fifty identical lozenges to scan past to find the
   one row that differs. It is a sentence now, derived from the FILTERS rather
   than from the rows on screen, so it cannot flicker as somebody pages. */
.pd-setline {
  display: flex; align-items: baseline; gap: .5rem;
  margin: 0 0 .55rem; padding: .5rem .7rem; border-radius: 9px;
  background: var(--inset); border: 1px solid var(--border-soft);
  font-size: var(--fs-sm); line-height: 1.5; color: var(--text-soft);
}
.pd-setline > i { color: var(--text-dim); font-size: var(--fs-sm); flex: 0 0 auto; }
.pd-setline b { color: var(--text); font-weight: var(--fw-bold); }

/* ...AND THE EXCEPTIONS, MARKED. Two independent facts, never a four-way chain:
   the old column let "Discontinued" silently also mean "and never sold". Each
   is a word as well as a colour, because colour never carries a fact alone.
   MEASURED, light on --surface #FFFFFF and dark on #171613:
     --warn-ink #7A5308 on --warn-weak #FBF1DC   6.10:1   (dark 7.82:1)
     --text-soft #55555C on --inset  #ECEDF1     6.32:1   (dark 10.25:1)
   The never-sold mark is deliberately the quiet one: 78,907 products are in
   that state and it is a fact about the extract as much as about the product. */
.pd-p-h { display: flex; align-items: baseline; gap: .35rem; min-width: 0; }
.pd-mk {
  flex: 0 0 auto; padding: .02rem .34rem; border-radius: 5px;
  font-size: var(--fs-2xs); font-weight: var(--fw-bold); line-height: 1.5; white-space: nowrap;
  border: 1px solid transparent; cursor: help;
}
.pd-mk.is-disc  { background: var(--warn-weak); color: var(--warn-ink); border-color: var(--warn-line); }
.pd-mk.is-never { background: var(--inset); color: var(--text-soft); border-color: var(--border); }

/* ===== THE STRIP IN DARK MODE =============================================
   The BAND's own dark-mode lift moved to base.css with the other four - it was
   the same two declarations on five pages. What is left here is this page's
   segmented strip, whose track is a step lighter (.34 against the review
   queue's .40) because its segments are 54px tall rather than 18px and a
   darker trough would read as a hole rather than as a groove.

   Declared twice, once per mechanism, so the toggle beats the OS in both
   directions. That is the house rule and it is not optional: a value declared
   only inside a media query is not overridable by the theme switch. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .pd-band-bar { background: rgba(0,0,0,.34); }
}
:root[data-theme="dark"] .pd-band-bar { background: rgba(0,0,0,.34); }

/* ===== DRILL-DOWN: every figure opens the rows it was counted from =========
   Asked for directly. One dialog serves all of them, so the rows a figure
   opens are rendered by exactly the same code as the page's own list. */
.pd-drill-h { align-items: flex-start; gap: 1rem; }
.pd-drill-h .modal-title { font-size: var(--fs-xl); font-weight: var(--fw-bold); line-height: 1.25; }
/* WHAT SELECTED THESE ROWS. A list shown without saying how it was chosen
   invites the reader to believe it is a different set from the one they
   clicked - which is the whole failure mode of a drill-down. */
.pd-drill-what {
  margin-top: .25rem; font-size: var(--fs-sm); line-height: 1.5;
  color: var(--text-dim); max-width: 78ch;
}
.pd-drill-foot {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  padding: .8rem 1rem; border-top: 1px solid var(--border-soft);
  background: var(--inset); font-size: var(--fs-sm);
}
/* The count is over EVERY matching row, never over the page - so it takes the
   space, and the pager buttons are pushed to the far edge. */
.pd-drill-n { color: var(--text-dim); margin-right: auto; }
.pd-drill-n b { color: var(--text); font-weight: var(--fw-bold); }
.pd-drill-hint { font-size: var(--fs-xs); color: var(--text-dim); }
.pd-drill-back { order: -1; }

/* THE LIST IN THE DIALOG RAN EDGE TO EDGE. The modal body is p-0 so the foot
   and the set line can span it, and the table inherited that: product names
   started 3px inside the dialog's left edge and the revenue figures ended 3px
   inside its right, while the header above sits in 1rem. The dialog's rows
   line up with its own heading now. `.pd-bases` already pads itself. */
#pdDrillBody > .pd-tbl { padding: 0 1rem; }
#pdDrillBody > .pd-setline { margin: .75rem 1rem .35rem; }

/* A clickable row that did not answer the pointer. The category table and the
   families table open things and neither had a hover; the product list did. */
.pd-tbl-cat .pd-tr.pd-click:hover,
.pd-tbl-fam .pd-tr.pd-click:hover { background: var(--accent-weak); }

/* Anything that opens something says so before it is clicked. */
.pd-click { cursor: pointer; }
.pd-bk.pd-click:hover .pd-bk-v { text-decoration: underline; text-underline-offset: .18em; }
.pd-fact.pd-click:active { transform: translateY(0); }

/* The families / types / categories table, which is what a figure counting
   GROUPS opens - a list of products would be the wrong object entirely. */
.pd-tbl-fam .pd-tr {
  grid-template-columns: minmax(180px,1.6fr) minmax(120px,1fr) 96px 132px 116px 124px;
}
.pd-fam-k { font-weight: var(--fw-medium); min-width: 0; }

/* ===== SIX BANDS OF CHROME BEFORE THE FIRST ROW =====
   Measured at 1600px: panel head, search, filter card, "Showing 1-50 of
   88,008", the set line, then the table header - roughly 250px of furniture
   above a list, and "of 88,008" restating the panel head's own "88,008
   matching" one band higher.

   SCOPED TO THIS PAGE, never to the component. PageHead is shared with TMC
   Entities and app/css/page-head.css owns the shell of it; a feature stylesheet
   restating a shell rule is fault 3 in CLAUDE.md, and it has already cost this
   project a silently dead hover. Everything here is a modifier under
   #page-products, so Entities is untouched. */

/* The search shares the filter row instead of taking one of its own. Both are
   already flex children of .ph-bar - the filter card simply had no basis, so it
   wrapped. Below 1500px it goes back to its own line, where a 6-control row
   cannot fit beside it anyway.
   1500, NOT 1200. The search (320px) and the six-control filter card (~1,000px
   at its 130px floors) need ~1,330px of panel beside each other, and at 1366 the
   panel holds ~1,206 - so the card wrapped "Sort by" onto a second line of its
   own, stretched full width, and the search box sank to the bottom of an empty
   left column (align-items: flex-end). Below 1500 they stack, and the six
   controls fit one row under a search that keeps PageHead's own 620px cap. */
@media (min-width: 1500px) {
    /* THE CONTAINER IS #pdHead, NOT .ph-bar. The first version of this put the
       flex basis on `.ph-bar .ph-filters` and nothing happened, because the
       band CLOSES before the filter card: they are siblings under #pdHead, not
       children of one flex row. Checked against the rendered markup rather than
       assumed - which is what the first attempt did not do. */
    #page-products #pdHead {
        display: flex; flex-wrap: wrap; align-items: flex-end; gap: .75rem;
    }
    #page-products #pdHead > .ph-bar { flex: 0 0 320px; margin-bottom: 0; }
    #page-products #pdHead > .ph-filters { flex: 1 1 560px; margin-bottom: 0; }
    /* The count and the pager keep their own full-width line, because the set
       line sits directly under them and the two describe the same set. */
    #page-products #pdHead > .ph-count { flex: 1 1 100%; }
}

/* The count line and the set line are one row: both describe the SET, and
   stacking them put two sentences about the same thing on two lines. */
#page-products #pdHead .ph-count { margin-bottom: .35rem; }
#page-products .pd-setline { margin-top: 0; }

/* ===== SMALL SCREENS =======================================================
   Measured at 1280x720, 1024x768, 768x1024 and 390x844 in both themes, page
   and every dialog. Nothing ran past the viewport - but at 1024 the full list
   scrolled sideways inside its own card by 24px, and below 768 the tables
   either scrolled with nothing to say they did or cut their own headers
   ("SHARE OF THE RANG", "% / INVOICED" on two lines). A table on a phone that
   has to be dragged sideways to reach its total is a table nobody reads, so
   below 576px each one REFLOWS to the columns that answer its question and the
   row - which opens the product - carries the rest.                         */

/* 1024: the six full-list tracks needed 885px in an 864px card. The two text
   tracks give up their surplus rather than the table scrolling for 24px. */
/* And below 1200 a product NAME wraps rather than being cut - it is the thing
   the row is about. The code line under it keeps its ellipsis. */
@media (max-width: 1199.98px) {
  .pd-tbl-list .pd-p-n, .pd-tbl-list .pd-p-m, .pd-tbl-list .pd-f > span { white-space: normal; overflow-wrap: anywhere; }
}
@media (min-width: 1001px) and (max-width: 1199.98px) {
  .pd-tbl-list.is-full .pd-tr {
    grid-template-columns: minmax(190px,1.7fr) minmax(120px,1.1fr) 76px 88px 132px 104px;
  }
}

/* The band's figures: five across on a laptop, but at tablet width auto-fit
   made four plus one - the fifth alone under the headline. The lead figure
   takes a row of its own and the other four share the next. */
@media (max-width: 991.98px) {
  .pd-band-kpis { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .pd-band-kpis > .pd-bk.is-lead { grid-column: 1 / -1; }
}

/* The families table in the drill dialog: the bar column and Discontinued give
   way first, and a family name wraps rather than being cut - it is the key. */
.pd-tbl .pd-tr > .pd-fam-k { white-space: normal; overflow-wrap: anywhere; }
@media (max-width: 900px) {
  .pd-tbl-fam .pd-tr { grid-template-columns: minmax(140px,1.6fr) 80px 110px 110px; }
  .pd-tbl-fam .pd-tr > span:nth-child(2),
  .pd-tbl-fam .pd-tr > span:nth-child(5) { display: none; }
}

@media (max-width: 575.98px) {
  #page-products .pd-tbl, #pdDetailModal .pd-tbl, #pdDrillModal .pd-tbl { font-size: var(--fs-sm); }
  #page-products .pd-tr { gap: .5rem; }

  /* A panel head is a number, a title and a source note, and at 390 the note
     wrapped into a two-line column beside a two-line title. It takes its own
     line under the title instead. */
  #page-products .panel-head { flex-wrap: wrap; row-gap: .2rem; }
  #page-products .panel-head > .pd-src { flex: 1 1 100%; margin-left: 0; padding-left: 2.1rem; }
  .pd-card-h { flex-wrap: wrap; row-gap: .1rem; }
  .pd-card-h > .pd-src { flex: 1 1 100%; margin-left: 0; }

  /* The window chip sat right-aligned on a line of its own. */
  .pd-band-top { flex-wrap: wrap; row-gap: .45rem; }
  .pd-band-win { margin-left: 0; }
  .pd-band-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .pd-band-kpis > .pd-bk:last-child:nth-child(even) { grid-column: 1 / -1; }

  /* The ranked rows: at 390 a 150px bar-and-figure left the NAME 111px, so
     "Personalized …" was all that remained of the thing being ranked. The bar
     goes and the name keeps the width; the figure is still the ranking. */
  .pd-rk { grid-template-columns: 18px minmax(0, 1fr) auto; }
  .pd-rk-v > .pd-rk-bar { display: none; }
  .pd-rk-nm { white-space: normal; overflow-wrap: anywhere; }

  /* The category table: Category, Products, Invoiced (figure only) and
     Revenue - the base of every percentage stays on the row. */
  .pd-tbl-cat .pd-tr { grid-template-columns: minmax(64px,.6fr) 66px minmax(70px,1fr) 76px; }
  .pd-tbl-cat .pd-catbar { display: none; }
  .pd-tbl-cat .pd-th .pd-th-s { display: none; }

  /* The product list, full set: Product, Unit price, Revenue. */
  .pd-tbl-list.is-full .pd-tr { grid-template-columns: minmax(0,1fr) 112px 76px; }
  .pd-tbl-list.is-full .pd-tr > span:nth-child(4) { display: none; }
  /* The narrow set (never invoiced): Product and Category. */
  .pd-tbl-list:not(.is-full) .pd-tr { grid-template-columns: minmax(0,1fr) 64px; }
  .pd-tbl-list:not(.is-full) .pd-tr > span:nth-child(2) { display: none; }

  /* The families drill: Family, Invoiced, Revenue. */
  .pd-tbl-fam .pd-tr { grid-template-columns: minmax(0,1fr) 88px 84px; }
  .pd-tbl-fam .pd-tr > span:nth-child(3) { display: none; }

  /* The three bases: the label wraps rather than being cut. */
  .pd-tbl-base .pd-tr { grid-template-columns: minmax(0,1fr) 62px 58px 52px; }
  .pd-tbl .pd-tr > .pd-base-k { white-space: normal; }

  /* The product dialog's two tables: who, and how much. */
  .pd-tbl-cust .pd-tr { grid-template-columns: minmax(0,1fr) 92px; }
  .pd-tbl-cust .pd-tr > span:nth-child(2),
  .pd-tbl-cust .pd-tr > span:nth-child(3),
  .pd-tbl-cust .pd-tr > span:nth-child(4) { display: none; }
  .pd-tbl-cust .pd-cu { white-space: normal; }
  .pd-tbl-ord.is-onekind .pd-tr { grid-template-columns: minmax(0,1fr) 96px 76px; }
  .pd-tbl-ord.is-onekind .pd-tr > span:nth-child(3),
  .pd-tbl-ord.is-onekind .pd-tr > span:nth-child(5) { display: none; }
  .pd-tbl-ord:not(.is-onekind) .pd-tr { grid-template-columns: minmax(0,1fr) 96px 76px; }
  .pd-tbl-ord:not(.is-onekind) .pd-tr > span:nth-child(2),
  .pd-tbl-ord:not(.is-onekind) .pd-tr > span:nth-child(4),
  .pd-tbl-ord:not(.is-onekind) .pd-tr > span:nth-child(6) { display: none; }

  /* The dialogs' own padding, a step in, so a 374px dialog is not half gutter. */
  .pd-d-h, .pd-d-k, .pd-d-grid { padding-left: .85rem; padding-right: .85rem; }
  .pd-d-h { padding-right: 3rem; }
  #pdDrillBody > .pd-tbl { padding: 0 .6rem; }
  #pdDrillBody > .pd-setline { margin-left: .6rem; margin-right: .6rem; }
  .pd-bases { padding: .8rem .75rem 1rem; }
}

/* The sold segment is a tenth of the bar, and at 390 a tenth is 31px - so its
   own label, "9,101 invoiced", was cut to "9,10". It keeps the width its label
   needs; the never segment is the one with room to give. */
.pd-seg.is-sold { min-width: 5.2rem; flex-shrink: 0; }
.pd-seg.is-never { flex: 1 1 auto; }


/* ==================== THE HOME PAGE'S VOICE ====================
   Every panel below the band takes the card language Home set: a bold --fs-lg
   title in sentence case with its soft note UNDER it, generous padding, no
   header rule, thick rounded bars that grow in, and rows that settle on
   --inset with an accent edge on hover. Scoped here, because base.css owns
   .panel-head for every other page. */
@keyframes pd-rise   { from { opacity: 0; transform: translateY(10px); } }
@keyframes pd-grow-x { from { transform: scaleX(0); } }
#page-products .pd-band { animation: pd-rise .55s var(--ease-out) backwards; }
#page-products .card-panel { animation: pd-rise .55s var(--ease-out) backwards; animation-delay: 160ms; }
#page-products .panel-head { padding: 1.1rem 1.2rem .5rem; border-bottom: 0; align-items: flex-start; gap: .7rem; }
#page-products .panel-num {
  width: 26px; height: 26px; flex: 0 0 26px; border-radius: 50%; margin-top: .05rem;
  background: var(--accent-weak); color: var(--accent-ink); border: 0;
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
}
#page-products .panel-title {
  font-size: var(--fs-lg); font-weight: var(--fw-bold); text-transform: none;
  letter-spacing: -.005em; color: var(--text);
}
.pd-hd { display: flex; flex-direction: column; min-width: 0; }
.pd-hd > .pd-src { margin-left: 0; margin-top: .15rem; font-size: var(--fs-xs); font-weight: var(--fw-normal); color: var(--text-soft); }
#page-products .panel-body { padding: .6rem 1.2rem 1.2rem; }

/* the lede: a quiet paragraph, not a ruled-off block */
.pd-lede { font-size: var(--fs-sm); color: var(--text-soft); border-bottom: 0; padding-bottom: 0; }
.pd-lede code { border-radius: var(--r-pill); padding: .02rem .45rem; border: 0; }

/* the ranked panels */
.pd-rank { gap: .15rem; }
.pd-rk { padding: .55rem .65rem; border-radius: var(--r-sm); transition: background .2s var(--ease), box-shadow .2s var(--ease); }
.pd-rk.pd-click:hover { background: var(--inset); box-shadow: inset 3px 0 0 var(--accent); }
.pd-rk-h { color: var(--text-soft); letter-spacing: .08em; border-bottom-color: var(--border-soft); }
.pd-rk-n {
  width: 22px; height: 22px; border-radius: 50%; display: inline-grid; place-items: center;
  background: var(--inset); color: var(--text-soft); font-weight: var(--fw-bold); text-align: center;
}
.pd-rk-h .pd-rk-n { background: none; }
.pd-rk-nm { font-weight: var(--fw-bold); }
.pd-rk-bar { height: 10px; border-radius: 5px; background: var(--surface-3); border: 0; }
.pd-rk-fill {
  border-radius: 5px; background: linear-gradient(90deg, var(--gold-300), var(--gold-500));
  transform-origin: left;
}
/* ---- THE BARS DRAW THEMSELVES IN, ONCE ----
   products.js hands .pd-gx out through _gx(), which answers once. Everything
   here repaints on every entry to the page, so bound to the element instead
   the whole top of the screen re-grew on every return visit.
   The 250ms delay lets the ranked rows' own card entrance land first.
   BACKWARDS FILL: the resting state is the full bar. */
.pd-rk-bar.pd-gx .pd-rk-fill {
  animation: pd-grow-x .32s var(--ease-out) backwards; animation-delay: 250ms;
}
.pd-catbar.pd-gx > span {
  transform-origin: left;
  animation: pd-grow-x .32s var(--ease-out) backwards; animation-delay: 250ms;
}
/* THE BAND'S BAR GROWS AS ONE BAR, not as two segments. The segments carry
   their own figures in 22px type inside an overflow:hidden box, so scaling
   them individually squashes the numbers horizontally for the length of the
   animation - the label is the point of the segment. The track grows from the
   left and the two labels fade in behind it, which needs no counter-scale and
   never shows a distorted figure. */
.pd-band-bar.pd-gx {
  transform-origin: left;
  animation: pd-grow-x .32s var(--ease-out) backwards;
}
@keyframes pd-fade { from { opacity: 0; } }
.pd-band-bar.pd-gx .pd-seg > * {
  animation: pd-fade .18s var(--ease-out) backwards; animation-delay: .26s;
}
.pd-rk.pd-click:hover .pd-rk-fill { background: linear-gradient(90deg, var(--gold-400), var(--gold-600)); }
.pd-rk-p { border-left: 0; font-weight: var(--fw-medium); color: var(--text-soft); }

/* the category table and its note */
.pd-cat-sum { border-radius: var(--r); background: var(--inset); border: 0; padding: .75rem .9rem; }
.pd-deadchip { border-radius: var(--r-pill); padding: .24rem .65rem; border-color: var(--border-soft); background: var(--surface); }
.pd-catbar { height: 7px; border-radius: 4px; background: var(--surface-3); border: 0; }
.pd-catbar > span { border-radius: 4px; background: linear-gradient(90deg, var(--gold-300), var(--gold-500)); }
#page-products .pd-th { color: var(--text-soft); letter-spacing: .08em; border-bottom-color: var(--border-soft); }
.pd-tbl .pd-tr { transition: background .2s var(--ease), box-shadow .2s var(--ease); }
.pd-tbl-cat .pd-tr.pd-click:hover,
.pd-tbl-fam .pd-tr.pd-click:hover { background: var(--inset); box-shadow: inset 3px 0 0 var(--accent); }

/* the list */
#page-products .pd-tbl-list .pd-tr:not(.pd-th):nth-child(even) { background: var(--surface-2); }
#page-products .pd-tbl-list .pd-tr.pd-click:hover,
#page-products .pd-tbl-list .pd-tr.pd-click:nth-child(even):hover { background: var(--inset); box-shadow: inset 3px 0 0 var(--accent); }
#page-products .pd-tbl-list .pd-tr { border-radius: var(--r-sm); border-bottom-color: transparent; }
.pd-p-n { font-weight: var(--fw-bold); }
.pd-setline { border-radius: var(--r); border: 0; }
.pd-pill { padding: .1rem .55rem; }

/* the reference drawer */
.pd-basis { border-radius: var(--r); border-color: var(--border); margin-bottom: 1.1rem; }
.pd-basis > summary { padding: .75rem 1.1rem; }
.pd-basis > summary > i {
  width: 28px; height: 28px; border-radius: 8px; display: inline-grid; place-items: center;
  background: var(--accent-weak);
}
.pd-basis-l { padding: 0 1.1rem 1rem; }

/* the dialogs: Home's card frame, KPI tiles and cards */
#pdDetailModal .modal-content, #pdDrillModal .modal-content {
  border-radius: var(--r-lg); border: 1px solid var(--border); box-shadow: var(--shadow-lg); overflow: hidden;
}
.pd-drill-h { padding: 1rem 1.25rem; border-bottom: 1px solid var(--border-soft); }
.pd-drill-h .modal-title { font-size: var(--fs-lg); letter-spacing: -.005em; }
.pd-drill-what { color: var(--text-soft); font-size: var(--fs-xs); }
.pd-drill-foot { background: var(--surface-2); padding: .85rem 1.25rem; }
.pd-drill-foot .btn { border-radius: var(--r-pill); }
/* The right padding keeps the heading clear of the absolutely-placed close
   button, which the shorthand would otherwise take back. */
.pd-d-h { padding: 1.15rem 3.8rem .95rem 1.35rem; }
@media (max-width: 575.98px) { .pd-d-h { padding: .95rem 3rem .8rem .85rem; } }
.pd-d-t h3 { font-size: var(--fs-2xl); letter-spacing: -.02em; }
.pd-d-m { color: var(--text-soft); }
.pd-d-k { gap: .8rem; padding: 1rem 1.35rem; background: var(--inset); }
.pd-k {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r);
  padding: .8rem .95rem; box-shadow: var(--shadow);
  transition: transform .25s var(--ease), box-shadow .25s var(--ease);
}
.pd-k:hover { transform: translateY(-2px); box-shadow: var(--lift-2); }
.pd-k-l { font-size: var(--fs-xs); font-weight: var(--fw-medium); text-transform: none; letter-spacing: 0; color: var(--text-soft); }
.pd-k-v { font-size: var(--fs-2xl); letter-spacing: -.02em; margin-top: .15rem; }
.pd-k-s { color: var(--text-soft); }
.pd-d-grid { padding: 1rem 1.35rem 1.25rem; gap: 1.1rem; }
.pd-card { border-radius: var(--r); border-color: var(--border); padding: 1rem 1.15rem; }
.pd-card-h { font-size: var(--fs-lg); }
.pd-mocell { border-radius: var(--r-sm); border: 0; }
.pd-mo-b { border-radius: 5px 5px 0 0; background: linear-gradient(180deg, var(--gold-300), var(--gold-500)); }
.pd-pr-track { height: 8px; border-radius: 4px; border: 0; background: var(--surface-3); }
.pd-catctx { border-radius: var(--r-sm); border: 0; }

@media (prefers-reduced-motion: reduce) {
  #page-products .pd-band, #page-products .card-panel, .pd-rk-fill { animation: none; }
  /* The gated forms, each at its own specificity: `.pd-rk-fill` alone is
     (0,1,0) and loses to `.pd-rk-bar.pd-gx .pd-rk-fill` (0,3,0), so listing
     only the bare class left the grow running for a reader who asked for no
     motion - silently, the way every fault in this family presents. */
  .pd-rk-bar.pd-gx .pd-rk-fill,
  .pd-catbar.pd-gx > span,
  .pd-band-bar.pd-gx,
  .pd-band-bar.pd-gx .pd-seg > * { animation: none; }
  .pd-k:hover { transform: none; }
}

/* The band's figures, divided by hairlines the way Home's footer stats are, so
   five numbers read as a row of separate facts rather than one long line. */
.pd-band-kpis > .pd-bk:not(.is-lead) { padding-left: 1.1rem; border-left: 1px solid var(--shell-line); }
@media (max-width: 991.98px) {
  .pd-band-kpis > .pd-bk:not(.is-lead) { padding-left: 0; border-left: 0; }
}

/* ==================== THE BAND'S WAIT ====================
   A mirrored skeleton inside the dark band while product_catalogue() answers:
   the same shimmer the other bands use (m1BandShimmer lives in base.css).
   base.css lists the other pages' skeleton classes; this page's is declared
   here because products.css is the one file this page owns. */
.pd-band-sk {
    display: block; border-radius: 6px;
    background: linear-gradient(90deg, rgba(255,255,255,.06) 25%, rgba(255,255,255,.14) 50%, rgba(255,255,255,.06) 75%);
    background-size: 200% 100%; animation: m1BandShimmer 1.2s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
    .pd-band-sk { animation: none; }
}
