# Compound architecture — the contract for organisms (decided Jun 2026)

How organisms / compound component sets / panels get built so they're shared and
reused WITHOUT duplicating HTML / CSS / JS, and are customizable (animation
presets, verbosity, text fragments) + showcased. Read before building or
refactoring any compound. Companion to `decisions.md` ("Compound components")
and the CSS architecture below. Compressed; see `CLAUDE.md` for format rules.

## Terminology & hierarchy (4 tiers)

- **Foundations** — tokens, type, color, space.
- **Components** — single-responsibility atoms/molecules (button, chip, field,
  switch, checkbox, breadcrumb). CSS-first; a controller is OPTIONAL.
- **Compounds** — orchestrate components + own a controller (`_x.js`) + a config
  schema (`_x.config.js`) + (often) animation. THIS is the "organism / panel /
  set". **Defining trait: it has a controller AND a config schema, not just
  styled markup.** (filter-pills, pagination, status timeline, list-toolbar,
  header, the cards, filename-title-with-loader.)
- **Patterns / Layouts** — page-level composition of compounds (templates).
- Use the word **"Compound"** (not "organism") in `@dsCard group=` + prose —
  more legible, already in use.

## The compound contract — 4 parts, one of each, never forked

| Part | Artifact | Rule |
|---|---|---|
| **Style** | `_x.css` | self-contained `@layer tokens/theme/components` (the one-file-per-component pattern) |
| **Behavior** | `_x.js` | idempotent `initX(root, opts)`; expose API on the element (`el.__x = {…}`) + CustomEvents for host integration |
| **Structure** | render fn OR slice | ONE owner — see SoT rule |
| **Config** | `_x.config.js` | declarative schema — the ONE genuinely new artifact |

- Style + behavior are ALREADY shared verbatim by both the DS preview and the
  prototype (`initFilterPills`/`initStatus`/`playTimeline`, the `el.__fp` API,
  the `filterpill:remove`/`clearall` events). Keep it that way — generalize, not
  reinvent.

## Structure source of truth (Option B — decided + user-confirmed Jun 2026)

- **Data-shaped compounds** (repeated / variant markup: timeline, filter-pills,
  pagination, table) → the **render function in `_x.js` is the SoT**:
  `window.X.render(data, opts)` returns the canonical tree. Every consumer (DS
  preview slice + prototype) CALLS it; nobody re-types the tree. The controller
  already does DOM-shaping (`_filter-pills.js` `makePill`, `_status.js`
  `ensureWrap`) so this fits the grain.
  - **Motivating case:** the status timeline existed in THREE drifted copies —
    hand-typed in `comp-status.html`, re-emitted by `buildTimeline()`/`dot()`/
    `callout()` in `data-preview.html`, AND a legacy `.timeline__*` impl in
    `ui-kit.css` that shares no class names with the canonical `.step/.dot`. That
    third copy is the cost of an un-owned structure, made visible.
- **Static compounds** (fixed markup: header, empty state, CTA panel) → the
  `comp-*.html` slice stays SoT; copying once is cheap. Don't force a renderer
  where there's nothing to repeat.
- The config schema FEEDS the renderer: verbosity / detail levels + text
  fragments are `render(data, opts)` ARGUMENTS, not forked markup.
- React wrappers (where a prototype is React) render the SAME tree (or call the
  same builder) then `initX(ref.current, opts)` in a `useEffect`. React =
  structure adapter, `_x.js` = behavior, `_x.css` = style. One of each.

## Config / motion manifest (`_x.config.js`) — the one new artifact

- A declarative descriptor read by the runtime, the prototype, AND the playground
  Inspector — a knob defined once works everywhere.
- Shape: `{ name, tier, motion:{ tokens:[…], presets:{…}, default },
  knobs:[{key,type,label,default,options}], strings:{…} }`. Registered on a
  global registry (e.g. `window.FP_COMPOUNDS['status']`).
- **Motion = CSS custom-property tokens** (`--st-dur`, `--st-stagger`,
  `--st-ease`), NEVER hardcoded JS consts. Reasons: (1) the motion governor
  already scales them via `playbackRate`; (2) presets become named token bundles
  (same mechanism as `FP_BTN_PRESETS`); (3) the animation Inspector just rewrites
  those tokens live — zero per-compound wiring. `_status.js`'s `STAG=380`/
  `DUR=520`/`EASE` consts are the anti-pattern to retire.
- **Verbosity / detail** is config, not forked code — generalize
  `comp-pagination.html`'s `data-responsive` `{mode,siblings,eyebrow,
  compactLabels}` profile idea into a `detail` knob.
- **Text fragments / i18n** live in `strings:{}` (the hardcoded Czech
  "Zrušit vše", "Navržené filtry", … move here).

## Config cascade (3 layers, last wins — mirrors @layer)

1. **System defaults** — the schema + `:root` token values.
2. **Instance config** — `data-*` attrs / `initX(el, opts)` per placement.
3. **Live overrides** — Inspector / tweaks broadcast rewriting tokens (the
   existing `FP_TWEAKS.applyToFrame`).

## Showcase / tooling — two separate surfaces (by intent)

- **Governor = playback** (speed / pause / scrub) — GLOBAL, wall-clock.
  Unchanged.
- **Inspector = authoring** (preset / timing / easing / verbosity / text) —
  PER-COMPOUND, intrinsic config. Contextual panel, reuses `tweaks-panel.jsx`
  shell, AUTO-GENERATED from the focused compound's schema (`knobs`→controls,
  `motion.presets`→preset-pills, `motion.tokens`→sliders, `strings`→text fields).
  Keep it SEPARATE from the global broadcast `FP_TWEAKS` panel (which is correctly
  app-wide STYLE only — chip treatment, button look, table density).
- A compound with motion grows a "Motion" Inspector section: preset picker +
  duration/stagger/easing sliders + prominent Replay (calls the compound's
  `playX()`); the governor still scales whatever the Inspector dialed in.

## Reference slice (first compound onto the contract)

- **Status timeline** is migrated first: tokenize `_status.js` consts → `--st-*`;
  add `Status.render(steps, opts)` consumed by `comp-status.html` +
  `data-preview.html` (retire the duplicate `buildTimeline` + the legacy
  `ui-kit.css .timeline__*`); write `_status.config.js` + the schema-driven
  Inspector. Copy the resulting pattern to filter-pills / pagination / table.

---

# Collection-motion — the paged-collection animation contract (decided Jun 2026)

How paged collections (table rows, card grids, cover walls) animate across their
THREE lifecycle phases — **reveal** (initial load), **refresh** (filter/sort
change), **paginate** (page change) — WITHOUT each content type re-implementing
motion. Builds ON the Compound-architecture contract above. Decided + user-
confirmed Jun 2026. Read before touching table/card/cover motion or pagination
animation.

## Three actors, clean seams (the orchestrator is its OWN compound)

- **Content compound** (`table`, card-grid, cover-grid) — owns item markup, a
  `render(data, opts)` SoT, and INTRA-item motion (e.g. chip row-hover). Knows
  nothing about phases.
- **Pagination compound** (`_pagination.js`) — owns the CONTROL + its OWN
  thumb-slide / compact-sweep self-animation. EMITS `pagination:change
  {from,to,dir,hint}` (bubbling CustomEvent) on every commit; KEEPS its own
  motion (the strip animating itself ≠ the content transition). It already
  derives all four in `changeTo`.
- **Collection-motion** (`_collection.js` + `_collection.config.js`,
  `FP_COMPOUNDS.collection`) — the ORCHESTRATOR. Binds a content container +
  (optionally) a pager; runs the three phase animations on the container's
  ITEMS. Content-agnostic: told "an item is selector X" + "items live in
  container Y". It also EMITS a bubbling `collection:entrance
  {phase, items, delays, dur}` CustomEvent after starting any entrance
  (reveal/refresh-in/paginate; not under reduced motion) so INTRA-item
  compounds can sync their own motion to the same per-item delays — e.g. the
  quick-tile count-up restarts with its tile's stagger slot. Same seam rule as
  the pager: the orchestrator emits, content compounds listen.

- **Why a THIRD compound, NOT folded into pagination:** reveal + refresh are NOT
  page events (a single-page filtered list animates a refresh but never touches a
  pager) — folding motion into pagination would couple non-paginated lists to a
  control they don't have. Pagination is a CONTROL (acts on the strip); the
  motion acts on CONTENT (different DOM + lifecycle). Cards + covers reuse the
  SAME orchestrator → DRY. The seam is the contract's own CustomEvent rule:
  pager EMITS, collection LISTENS.

## Two motion vocabularies (entrance ≠ page-transition)

- **Per-item entrance (reveal + refresh) = a COMPOSABLE effect VECTOR, not an
  enum.** Independent, individually-toggleable sub-effects so you can keep "just
  the slide" or "just the fade":
  - `move`: none·up·down·left·right (+ distance `--col-dist`)
  - `fade`: on/off
  - `zoom`: start scale (1 = off; e.g. 0.92)
  - `stagger`: none·by-row·by-column·by-index·from-center (+ `--col-stagger` + a cap)
  - *slide-in* = move:up,fade:off · *fade-in* = move:none,fade:on · *zoom-in* =
    zoom:.92,fade:on · *by-row slide+fade* = move:up,fade:on,stagger:by-row.
- **Refresh = quick out→in RESTAGGER** (DECIDED: NO position-tracking / FLIP —
  out of scope for our use case). Old items fade/contract out, the new set
  restaggers in using the SAME entrance vector.
- **Page transition (paginate) = a small ENUM of named TECHNIQUES** (two-set,
  direction-derived from `hint`/`dir`; they DON'T decompose into per-item
  toggles): `restagger` (re-run entrance on the new page — DEFAULT, reuses the
  vector) · `slide-in` · `slide-through` (whole-page; needs double-buffering —
  DEFERRED to step 4) · `flip` · `crossfade` · `zoom`.
- **Orthogonality (cardinal):** technique/variant = keyframe SHAPE; preset/token
  = TIMING. Independent axes. The existing `gentle/snappy/deliberate/instant`
  presets retune timing of WHATEVER technique is selected — same mechanism as the
  other compounds, ZERO new wiring for the governor / Inspector.

## Config manifest shape (extends `_x.config.js`, doesn't break it)

```
motion: {
  presets: { gentle, snappy, deliberate, instant },   // cross-phase TIMING bundles (unchanged mechanism)
  phases: {
    reveal:   { effect:{move,fade,zoom,stagger}, tokens:['--col-rv-*'], controls:[…] },
    refresh:  { effect:{…},                      tokens:['--col-rf-*'], controls:[…] },   // restagger only
    paginate: { technique, tokens:['--col-pg-*'], controls:[…] },
  }
}
```
- `--col-*` tokens live ON THE CONTAINER (same rule as `--st-*` on `.timeline`,
  `--pag-*` on the nav), read at play time, governor-scaled, Inspector-retuned.
- Inspector grows a PHASE switcher (Reveal / Refresh / Paginate); each surfaces
  its effect/technique controls + timing sliders + a Replay calling
  `Collection.<phase>()`. The global FP_TWEAKS broadcast panel stays STYLE-only.

## Hazards (all already logged — see pitfalls.md / CLAUDE.md)

- Restart-safe (run-token + cancel in-flight) — the `_status.js playTimeline`
  pattern; trivial to re-trigger at slow playground speed.
- WAA `element.animate()` / CSS transitions ONLY (governor-scaled via
  playbackRate); NEVER a setTimeout/rAF timeline (not scaled).
- Reduced motion + print/PDF: end-state = base (content VISIBLE), animate FROM
  hidden, gate on `?rm` + matchMedia.
- Frozen-clock verification: offscreen WAA currentTime is frozen → animated
  content reads blank; provide a `.finish()`/`?rm` end-state path.
- Stagger is GEOMETRY-derived (read item rects → bucket rows/cols), so ONE
  orchestrator drives 1-D tables AND 2-D card/cover grids. Cap to page-size;
  clamp the total stagger window (no 400-item anim storm).
- `slide-through` needs DOUBLE-BUFFERED content (both page sets in the DOM,
  pointer-events:none mid-transit) — IMPLEMENTED (Jun 2026): a cloned outgoing
  "ghost" overlaid on the wrap (`.col-transit`) slides out as the real incoming
  layer slides in; the orchestrator owns the swap (clone → renderFn → animate),
  so renderFn must swap rows IN PLACE, never replace `wrap.innerHTML`. flip is a
  two-half rotateX on the layer with a midpoint swap (`.col-flip` perspective).
  See pitfalls.md "Collection-motion compound".

## Build order (status-first reference-slice approach)

1. Promote table to a real compound: `_table.js` (`Table.render`/`init`) +
   `_table.config.js` — fills the missing controller, NO motion.
2. `_collection.js` + `_collection.config.js` with REVEAL (effect vector); wire on
   the table card + schema-driven Inspector.
3. Pagination `pagination:change` emit; `paginate:restagger` (reuses reveal).
4. Refresh (restagger) + richer paginate techniques (slide-through, flip,
   crossfade) one at a time.
5. Generalize to card + cover grids (near-free if orchestrator stayed agnostic).
6. Phased Inspector polish.

(First build = steps 1–3 + refresh-restagger + paginate-restagger. Then (Jun
2026) the full paginate technique enum — slide-in / crossfade / zoom /
slide-through / flip — landed; the dataset-preview prototype was wired
end-to-end; geometry stagger (by-row/by-column/from-center via rect-banding)
generalized the orchestrator to 2-D; and the table + library card grid + cover
wall all run on it via the SHARED `_collection-inspector.jsx`
(`window.CollectionInspector`). The compound now covers every paged collection
in the system. Done.)

---

# CSS architecture — layers, token contracts, unified state

The target architecture for the design system. Read before refactoring any
component onto the system. Compressed; see `CLAUDE.md` for format rules.
Companion to `decisions.md` ("CSS architecture") and `pitfalls.md`.

## Why

Today the system is **theme-as-override**: light values are the "real" rule and
`_dark.css` hand-mirrors every `variant × state` cell under
`body[data-theme="dark"] .x`. The matrix tears whenever a cell is forgotten (the
reported secondary/ghost/danger **pressed-in-dark** gap), and conflicts are
settled by hand-tuned specificity (`(0,2,1)` beats `(0,2,0)`; tweaks need
`!important`). `pitfalls.md` is a changelog of that matrix tearing.

The fix has two moves:

1. **State lives in the component, theme lives in token values — they never
   multiply.** A component is written ONCE, theme-agnostic, every colour/shadow
   routed through a token. Dark mode changes ZERO component rules; it only
   re-values tokens. A missing dark state cell becomes structurally impossible.
2. **`@layer` replaces specificity** as the conflict-resolution mechanism, so
   theme/app/tweaks win by layer order, not selector weight or `!important`.

## Layer order (cardinal)

Registered once, first, at the top of `colors_and_type.css`:

```css
@layer reset, tokens, theme, base, components, app, tweaks;
```

Later layers beat earlier ones **regardless of selector specificity**.

| Layer | Holds | Rule |
|---|---|---|
| `reset` | normalize, box-sizing, `.bi` glyph fix, reduced-motion policy | global, element-level |
| `tokens` | raw palette + **semantic token defaults (light)** + component token defaults | `:root` custom props only |
| `theme` | `[data-theme="dark"]` (future: hi-contrast) — **token VALUES only, never component selectors** | re-value custom props |
| `base` | html/body, type defaults, **the global `:focus-visible` ring** | element selectors |
| `components` | every component (`.btn`, `.field`, `.cv`…), theme-agnostic | class selectors, all colour via tokens |
| `app` | prototype/page-specific surfaces | the thin per-app layer |
| `tweaks` | generated tweak CSS — wins without `!important` | target component TOKENS |

- **Unlayered rules beat ALL layered rules.** So introducing the order
  statement alone changes nothing — existing unlayered CSS keeps winning. Migrate
  a component by moving it INTO layers; everything else stays put. This is what
  makes the migration incremental and safe.
- **Corollary — don't `@layer` a component that OVERRIDES a still-unlayered
  shared atom.** If component X restyles selectors owned by an unlayered atom
  (e.g. doc-card's `.dc:hover .vchip` overrides `_chips.css`'s `.vchip`), moving
  X into `@layer components` makes those overrides LOSE to the unlayered atom
  base (unlayered beats layered) — the override silently stops applying. Such a
  component can still get the token/theme/state wins (those don't depend on
  layering) but must keep its RULES unlayered until the atom it overrides is
  itself layered. Cover-card was safe to layer (its `.cv*` rules touch no
  unlayered atom); doc-card and the table were NOT at first (both override
  `_chips.css`) — they were layered only once `_chips.css` itself moved into
  `@layer components`, the exact unblock this corollary predicts.
- **Never put a component selector in `theme`.** If dark needs a different look,
  add/expose a token and re-value it. If you're writing `[data-theme="dark"] .x`,
  you're doing it wrong (the one exception: surfaces that must NOT flip — see
  pitfalls "Some surfaces must NOT flip in dark").

## Token contract (per component)

A component declares component-scoped intermediate props (`--_bg`, `--_fg`,
`--_shadow`, `--_ring-halo`…) consumed by its base rule. Variants and states set
ONLY those intermediates.

- **Literals that differ by theme** (brand fills, tuned shadows, focus halos):
  store as a `--btn-*` token with a light default in `tokens` and a literal
  override in `theme`.
- **Flip-sensitive values** (anything that is just a step on a remapped scale —
  `--gov-color-primary-50`, `--color-bg-surface`, `--gov-color-neutral-200`):
  reference the scale token **DIRECTLY in the component rule**, NOT pre-stored in
  a `:root` `--btn-*` token.

```css
.btn { background: var(--_bg); box-shadow: var(--_shadow); }
.btn--secondary { --_bg: var(--btn-secondary-bg); --_fg: var(--gov-color-primary-500); }
.btn--secondary:active,
.btn--secondary[data-state~="pressed"] { --_bg: var(--gov-color-primary-100); }
```

- **⚠ Custom-property FREEZING — the trap that caused the dark-hover-goes-white
  regression.** A token like `--btn-x: var(--gov-color-primary-50)` declared on
  `:root` is resolved AT THE HTML LEVEL, where the body-scoped
  `body[data-theme="dark"]` scale remap does NOT reach — so it captures the
  LIGHT value and inherits that frozen value down forever, even in dark. The dark
  remap must be visible *where the indirection resolves*. So: never put a
  `var(--gov-*)`/`var(--color-*)` that flips under `body[data-theme="dark"]`
  inside a `:root` `--btn-*` token. Either (1) reference it directly in the
  component rule (resolves on the `.btn` element, which inherits the dark scale
  from `<body>`), or (2) give the token an explicit literal value in BOTH
  `tokens` and `theme`. The focus-ring spacer (`var(--color-bg-surface)`) is
  inlined in the focus rule for exactly this reason.
- **Indirect the value tokens, NOT an animated property.** `box-shadow` etc. stay
  declared as real properties on the state selector; only their VALUE is a
  `var()`. A custom-property change that flows into a transitionable property
  still triggers that property's transition — but you cannot transition the
  custom property itself (no `@property` for shadow). So the focus-ring bloom
  works because the `box-shadow` declaration sits on `.btn:focus-visible` and its
  value swaps; it would NOT work if the whole shadow were an intermediate var you
  transitioned directly.
- Empty shadow layer (for "no depth under the ring") is `0 0 #0000`, never
  `none` — `none` is invalid inside a comma list and kills the whole declaration.

## Unified state model (one approach, everywhere)

There is ONE vocabulary for "what state is this element in", carried by a single
`data-state` attribute (space-separated tokens), each bridged to its live
pseudo-class in the SAME selector group so forced and real states are identical:

| State | Canonical bridge (live + forced) |
|---|---|
| hover | `:hover, [data-state~="hover"]` |
| pressed | `:active, [data-state~="pressed"]` |
| focus | `:focus-visible, [data-state~="focus"]` |
| disabled | `:disabled, [disabled], [data-state~="disabled"]` |
| selected | `[aria-pressed="true"], [aria-selected="true"], [data-state~="selected"]` |

- **`data-state` is the single forced/programmatic channel.** The focus
  playground stamps `data-state="hover|pressed|focus"` (one at a time). Use `~=`
  (not `=`) so a real multi-state element can carry e.g. `data-state="hover focus"`.
- **Disabled prefers native.** Real form controls use `:disabled`/`[disabled]`
  (accessible + free). `[data-state~="disabled"]` is only for non-form elements
  (div/span) that can't be natively disabled.
- **Retire the per-component focus/disabled CLASSES** (`.btn--focus`,
  `.field--focus`, `.is-focus`, `.btn--disabled`, `.field--disabled`…). They were
  the inconsistency — hover/pressed already rode `data-state`, focus/disabled did
  not. As each component migrates, replace its bespoke class with the
  `data-state` token. (Buttons are fully migrated; others still use the old
  classes until their turn — the playground stamps BOTH channels during the
  transition.)
- **Composed/organism components follow the same rule:** state is owned by the
  element it visually belongs to, expressed in this exact vocabulary. A parent
  that must reflect a child's state uses `:has()` or sets `data-state` on the
  relevant sub-element — never a new ad-hoc class.

## One file per component

A migrated component is **self-contained**: its file carries its own
`@layer tokens` (light defaults), `@layer theme` (dark remap), and
`@layer components` (rules). No `_dark.css` entry. Drop the file in and the
component is themed, focus-ringed, and tweakable. This is what makes sharing with
a prototype a one-line import. `_buttons.css` is the reference implementation.

## Shared vs harness

- **core** (shippable): `colors_and_type.css` (layer order + tokens + reset),
  component files (`_buttons.css`, …), `_chips.css`, `_tooltip.*`. A prototype
  imports these and gets the system.
- **harness** (preview-only, never shipped): `tweaks-panel.jsx`,
  `playground.html` + `playground-tweaks.jsx` (was `comp-focus-playground.html`),
  `_help.css`, per-card demo scaffolding.

## Migration status

- ✅ **Buttons** — fully on the model (self-contained `_buttons.css`; fixes the
  dark pressed gap for secondary/ghost/danger; focus = one tokenized rule).
- ✅ **Cover-card** — migrated IN PLACE (self-contained in its own `<style>`:
  `@layer tokens`/`theme`/`components`). Dark = token revalues (`--cv-tint`
  4%→13%, `--cv-frost-*`); paper/footer ride `--gov-color-neutral-0` (flips).
  `_dark.css` cover block deleted. Organism pattern: layer ONLY the component
  rules; keep shared/scaffold selectors (`.card` override, `.demo-bar`, `.annot`)
  UNLAYERED so they don't lose to `_card.css`'s unlayered base.
- ✅ **Doc-card** — migrated AND `@layer components`-WRAPPED (verified Jun 2026,
  both themes). Safe to wrap fully because `_chips.css` is itself
  `@layer components`, so doc-card's `.dc:hover .vchip/.achip/.cchip` overrides beat
  the chip base by SPECIFICITY within the shared layer ((0,4,0) > (0,1,0)) — same
  pattern as the table. Scaffold `.card`/`.demo-bar` stay UNLAYERED (override
  `_card.css`'s unlayered base); the `body[data-theme="dark"] .dc:hover .dc-chip--*`
  rules (box-shadow only) coexist with `_dark.css`'s unlayered dc-chip bg/colour.
  Token wins retained: `--dc-surface` (white→`color-bg-subtle`), `.dc`
  text + `.btn-mini--out` colour ride flipping tokens, main `.dc` hover mirrors
  `data-state`, `_dark.css` doc-card block deleted.
- ✅ **Table** — migrated AND `@layer components`-WRAPPED (Jun 2026). Unblocked by
  moving the global `.bi` reset into `@layer reset`: the table's
  `.tbl thead th .h-icon { display:none }` (`.h-icon` IS a `.bi`) now wins from
  `@layer components` over `@layer reset`. Safe to wrap fully because `_chips.css`
  is itself `@layer components`, so the table's chip overrides
  (`.tbl tbody tr:hover .vchip/.achip`, `.tbl .dc-chip` font-size, the ≤640/≤460
  container-query chip tweaks) beat the LAYERED chip base by SPECIFICITY within the
  shared layer. Scaffold `.card`/`.demo-bar` overrides stay UNLAYERED (they override
  `_card.css`'s unlayered base). The `body[data-theme="dark"] .tbl … .dc-chip--*`
  rules (box-shadow only) coexist with `_dark.css`'s unlayered dc-chip bg/colour
  regardless of layer — the documented dc-chip-dark exception.
  `.tbl-wrap`→`--color-bg-surface`, header→`--color-bg-subtle`, zebra wash +
  outlined-chip fill are co-located `--tbl-*` tokens, main row hover mirrors
  `data-state`. Both `_dark.css` table blocks deleted.
- ✅ **Metabox** — migrated AND fully wrapped in `@layer components` (overrides
  no unlayered atom — it only contains a `.cchip`, doesn't restyle it).
  `--metabox-bg` token (white→`color-bg-subtle`), row hover mirrors `data-state`.
  Removed from the `_dark.css` field/surface blanket.
- ✅ **CTA-panel** — migrated AND fully layered. Also **dropped its stale local
  `.btn` copy** and now links shared `_buttons.css`, so its buttons gained real
  pressed/focus/disabled + dark states (primary went from a stuck `#2362a2` to
  the proper dark `#2c70b8`). `--cta-bg` token; status tints already flipped via
  tokens. Removed from the `_dark.css` blanket.
- ✅ **Fields (`.field` input-box: comp-inputs / comp-search / comp-select /
  comp-textarea)** — FULLY MIGRATED + `@layer components`-WRAPPED (Jun 2026). Shared
  field token contract: light defaults in `colors_and_type.css` `@layer tokens`
  (`--field-bg`, `--field-disabled-bg`, `--field-disabled-bd-style`,
  `--field-pressed-bg`), dark re-value in `_dark.css` `@layer theme`. Each card's
  `.field*` rules now live in `@layer components`; the whole
  `body[data-theme="dark"] .field*` blanket was DELETED from `_dark.css`. The wrap
  was UNBLOCKED by moving the global focus ring into `@layer base` (see below): the
  inner `.field input:focus { box-shadow:none }` suppression now sits in
  `@layer components` and beats the base ring on the native control, while the
  wrapper ring (`.field:focus-within` / `.field--focus`) wins too. Per-card scaffold
  (`.col/.row/label/.err/.counter`), the search `.clear`/`.btn`/`.suggest`, and the
  select `.tb-select`/`.combo-*` stay UNLAYERED (the latter two are prototype-shared
  deferrals — their `_dark.css` blanket entries remain). comp-select's `.field` is a
  `[tabindex]` trigger, so its plain `:focus-visible` ring still comes from
  `@layer base`; its open/forced ring (`[aria-expanded]`/`--focus`) wins from
  `@layer components`.
- ✅ **Global focus ring lifted into `@layer base`** (`colors_and_type.css`): the
  `:where(button,[role=button],input,select,textarea,[tabindex]):focus-visible`
  ring + `a:focus-visible`. It was UNLAYERED, which (a) blocked layering the field
  family and (b) made it beat every layered component ring — so `.btn` showed the
  GENERIC blue ring, not its tokenised one. Now in `@layer base`: layered
  components (`.btn`, the wrapped `.field`) WIN with their own ring, while UNLAYERED
  not-yet-migrated controls (`.seg`/`.pag`/`.filter-pill`/`.clear`/checkbox/radio/
  switch …) still beat base and are UNCHANGED, and plain focusable elements still
  get the default. **Intended visual change:** `.btn--danger` focus now shows a RED
  halo (was blue) and all `.btn` rings regained their resting-depth layer — verified
  (primary 4-layer blue, danger 4-layer red).
- ✅ **Boolean switch (`.sw`, comp-switch.html)** — MIGRATED AND FULLY LAYERED
  (self-contained `@layer tokens`/`theme`/`components` in its own `<style>`, like
  buttons). Safe to fully layer: the only focus ring sits on `.sw__track` (a
  `<span>`, no global competitor) and the hidden `<input>`'s global ring is
  invisible (`opacity:0`) — nothing to suppress, unlike `.field input`. Dark =
  token revalue (`--sw-track-off/-hover/-disabled` shift one neutral step,
  `--sw-thumb` stays a bright puck). `.sw`/`.sw__track` blanket deleted from
  `_dark.css`. The theme-switch `.tsw`/`.tsw3` were NOT migrated (shared with the
  prototype's ui-kit.css, which relies on `_dark.css` for their dark — same
  deferral as `.tb-select`); their `_dark.css` block stays.
- ✅ **Buttons disabled `!important` DROPPED** (`_buttons.css`). The generator
  already writes only `--btn-*` token assignments into `@layer tweaks` (no
  unlayered `.btn--primary{background…!important}`), so `.btn:disabled` (0,2,0 in
  `@layer components`) wins on its own over `.btn--variant` (0,1,0) and the tweak
  layer never sets `background`/`box-shadow` directly. The long-noted cleanup.
- ✅ **Prototype field surfaces `.input` / `.tb-select`** — migrated onto the SHARED
  field tokens (Jun 2026). `.input` (ui-kit.css) and `.tb-select` (ui-kit.css +
  comp-select.html) now ride `--field-bg` for the rest well and `--field-pressed-bg`
  for the toolbar-select pressed tint; their `_dark.css` blanket entries (base bg,
  inner-input colour, `.tb-select` hover) are DELETED. Rules stay UNLAYERED (the
  prototype's `ui-kit.css` is an unlayered app sheet; comp-select's `.tb-select`
  was the deferred half of the field card). Hover/focus already flipped via
  gov-scale `var()`s.
- ✅ **Theme switch `.tsw` / `.tsw3`** — migrated onto a SHARED `--tsw-*` token
  contract (light in `colors_and_type.css` `@layer tokens`, dark in `_dark.css`
  `@layer theme`): `--tsw-bg`/`--tsw-bd` (track), `--tsw-puck-light`/
  `--tsw-puck-dark` (the theme-flipping puck+icon colours), `--tsw-thumb-shadow`.
  Converted EVERY consumer in lockstep: comp-switch.html (`.tsw`+`.tsw3`),
  ui-kit.css (general `.tsw` AND the always-dark `.bar .tsw` thumb override),
  brand-header.html (`.tsw`). The whole `.tsw`/`.tsw3` `_dark.css` blanket is
  DELETED. **Key trap handled:** the always-dark `.bar`/brand-header thumbs used
  `var(--gov-color-neutral-900)` for the icon, which INVERTS to near-white in dark
  — they only stayed legible because the old blanket pinned literal hexes. Routing
  them through `--tsw-puck-dark` (neutral-900→#1a2030) keeps the dark icon dark in
  both themes. Verified: prototype + comp-switch + brand-header, both themes.
- ✅ **Core dark token remap wrapped in `@layer theme`** (`_dark.css`). The big
  `body[data-theme="dark"], html[data-theme="dark"] body { --tokens }` block is now
  inside `@layer theme`; the SELECTOR-property dark rules (header/footer/chips/
  tooltip/pagination/swatches/per-card rescues) stay UNLAYERED (they override
  unlayered per-card styles — layering them would make them lose). Safe because
  token VALUES resolve by inheritance, not layer (verified: `--color-bg-surface`,
  `--field-bg`, `--tsw-*` all compute to their dark values).
- ✅ **`.bi` glyph reset → `@layer reset`** (`colors_and_type.css`, Jun 2026). Was
  UNLAYERED, which blocked `@layer`-wrapping comp-table (its `.h-icon`/`.bi` display
  override lost to the unlayered reset once layered). Now in `@layer reset`, so a
  LAYERED override of `.bi` (the table's, any future component's) wins cleanly while
  still-UNLAYERED `.bi` overrides keep winning over reset. No consumer regressed.
- ✅ **Raised controls/panels migrated onto a shared `--color-bg-raised` token**
  (Jun 2026): `.filter-pill`, `.view-switch`, `.pag button` (comp-pagination +
  comp-region-nav + ui-kit), `.related a`, `.quick-tile`, `.lib-card`,
  `.filter-group`, `.notice` (base). The token is white in `@layer tokens`,
  `--color-bg-subtle` in `@layer theme` (`[data-theme="dark"]`) — a SEPARATE token
  from `--field-bg` (same values, different intent: raised panel/control vs. input
  well). Converted EVERY consumer in lockstep (preview cards + the prototype's
  `ui-kit.css`). This COLLAPSED the whole `body[data-theme="dark"] .x { background:
  color-bg-subtle }` blanket in `_dark.css` plus the `.filter-pill` hover, `.notice`
  tint, `.pag` active/in-flight and `.view-switch` dark re-assertions (all now ride
  flip-on-their-own gov-scale tokens). **checkbox/radio** pressed routed through
  `--field-pressed-bg` (its only per-theme `:active` override deleted).
- ⏳ Pending: the field family, the raised-control family, the table AND doc-card
  are all DONE. `.bi` is layered, `_chips.css` is layered, both organisms that
  depended on it (table + doc-card) are `@layer components`-wrapped. Remaining
  unlayered surfaces are the `_dark.css` per-card rescues for not-yet-migrated
  brand/type/colour cards — migrate as their turn comes.
