/* ════════════════════════════════════════════════════════════════════
   /vesper-cards.css — THE SHARED CARD COMPONENT  (Vesper Silk, 2026-09-09)
   ════════════════════════════════════════════════════════════════════
   Owner ruling 2026-09-09 (OQ-27), on the Silk mocks: "cards in combination
   with tables… atmosphere and depth". This file is the ONE home for the card,
   the KPI tile, the row-as-card and the right-hand stack. It contains no
   page-specific rule and no literal colour, size or shadow — every value is a
   token declared in `vesper-skin.css`'s `html[data-palette="silk"]` blocks.

   ⚠️ EVERY RULE IS SCOPED TO `html[data-palette="silk"]`, WHICH IS WHY THIS
   FILE IS SAFE TO ADD TO A PAGE. Under schematic and evening it declares
   nothing at all, so loading it moves no pixel on the two shipped palettes —
   which is the round's contract, and what the schematic screenshot digests
   prove. It also means a page can adopt it before the owner flips the pin.

   ⚠️ LOAD IT LAST — after `/vesper-theme.css`, `/vesper-shell.css`,
   `/vesper-skin.css` AND `/vesper-rail.css`. The row recipe reaches the
   SHELL's own `.ledger .row` (a shared contract, not a page's), and it wins
   there on specificity; the card and stack recipes are new classes and win
   anywhere. Being last also means a page's own inline <style> is the only
   thing that can out-rank it, which is the right order for a component.

   THE FOUR PIECES  (SILK-TOKENS §7, §8, §9)
     .vcard          the plane: radius 14, --card fill, --card-edge, --sheen,
                     --shadow-2. Sub-parts .vcard-hd / -bd / -foot / -tag.
                     `.vcard.is-door` adds the lift on hover.
     .vcard-kpis     the KPI card row (4 up, 2 up under 1180) of .vcard-kpi:
                     a glyph chip, a mono caps label, a 27px mono figure and a
                     footnote that carries the CAVEAT, never an invented delta.
     .vcard-row      the row-as-card: its own --r-inset plane on --card-2,
                     8px apart rather than divided, lifting on hover.
                     Applied to `.ledger .row` so the shell's list adopts it.
     .vcard-stack    the right column: cards 16px apart, top-aligned.

   ⚠️ A HAIRLINE SEPARATES THINGS ON ONE PLANE; A CARD-EDGE LIFTS A PLANE.
   Never both on one box (SILK-TOKENS §3). That is why `.vcard` draws itself
   with `--card-edge` + a shadow and its INTERNAL divisions with `--line-soft`,
   and why `.vcard-row` (which is not lifted at rest) uses `--line-soft`.
   ════════════════════════════════════════════════════════════════════ */
/* the ledger caption's ruled leader line — a rule wearing a background */

/* the column-header strip carries no rule anywhere (the gap under it is the
   separation), and the `.vcard-tag` chip keeps its edge: it is a CONTROL-
   sized box, on the allowlist beside inputs and focus rings. */


/* ════════════════════════════════════════════════════════════════════
   SF-36 — THE NESTED TILES ARE PLANES, NOT BOXES WITH RULES
   ════════════════════════════════════════════════════════════════════
   Owner: "should we add depth to the tiles within the record tile?" — yes.
   The two shipped nested shapes drew themselves the SAME way: a 1px gap over
   a `--line-soft` ground inside a `--line-soft` border, so the gaps read as
   hairlines. That is the hairline device the ruling retires. Each cell
   becomes its own one-step plane with the contact shadow and no border, and
   the container stops painting a ground at all.
   ⚠️ AND THE 4-CELL COUNTER BOX BECOMES FOUR TILES. `.vcard-quad` was ONE
   card with an inset 2×2 grid, which is why its dividers had to be drawn;
   four tiles 8px apart need no divider to be four things. */


/* ════════════════════════════════════════════════════════════════════
   SF-11 — THE STRIP RETURNS TO THE MEASURED DENSITY
   ════════════════════════════════════════════════════════════════════ */

/* a pane whose child already owns the gutter must not pay it twice — the
   family's own inner wrapper keeps its padding and the pane drops to 0. */

/* SF-31 — the record scrolls INSIDE itself. `overflow:clip` is what BUG-024
   put on the panes so a sticky header cannot re-anchor; the record pane adds
   a scroll axis of its own so a long record never lengthens the page. */


/* RULE 2 — deltas carry sign, on the figure and on nothing beside it */


/* RULE 3 — the family hue is the surface's GLYPH TILE, saturated and flat,
   AND IT CARRIES SF-06's CARE (owner amendment 2026-09-10). A tile is a
   small plane, so it gets what every other plane on this skin gets: a lit
   top edge, a sheen that stops in the top third, and a contact shadow. Flat
   MARK, lit TILE — the gloss the reference wears is still not copied. */

/* the tile at record-card size — one step up from the 24px KPI disc, same
   corner ratio (8 on 28). */

/* the tile's own lit top edge (`.vcard::after`, SF-06) takes the meaning's
   hue instead of the neutral highlight every other card wears — "the tile's
   lit edge takes the same colour" (owner). Raw hue, not the boosted icon
   mix: an edge is a UI element (3:1 floor), not text-weight content. Gated
   the same whitelist as the glyph itself: an unadopted page's tile keeps the
   plain card top it always had. */


/* RULE 4 — ranked bars take the family hue, the leader a step stronger.
   ⚠️ 70%, AND THE MOCK DREW IT AT 34%. Measured against its OWN TRACK, the
   34% dawn field bar read 2.33:1 — a bar visible only to a reader who
   already knows a bar is there. Dawn needs 70 to clear 3:1. The cost is
   written down rather than hidden: at 70 the leader stands 1.77:1 clear of
   the field and reads as emphasis rather than as a separate class.
   Legibility wins. */


/* ════════════════════════════════════════════════════════════════════
   SF-35 — TITLE LEFT, GRAIN RIGHT, ON THE SAME ROW
   ════════════════════════════════════════════════════════════════════
   Owner: "title hero should be above the grains" + "grains maybe to the
   right and do that across the board". `.vs-titlerow` is the one shape: the
   masthead title takes the left, the grain — which is a `.seg`, and
   therefore already wears SF-10's segmented control — takes the right, and
   NOTHING ELSE is allowed on that row (every filter went to the scope card
   under SF-34). The pills that remain drop one step. */

/* SF-34 — a filter pill one step smaller than the grain it used to sit beside */


/* ════════════════════════════════════════════════════════════════════
   SF-27 — THE DOOR'S LOADING STATE IS THE SKELETON'S SHEEN
   ════════════════════════════════════════════════════════════════════
   Owner: "Instead of reading... for data, can you do that wave animation".
   The three Home doors printed the word "reading…" while their counts were
   in flight. `.vld-sk-b` is the loader's own animated placeholder (SF-13);
   `.is-reading` puts that same left-to-right sheen over the figure and the
   caption, so a door that is waiting looks like every other thing on the
   estate that is waiting. Reduced motion keeps the placeholder, drops the
   travel — the loader's own contract, restated for the door. */

@keyframes vs-door-sheen{ from{ background-position:120% 0 } to{ background-position:-160% 0 } }
/* ⚠️ `:nth-child(n)` IS NOT DECORATION. Every one of these five pages draws
   its dividers with `> div:nth-child(2n){border-right:0}` and friends, which
   is (0,2,1) — the same as this selector without the pseudo-class — and the
   page's inline <style> comes AFTER this sheet, so a TIE goes to the divider.
   Measured: Bids' cells kept `border-right`/`border-bottom` inked after the
   rule above was written. The pseudo-class matches every child and breaks
   the tie in the shared component's favour, where the ruling lives. */


/* ── SF-43 · THE COUNTER CELLS SIT ON ONE BASELINE GRID ─────────────────
   Four tiles side by side whose contents start at four different heights
   read as four boxes, not as one counter. The cell is a column that pins its
   label to the top and its figure to a shared line: the label's box is a
   fixed height (the mono 8.5px cap at 1.0 plus its own leading), so every
   figure below it starts on the same rail whether or not the label wrapped.
   A cell with no note grows its figure block instead of leaving a hole. */


/* ── SF-43 · A TILE'S SINGLE LINE IS CENTRED IN THE TILE ─────────────────
   `.vtile` and `.vcard-layer` are one-step planes with 11/13 padding; a tile
   whose whole content is one line was top-aligned inside it, so the ink sat
   above the tile's own centre by half the leading. Centring the CONTENT box
   costs nothing when there are several lines and fixes the one-line case. */


/* ── SF-38 (owner amendment) · ONE `--pane-pad`, ACTUALLY TAKEN ──────────
   The polish round set `--pane-pad` and applied it to the three panes; three
   pages then overrode it from their own sheets, so the reviewer measured
   `0 16 16` on Bids' list, `16 24` on Spending's and Vendors', and
   `16 24 16 16` / `18 16 16` on Coffers'. The token was one; the padding was
   four. These rules restate it at the depth those pages declare theirs, so
   the token is the padding rather than a suggestion.

   ⚠️ BIDS' `padding-top:0` IS NOT A DEVIATION AND IS NOT OVERRIDDEN. Its
   list pane is a scroll container whose sticky header must pin at the very
   top of the scrollport (BUG-024); the 16px is not deleted, it moves to the
   first flow child (`#bx-lhead{margin-top:16px}`). The rendered INSET is
   `--pane-pad` on all four sides, which is what the ruling is about, and the
   gate measures the inset rather than the declaration. */


/* ── SF-31 (owner amendment) · THE LIST IS BOUNDED BY THE VIEWPORT ───────
   The reviewer measured Vendors' list pane 2,782px tall at `overflow:visible`
   beside a 60px scope stub, on a document 3,602px long — three cards that do
   not read as one frame because two of them are not frames, they are the
   page. Bids already behaves: its panes row is viewport-bounded and the list
   scrolls inside itself. This is that shape, expressed once.

     · the scope and the list stick under the command band, as the record
       already does on five of the six pages;
     · each is capped at the viewport minus the band and one gutter;
     · each scrolls internally, so a sticky column header (BUG-024) pins
       against the PANE's scrollport instead of the document's.

   ⚠️ `max-height`, NEVER `height`. A pane shorter than the cap keeps its own
   height — Vendors' 60px scope stub must not become a 768px empty box — and
   Bids, whose row is already bounded at 706px, is unchanged by a 768px cap.
   ⚠️ AND `align-self:start`, or a stretched grid item ignores the cap.
   ⚠️ The cap is `--cmd-band-h`, the band's OWN variable, not a literal: the
   frame lane moved that number twice in this round alone. */

@media (min-width:1280px){
  /* ══ THE STICKY REGION IS ONE BAND, NOT A CARD FLOATING OVER THE ROWS ══
     OWNER, draft preview 2026-09-12: "filter is still acting weird on top of
     content". Measured on Coffers at 1672x930, scrolled: the strip pins at
     `--pane-top` = `--cmd-band-h` + `--pane-pad`, which leaves `--pane-pad`
     (16px) of BARE PAGE between the command bar's bottom edge and the
     strip's top — and `.vsm-c2` scrolls underneath, so a list row rode up
     THROUGH that gap and showed between the two bands. `elementFromPoint`
     4px below the band's top returned a row, not the band. The strip read as
     a card laid on the content rather than as the bottom edge of the band.

     The gap is not deleted — the owner keeps it ("keep the visual gap inside
     the band, not outside it"), so it is GROUNDED instead: an opaque strip
     of the page ground, exactly `--pane-pad` tall, drawn from the strip's own
     box upward to the command bar. The sticky region now reads from the
     bar's bottom edge to the strip's bottom, and rows pass UNDER it.

     ⚠️ A PSEUDO, NOT A `box-shadow` SPREAD. The spread trick would have to
     restate SF-06's card shadow to avoid replacing it — a second writer for
     the card's depth, and the first thing to drift.
     ⚠️ `z-index:-1` IS SAFE HERE AND ONLY HERE. `.vsm-c1` carries
     `z-index:3`, so it is a stacking context: the pseudo goes behind the
     strip's own card plane but travels WITH the whole context, which paints
     above `.vsm-c2` (z-index 0). It cannot fall behind the rows.
     ⚠️ AND IT HIT-TESTS AS THE BAND, DELIBERATELY. `pointer-events:none`
     was the first draft and it is wrong: a ground you can click a row
     THROUGH is not a band, it is a picture of one — `elementFromPoint` 4px
     below the band's top still returned `TD.d-rec`, the row, with the paint
     already correct. A pseudo hit-tests as its originating element, so the
     16px now belongs to the strip for the pointer as well as for the eye,
     which is what "rows pass UNDER the band" has to mean. */
  /* ⚠️ IT IS OPT-IN, AND THAT IS A MEASUREMENT, NOT CAUTION. The rule above
     makes `.vsm-c1` sticky, but FOUR of the five family pages override it
     back to `position:static` in their own sheets — measured at 2499x1200:
     `#sp-scopes`, `#rc-scopes`, `#bx-scopes` and `#pl-scopes` all compute
     `static`, and only Coffers' `#cn-scopes` is `sticky`. An absolutely
     positioned pseudo on a STATIC parent resolves against the nearest
     positioned ANCESTOR, so on those four the ground escaped the strip
     entirely and drew its 18px of `--paper` 17px above `.stage` / `.rc-panes`
     / `.bx-panes` / `.pl-body` — over the bottom edge of the tiles row,
     grounding nothing and covering something. A strip that is not pinned has
     no persistent gap to ground in the first place: it scrolls away with the
     rows.

     So the ground is drawn only where a page says its strip IS the pinned
     band, by setting `--vsm-band-ground:1`. At the default 0 the pseudo has
     ZERO height — it paints nothing and hit-tests nothing — rather than being
     switched off with `display`, so a page opting in changes one number and
     nothing about the box. */

  /* ⚠️ AND A STICKY BAND INSIDE THE PANE TAKES THE PANE'S OWN OFFSET. The
     record band (the row carrying the crumb, SAVE and the ✕) is
     `position:sticky; top:0`, which was right while the PANE was the
     scrollport: zero meant "the card's own top edge". With the document as
     the one scroller that zero is the DOCUMENT's top edge, so on a pane
     taller than the viewport the band pinned BEHIND the command bar
     (measured on Coffers: band box 0-49, `elementFromPoint` over its centre
     returning `DIV.cn-cmd`). Its z-index does not need to rise — the band is
     a CHILD of the pane and the pane is a stacking context at z-index 1, so
     the strip at 3 paints over the whole pane, band included; the OFFSET is
     the fix, not a z-index fight. */
  /* ⚠️ DESCENDANT, NOT CHILD. Measured: `.cn-recband` is wrapped (the pane
     renders crumb + body inside a record container), so a `>` combinator
     matched nothing and the band stayed pinned at the document's top edge —
     `elementFromPoint` over its centre returning `DIV.cn-cmd`, the command
     bar, exactly the defect this rule exists to close. */

  /* ── SF-80 · THE LIST'S OWN CAP IS A RECORD-OPEN THING, NOT A REST THING ──
     This rule used to include `.vsm-c2` unconditionally, so every page
     carrying the class got a SECOND, independently-scrolling scroll
     container AT REST too — measured by the 2026-09-11 scroll sweep
     (docs/sweeps/SCROLL-2026-09-11.md): Vendors 8,543px hidden, Spend
     1,473px, Recompete 1,821px, Pulse 1,418px, with the document ALSO
     scrolling underneath all four (a genuine double scrollbar — the wheel
     over the table went to the nested box, never the document). SF-67 fixed
     the identical shape for Coffers alone, page-locally, three times over
     (`.cn-list`, `.xp-listcol`, `.tr-charts`) — this generalizes that fix
     into the shared rule instead of a fourth, fifth and sixth paste.
     At rest the list keeps `position:sticky` (so its header still has a
     scroll ancestor to anchor to once a record opens) but is otherwise
     UNCONSTRAINED: no `max-height`, no forced `overflow-y`, so it grows to
     its content and the document is left as the only element with anything
     to scroll — SF-30's one-scroller rule. The cap comes back the moment a
     record pane is open (`body.vsm-slideopen`, the shared marker every
     `.vsm-c3`-carrying page already writes on open/close — vesper-shell.js
     `syncStackRecord()`, restated locally by spend/recompete/bids/lbr):
     with the pane sharing the viewport the list genuinely needs to be a
     bounded, independently-scrolling card beside it, which SKELETON §7
     allows ("record open -> the pane may scroll, and the list may") and
     which is the exact shape Spending's own record-open state already
     measured as unchanged (`.vsm-c2` still 1,473px over its own content,
     the pane fitting its own exactly) — reproduced here, not invented. */

  /* ⚠️ BUG-050 RETIRED THE RECORD-OPEN CAP. SF-80 kept `body.vsm-slideopen
     .vsm-c2{max-height; overflow-y:auto}` on the reading that "record open ->
     the list may scroll" (SKELETON §7). The owner's 2026-09-12 ruling is
     stricter and supersedes it: ONE SCROLLER, ALWAYS — with a record open
     too. Measured on Recompete at 2000x1100 before this change: the document
     could not scroll at all (1100 == 1100) while `SECTION.rc-list.vsm-c2`
     held 967px inside a 764px box — the owner's "its list card scrolls
     inside itself". The cap and every per-page restatement of it are gone,
     not commented out; `.bx-list` is untouched because Bids' list owning the
     scroll is its own recorded ruling (`body.bx-app`). */
  /* ⚠️ AND THE THREE PAGES THAT DECLARE THEIR OWN AXIS RESTATE IT HERE —
     ONLY WHILE A RECORD IS OPEN. `body.vx-family #vesper-shell-root
     .canvas{overflow:visible}` is (0,3,1) and beats the rule above at
     (0,2,1); `.pl-body` clips on both axes. At REST that `visible` is
     exactly the SF-80 fix above wants (the document is the one scroller a
     document-flow list owes), so it is no longer restated unconditionally —
     only gated the same way the cap is, for the pages that actually open a
     `.vsm-c3` pane. Vendors' bento dossier and Pulse's record are their own
     overlays and never write `body.vsm-slideopen`, so their lists stay
     `overflow:visible` in every state — measured by the sweep as the
     correct shape for both, not a gap this rule needs to close. */
  /* ⚠️ ONE <body>, ONE SELECTOR — `body.vsm-slideopen body.sp-family` (a
     SPACE between them) describes two ancestor <body> elements, which the
     document never has; it silently never matches ANYTHING, which is
     exactly how this shipped broken the first time (measured: `.matches()`
     false, `check_stack_record` red on Spending — both classes belong on
     the SAME element, so they take one compound selector). */

  /* ── BUG-024, RESTATED FOR A PANE THAT IS NOW THE SCROLLER ────────────
     Every list header on the family is `position:sticky; top:var(--X-cmd-h)`
     — the band's height, measured from the DOCUMENT, because the document
     was the scroller. Bids already carries the other half of this pair
     (`body.bx-app .bx-grid th{top:0}`) for exactly the reason the rule above
     now generalises: when the PANE is the scrollport, a `top` of one band
     height pins the caps a band's height DOWN INSIDE the card, over the
     rows. Pulse measured that overlap at 42.2px and worked around it by
     refusing to make the pane a scroller at all — a workaround this ruling
     retires, so its `overflow:clip` override goes with it.
     ⚠️ AND THE PINNED HEADER MUST BE OPAQUE, ON ITS OWN, NOT VIA `html.X-stuck`.
     That class is written by a DOCUMENT-scroll listener; with the pane as the
     scroller the document may never scroll, so the class may never arrive
     while the header is pinned. The header sits on a card plane in every
     state, so it is painted `--card` in every state.
     ⚠️ SF-80 FIX ROUND (fresh review, finding 4) — `top:0` IS ONLY RIGHT
     WHILE THE PANE IS THE SCROLLPORT. At rest `.vsm-c2` is no longer capped
     (SF-80: it grows to its content, the document is the one scroller), so
     a header pinned at `top:0` pins to the DOCUMENT's own top edge —
     BEHIND the command band, which occupies the first `--cmd-band-h` of
     the viewport. Measured: a 60-row table scrolled past the band read
     with no visible column labels at all, `elementFromPoint` on the
     header's own coordinates returning the band. The pin only belongs at
     the card's own top edge once `body.vsm-slideopen` makes `.vsm-c2` a
     scroller again; at rest it has to read from the document like every
     other sticky thing on the page does. */
  /* ⚠️ `.bx-grid` IS DELIBERATELY ABSENT FROM BOTH GROUPS BELOW. Bids is
     BOARDED (finding 2/3 of the fix round): its `.bx-list` is ALWAYS the
     scrollport, never the document, by a recorded owner-driven decision
     this round does not reverse — so its header stays `top:0` in every
     state, unconditionally, exactly as `body.bx-app .bx-grid th{top:0}`
     (bids.html) already says. Including it here at REST's `--cmd-band-h`
     value regressed BUG-024's own check (measured: Bids' header sitting
     100px from the pane's top instead of 0) because this shared rule's
     higher specificity ((0,3,3) vs bids.html's (0,2,1)) wins regardless of
     source order — Bids never writes `body.vsm-slideopen` either, so the
     record-open variant could never have rescued it back to `top:0`. */

  /* ⚠️ BUG-050 RETIRED THE `top:0` HALF TOO. It existed for exactly one
     state — the list being its own scrollport with a record open — and that
     state no longer exists on this family. The header reads from the
     document at `--cmd-band-h` in every state, which is what the rule above
     already says. Bids keeps `top:0` unconditionally from bids.html's own
     `body.bx-app .bx-grid th{top:0}`, untouched here. */

}
/* the shell's figure strip on Budget and LBR — a row of figures divided by
   `border-right`, with a `border-top` over the whole strip */

/* the DAY BAND's own rule, on the two pages that band by date. Its selector
   is `.rc-grid.banded .rc-dayband td` — (0,3,1), which BEATS the row rule
   above at (0,2,4), so it needs its own line rather than a wider one. The
   band is a caps label announcing a date; the ruling leaves it the label. */


/* the record card's TWO-cell tile grid — the same shape as the four-cell
   counter, one page down. Bids and Coffers both draw it, and the rendered
   sweep found both still boxed in `--line` after the counter was fixed. */

/* ⚠️ AND THE ICON DOES NOT EXIST OUTSIDE SILK. The <svg> is real markup on
   index.html / vendors.html / spend.html, so it is in the DOM under every
   palette. Unstyled it strokes nothing (SVG's initial `stroke` is `none`) —
   but an inline replaced element still TAKES PART IN LAYOUT, and a glyph
   plate on schematic is a different box with a child in it than without one.
   `display:none` is the guarantee that schematic and evening render exactly
   what they rendered before this round, rather than an argument that they
   probably do. The same reason the SF-40 family strip is hidden there. */
html .vcard-glyph > svg{ display:none; }
/* the plate is a flex box so the icon centres in it whatever its size */


/* SPENDING is the `procurement` family, and its record card carries the
   family tile the KPI discs carry — the round gave the GRID the variable and
   nothing on the page a tile to spend it on. */

/* `.sp-rest-id` is `align-items:baseline`, and a 24px plate has no baseline
   of its own — it would hang below the eyebrow beside it. The row centres
   where the tile is, and the tile keeps the KPI disc's own size. */

/* the strip does not exist outside Silk — the family tier is a Silk device */
html .sp-scope-fam{ display:none; }
/* ⚠️ SF-49 — THE TILES ROW CLAMPS WITH THE TABLE, AND IT DID NOT. `.vsm-kpis`
   is a SIBLING of the panes row on the hand-built pages, and the family's
   2,100px stage cap is declared on the panes row alone: measured at 2560 the
   tiles row was 2,249px over a 2,100px table on Bids and Recompete, so the row
   overhung the very thing it describes by 149px. Spending was already right,
   because its row is inside the shell canvas, which carries the cap — which is
   exactly why this is declared HERE rather than on each page: two of the three
   subjects needed it and the third did not, and a per-page copy is a rule that
   is true on the pages somebody remembered. `max-width` only ever caps, so on
   Spending it is the min of two caps and moves nothing. */

/* the vertical rules the three-pane frame drew between its tracks have no
   tracks left to separate. They are the container's own pseudo-elements on
   two pages and the resize divider on the rest — under the stack there is
   one column, so they are not drawn rather than drawn and covered. */


/* ⚠️ THE SCOPE COLUMN IS NOT HIDDEN — IT BECOMES THE CONTROLS TILE.
   Hiding it would take the scopes, the lanes, the windows and the sort with
   it, which is every control the page has. The pane keeps its card and its
   padding and lays its contents out horizontally; the page says which of its
   own children are groups. */

/* the list is the flexible child: it takes the height the tiles row and the
   controls tile leave, and scrolls inside its own card. That is what puts
   rows above the fold on a 720px window. */


/* ── 1 · THE TILES ROW ───────────────────────────────────────────────────
   The counters that lived in the record pane's rest block, plus the two
   ranked lists as single-line family-hue bar chips.
   ⚠️ THE ROW FILLS THE WIDTH, AND THAT IS A GRID DECISION NOT A MAX-WIDTH.
   `repeat(4,minmax(0,1fr)) 1.5fr 1.5fr` sums to 100% at every width, so the
   free space right of Vendors' four-up cannot happen here: there is no free
   space to leave. `minmax(0,…)` rather than `minmax(auto,…)` because a mono
   figure's min-content is wider than a sixth of 1280 and `auto` would let it
   push the row past the frame. */

/* ⚠️ A SURFACE WITH ONE RANKED LIST GETS FIVE CELLS, NOT SIX — and it still
   sums to 100%. Recompete has a single ranked list (its buyers), so the
   six-column default would leave the row stopping short of its own right
   edge, which is exactly the free space the row rule forbids. */

html .vsm-kpis{ display:none; }

/* ⚠️ SHORTER THAN THE MOCK AND SHORTER THAN VENDORS (owner: "too tall… too
   much vertical real estate"). The mock stacked glyph / eyebrow / figure /
   footnote in four lines and measured 114px. The glyph and the eyebrow share
   ONE line here — the glyph is the eyebrow's own mark, not a row of its own —
   which takes the tile to ~92px with the same four facts on it. The gate
   `tile row height <= 110` is measured, not asserted here. */

/* ONE caption line. A paragraph is what made the mock's tile tall. */


/* the ranked list, as chips: one line each — name · track · figure. The bar
   is the family hue (SF-40 rule 4), the leader a step stronger; 70% is the
   measured floor against the track in dawn. */

/* ⚠️ ONE LINE, ELLIPSISED. The head wrapped to two lines at 1280 and 1440 and
   nowhere else, so the tiles row measured 122.9 under the 110 ceiling at the
   two widths the owner uses and 108.9 at 1672 — a row whose height was a
   function of how long somebody's label happened to be. */

/* ⚠️ THE CHIP'S OWN HEIGHT IS THE ROW'S HEIGHT, AND THE ROW HAS A CEILING.
   Four chips at the mock's `padding:2px 0` and a 1.4 line box measured the
   tiles row at 131px — over the 110 the owner ruled. `line-height:1.25` on a
   12px name and 1px of padding takes each chip to 17px, so head + four chips
   + the card's own padding lands at ~105 with the same four facts on it. */


/* ── 2 · THE CONTROLS TILE ───────────────────────────────────────────────
   ONE horizontal card: the scope groups first, then the filters, then the
   sort. The grain toggle is NOT here — SF-35 keeps it on the title row. */


/* ── SF-51 · ONE GRID FOR THE GROUPS ─────────────────────────────────────
   Owner, 2026-09-10 (P1): "the pill stacks are messy". The tile was ONE
   `flex-wrap` row holding every eyebrow and every pill as flat siblings, so
   the wrap points fell wherever the widths happened to land: measured on Bids
   at 1440, CLOSING's five pills ended a line with two of CHANGED's beside
   them and the third alone on the next — a reader looking for "Changed" had
   to find the eyebrow first and then read forwards past pills that were not
   in its group.

   TWO COLUMNS, AND THE GROUP IS `display:contents`. `max-content` sizes the
   label column to the LONGEST eyebrow (so every eyebrow left-aligns down one
   edge, which is what makes the groups scannable) and the pills column takes
   the rest. Each group contributes its eyebrow to column 1 and its own pill
   box to column 2, so a pill can only ever wrap inside that box: a group's
   pills cannot cross into another group's line, which is the whole ruling.
   The group wrapper itself lays out no box (`display:contents`) — one grid,
   not a grid of nested flex rows. */

/* the eyebrow, on the pill row's own optical baseline. The pills are 4px/10px
   boxes on a 1.3 line box (≈23px tall); 5px of padding-top puts a 9px mono
   cap height level with the pill label rather than with the pill's edge. */

/* ⚠️ NOTHING IN A GROUP MAY BE WIDER THAN THE GROUP. The pill box is narrower
   than the tile by the label column, so a control that fitted the flat row can
   run past the page here — Spending's filter bar did, by 56px at a 1000px
   viewport. A flex item's `min-width` is `auto`, so the floor has to be said. */

/* ── the pill itself, ONCE, for every page that adopts the stack ────────
   Bids and Spending each wrote this recipe into their own <style> when SF-46
   landed, byte-for-byte the same declarations against `.bx-scope` and
   `.sp-scope`; a third and fourth copy is how `lint_components` rule 5 says a
   page-local twin of a shared rule gets made. The pill box is the one place
   every family's scope button now lands, so the recipe belongs to the box.
   `> *` at (0,5,2) outranks the page copies — which stay in place for the
   fallback row a cached page without vesper-stack.js still renders. */

/* ⚠️ A BOX THAT IS NOT A PILL OPTS OUT BY NAME. The recipe above is `> *`
   because the pills are the box's whole population on five of the six pages;
   Spending's filter bar and the family tile are the exceptions, and they say
   so rather than the recipe guessing. */

/* the door on the right half of the tile opens leftwards rather than off the
   card — the pill knows which half it is on and says so */

/* the MULTI-SELECT check mark (S-07 round 3). `VesperStack.picker` emits the
   span only under `opts.multi`, so on the three single-select adopters this
   selector matches nothing and the file's rule count for them is unchanged.
   It holds a fixed 12px column whether the mark is there or not — a list
   whose labels step left and right as picks come and go is harder to scan
   than one with a gutter. */

/* ⚠️ `[hidden]` LOSES TO A `display` ON THE SAME ELEMENT, and both of these
   carry one. The popover is a flex column and each option is a flex row, so
   `pop.hidden=true` and `option.hidden=true` — which is how the door closes
   and how the search filters — would have laid out exactly as before. */

/* ⚠️ A PILL WITH A CAPTION KEEPS ITS COUNT ON THE LABEL LINE. A flex column
   would have stacked label, count and caption as three lines — three lines in
   a pill on a row of one-line pills. The two-column grid above puts the label
   and its count on row 1 and spans the caption across row 2. */


/* the footer line: Clear filters and the door back, together, right-aligned
   under the groups. Both used to float in the pill flow — Clear beside the
   last group's pills, the door alone at the far right of a line of its own. */

/* `.vsm-ctl-toggle` carries `margin-left:auto` for the flat row it came from;
   on the footer line `justify-content` already does that work and the auto
   margin would push Clear filters to the far left. */


/* the COLLAPSED state — one line: the active filters as chips, and a door
   back to the rest of them. Collapsed by default under ~800px of viewport
   height, and remembered per surface (the page writes localStorage). */

/* ⚠️ HIDDEN IN EVERY PALETTE BY DEFAULT, NOT ONLY IN SILK. The brief and the
   collapse door are the STACK's own markup, and the scope column emits them
   on every repaint so that one DOM serves both frames — which means that
   under schematic and evening they are ordinary boxes unless something says
   otherwise. Scoped to `[data-palette="silk"]`, the `display:none` said
   nothing there and the chips rendered: measured on Spending, the scope
   column, the canvas, the record pane, the stage, the shell and the document
   all grew by 41.84px under BOTH other palettes — the one thing SF-46 is not
   allowed to do. A bare selector is the whole fix, and the show rule below
   is already stack-scoped. */
.vsm-ctl-brief{ display:none; }
.vsm-ctl-toggle{ display:none; }
/* ⚠️ `display:contents`, NOT `display:block`, AND THE DIFFERENCE IS ONE BOX.
   The full row is a WRAPPER the stack needs so the brief can replace it; the
   other two palettes must see the scope column's own box tree, unchanged. Left
   as a plain block it laid out over exactly the same pixels and moved nothing
   — and it was still one more element with a rect than the branch point had,
   on two pages in two palettes. The stack's own row rule (`.vsm-ctl-row`, at
   (0,3,1)) reinstates the flex where it is wanted. */
.vsm-ctl-full{ display:contents; }
/* ⚠️ A DESCENDANT, NOT A CHILD, SINCE SF-51. The door used to be the full
   row's last child; it now sits on `.vsm-ctl-foot` with Clear filters, and the
   child combinator quietly left it `display:none` — measured at 1440, the
   expanded tile had a Clear filters button and no way back to the brief. */


/* ⚠️ THE ENTRANCE IS A SETTLED OBJECT ARRIVING, NOT A PANEL SLIDING (owner:
   "come up with a snazzy way of making it appear"). ONE motion, 260ms, no
   bounce, no blur: the pane starts 24px right and 2% smaller with its lit
   edge BRIGHT and almost no shadow — a card still in the air — and eases to
   place while the shadow deepens to `--shadow-3` and the edge highlight
   fades back to the plane's own ring. The overshoot-free curve is the
   estate's own ease-out; a spring would read as a notification. */
@keyframes vsm-slide-arrive{
  0%{
    transform:translateX(24px) scale(.98);
    box-shadow:0 0 0 1.5px var(--accent-fill, var(--accent)),0 2px 6px rgba(9,12,22,.10);
  }
  100%{
    transform:none;
    box-shadow:0 0 0 1px var(--card-edge),var(--card-contact),var(--shadow-3, var(--shadow));
  }
}
@keyframes vsm-slide-recede{
  0%{ transform:none; opacity:1; }
  100%{ transform:translateX(24px) scale(.98); opacity:0; }
}

/* the pane's own close control, in the corner of the card rather than in the
   flow the record's first line owns */

