# Changelog

|> **Versioning:** pre-1.0, breaking changes ship in the _minor_. Pin to the
|> minor — `~0.4.0` (equivalently `^0.4.0`) resolves to `>=0.4.0 <0.5.0`; a bare
|> `^0` / `*` wildcard does **not** protect you. See README → Versioning, and
|> the deprecation policy in CONTRIBUTING.md.

## 0.16.1 — 2026-10-09

### Changed

- **npm package presentation:** the published README, maintainer identity, description, homepage and discovery keywords now match the live Bronto UI website, so a developer arriving from npm gets the same product explanation and links as on GitHub.
- **Onboarding and examples:** a working one-file HTML specimen, an npm/Vite first-component walkthrough, a consumer upgrade checklist, source-linked application/report/theme demos, and a curated 20-pattern copyable catalog. The website remains separate from the npm runtime package.
- **Documentation experience:** an active-category sidebar and article-first mobile reading order, copyable Markdown code blocks, and direct source links. Browser regression tests now exercise the built public website alongside the existing demo matrix.

### Fixed

- Theme playground light-theme label contrast and swatches that update when an embedding host changes the root theme.

### Compatibility

- Metadata and documentation maintenance release. CSS/JS runtime assets, tokens, and public exports are unchanged from 0.16.0.

## 0.16.0 — 2026-10-02

### Added

- **`--edge`, the token for lines that carry meaning.** A relationship between
  nodes, a connector or annotation leader, or a bracket mark is a graphical
  object WCAG 1.4.11 holds to 3:1 against its surface. The hairlines
  (`--line`, `--line-strong`) are decorative and exempt, and `--line-strong`
  measures 2.39:1 on a light card and 2.29:1 on a dark one. `--edge` is the
  dim-text ink (`var(--text-dim)`): 5.60:1 light and 6.52:1 dark on a card,
  and it follows the high-contrast step. The contrast gate now enforces it on
  the page and on a card in every theme and colorway.
- **`edge` in `readTokens()`**, for renderers that draw relationships on a
  canvas (graph and network views).

### Changed

- Connectors, annotation leaders (default and `--accent`) and the bracket note
  draw in `--edge` instead of `--line-strong`. The `--muted` connector and
  annotation keep the decorative `--line`.
- The bracket note's label was text in `--line-strong` (2.39:1). It is now
  `--edge`, which clears the 4.5:1 text floor.
- Mermaid `lineColor` and D2's `B1` slot (connections, and the shape borders D2
  draws from the same slot) resolve to `--edge`: `#686863` light, `#a0a0a0`
  dark.

## 0.15.0 — 2026-10-02

### Added

- **`ui-eyebrow--caps` and `ui-eyebrow--mono`**, the label voice of a tool
  surface, taken from Spatial. `--caps` sets capitals on the wide tracking step,
  for a label that names a region (a panel, a section, a gate); `--mono` sets the
  mono face, for a label over data, code or a readout. Both compose with
  `--muted` and `--sm`, and `ui.eyebrow({ caps, mono })` emits them. A tool that
  restated this voice by hand can use the base eyebrow and keep only its spacing.

### Changed

- **`css/blocknote.css` draws the nested-block guide in `--line`.** BlockNote
  draws the rule beside a nested block in its side-menu colour, which the leaf
  maps to `--text-dim` for the drag handle. In 0.13 that made the guide as dark as
  a control: light mode went from BlockNote's `#cfcfcf` to `#686863`. The guide
  now takes the border token and the handle keeps `--text-dim`. The rule mirrors
  BlockNote 0.54's selector under `.bn-root`, and a test pins that shape.

## 0.14.0 — 2026-10-01

### Added

- **`initCommand({ match })` and `initCommand({ headless: true })`.** `match(row,
  query)` replaces the substring test, so a row can match on keywords it does
  not show, or stay visible for every query (a "Search everything" row inside
  the list, where the keyboard reaches it). Headless mode leaves the result set
  to the host: Bronto hides nothing and reads the list live, so the host can
  render new rows on every keystroke without re-running `initCommand`, and
  Bronto keeps ids, roles, the active row, the keyboard and the empty state's
  live region. Cleanup restores rows added after init as well.
- **`@ponchia/ui/css/tool.css`**, the default bundle for a tool, imported
  instead of `@ponchia/ui`. It is core in core's order without the
  `navigation`, `site`, `table` and `app` leaves, about 15 kB less.
- **`css/fonts-inter.css` and `css/fonts-jetbrains-mono.css`**, opt-in leaves
  that ship the faces `--sans` and `--mono` name first. Before, the package
  shipped Doto only and every OS drew its own fallback. Inter 4.1 is one
  variable face per style, and JetBrains Mono 2.304 comes in regular, bold and
  their italics. Both are unmodified upstream files under the SIL OFL 1.1, with
  their licenses in `fonts/`.
- **`ui-body-state` and `ui-alert--band`** for node, panel and card bodies of
  200–400px. A body state replaces the content: it fills the body and centres
  one sentence. Use `ui.bodyState()` when empty or loading (`aria-busy` says
  loading) and `ui.bodyState({ state: 'error' | 'stale' })` for a tone dot. A
  band is a full-bleed, one-line alert above content the body still shows
  (`ui.alert({ tone, band: true })`). See usage.md → "Small bodies".

### Changed

- The default bundle grows from 95.8 kB / 16.6 kB gzip to 97.1 kB / 16.8 kB,
  for the body state, the band and `--alert-tone`. `css/tool.css` is 81.8 kB /
  14.7 kB.
- Each `ui-alert--<tone>` also sets `--alert-tone`, which the band variant
  tints with.
- `.ui-meta` (the date · author line) moved from `css/site.css` to
  `css/content.css`, so the tool entry keeps it. The default bundle is
  unchanged, since content follows site in its cascade. A direct import of
  `css/site.css` that used `.ui-meta` also needs `css/content.css`.

## 0.13.0 — 2026-10-01

### Added

- **`ui-prose--blocks`: prose on a block editor's geometry.** A read view that
  must put every line where a block editor (BlockNote's model) draws it: 3px
  block padding instead of flow margins, line-height 1.5, headings with an 18px
  lead and a 1.6/1.3/1.15em scale at weight 600, a 24px list column with disc,
  circle and square, and the editor's task-row geometry. The px are exposed as
  `--prose-block`, `--prose-heading-lead` and `--prose-list-column`.
  `recipes.prose({ blocks: true })` emits it.
- **`css/blocknote.css`: BlockNote interop.** Maps BlockNote's `--bn-*` theme
  variables (editor, menus, tooltips, side menu, selection, border, font,
  radius) to bronto tokens, and its nine text highlights to the categorical
  identity inks and tints, so the editor follows theme, skins, contrast and the
  OLED surface. Import the unlayered build after BlockNote's stylesheet. See
  [BlockNote interop](docs/interop/blocknote.md).
- **Half spacing steps for dense tool chrome.** `--space-0-5`, `--space-0-75`,
  `--space-1-5` and `--space-2-5` (2, 3, 6 and 10px at a 16px root), scaled by
  the density presets with the rest of the scale.
- **Workspace layers.** `--z-canvas`, `--z-chrome`, `--z-panel`, `--z-modal`,
  `--z-menu`, `--z-tooltip` and `--z-navigation` name the stack of a tool drawn
  over a canvas, aliased to the page layers where they mean the same.
- **Zoom-aware hairlines and focus rings.** A host that scales bronto UI marks
  the scaled element `[data-ui-zoom]` and sets `--ui-zoom`; inside it `--ui-px`
  is one screen pixel and `--hairline`, `--focus-ring-width` and
  `--focus-ring-offset` are re-declared in it.

### Changed

- Every bronto focus ring reads `--focus-ring-width` and `--focus-ring-offset`
  instead of literal px. At the default zoom they are the same 2px, so nothing
  moves; inside a `[data-ui-zoom]` surface a ring keeps its on-screen width.
- The default bundle grows from 92.8 kB / 16.0 kB gzip to 95.8 kB / 16.6 kB:
  1.9 kB is the block prose variant, the rest the new tokens and the focus-ring
  variables. Notes are now the main reading surface, so block prose ships in
  the core prose vocabulary rather than a leaf.

### Internal

- `npm run check` runs every gate and ends with one line naming each gate
  that failed; `npm run check -- --bail` stops at the first. It was an `&&`
  chain that stopped at the first failure: since 0.8, 13 of the 20 failed CI
  runs were the Dependabot dev-dependency group stopping at `check:exports`
  on TypeScript 7, so no run measured the rest of the group. The gate list is
  the `check:*` scripts themselves, so `check:chain`, which checked that the
  hand-kept chain named every gate, is gone.
- The package contract no longer lists `react/`, `solid/`, `qwik/`,
  `svelte/` and `vue/` as authored JS directories; the package has none. The
  list is now read from `tsconfig.dts.json`.

## 0.12.0 — 2026-10-01

### Changed

- **BREAKING: a categorical palette with no accent slot.** `--chart-1..8` are
  now eight fixed hues — blue, orange, aqua, yellow, magenta, green, violet,
  red — authored as measured sRGB per theme. Series 1 is no longer
  `var(--accent)`, so an ordinary first series stops reading as an alert, and
  the `ACCENT` export of `@ponchia/ui/charts` is removed. The 0.11 set failed
  the measurable palette checks on the package's own surfaces (Okabe-Ito
  yellow and the slate outside the lightness band, the slate under the chroma
  floor). The values come from a consumer that validated and shipped them as a
  recorded divergence. `CATEGORICAL_HUES` names the slots.
- **BREAKING: `--chart-seq-*` is one blue hue in five steps.** `--chart-seq-6`
  is removed. Step 1 sits nearest the surface in both themes.
- **BREAKING: the static Vega config is frameless.** `brontoVegaConfig()` sets
  no plot frame, draws a single series in the first categorical hue, keeps a
  `--panel` gap between adjacent fills and sets readable label sizes. It is now
  generated from `@ponchia/ui/renderer`'s `vegaConfig()`, so the static and
  runtime configs share one mapping. `brontoVegaAccent()` returns `--accent`
  and `brontoVegaNeutral()` returns `--text-dim`.
- `check:charts` measures what a reader sees: per theme and against the panel,
  the page and the OLED surfaces, each slot sits inside the OKLCH lightness
  band and above the chroma floor, and adjacent slots stay apart under
  simulated protanopia/deuteranopia (ΔE×100 ≥ 6) and normal vision (≥ 15).
  All-pairs separation and sub-3:1 contrast are reported, not gated; the
  pattern fill remains the second channel. ADR-0001 step 7 records the
  amendment.

### Added

- **Categorical identity tokens** in `css/dataviz.css`: `--cat-1..8` (the same
  hues), `--cat-N-tint` (a 16% wash over `--panel`, so it follows skins and
  OLED) and `--cat-N-ink` (text that holds 4.5:1 on the panel, the page and its
  own tint, gated). For tags, participants and user-chosen tints — never
  status. `charts.json` gains `hues`, `tint` and `ink`.
- **`@ponchia/ui/renderer`** — the live theme for renderers that cannot read
  CSS. `readTokens()` resolves the bronto roles a renderer needs (surfaces,
  inks, lines, accent, status, fonts, categorical set and ramps) to `#rrggbb`
  or `rgba()` literals, following skins, contrast and the OLED surface.
  `observeTokens()` calls back when one of them changed. `vegaConfig()` and
  `xtermTheme()` map tokens to those renderers' configuration; `parseColor()`,
  `formatColor()` and `resolveColor()` convert any CSS colour, including
  `oklch()`, `lab()` and `color(display-p3 …)`.

See [the migration guide](docs/migrations/0.11-to-0.12.md).

## 0.11.0 — 2026-09-09

### Changed

- **BREAKING: complete the supporting typography.** Figure captions, legend
  text, generated-content labels and command groups use sentence-case sans.
  Technical log bodies and identifiers retain mono. Caption text is 14px at
  the default root size; inline citations no longer shrink below 12px.
- Figure keys and decision/action rows respond to their own available width.
  Two-up comparisons wrap intrinsically, including outside a report. Named
  containment on figure, decision-grid and actions wrappers is disabled for
  print to preserve document flow.

### Fixed

- Narrow evidence panels no longer squeeze a chart into 40px or decision text
  into 21px on a wide page. Long action status and legend labels wrap.
- Long mobile navigation labels wrap while keeping every destination reachable.
- The figure specimen's authored annotation stays inside its viewBox.
- Generated-content disclosure summaries retain the coarse-pointer target floor.

### Added

- Optional `discussion.css` with thread lists, quoted passages, messages and
  composers. State and posting remain host-owned; includes a local interactive
  specimen and a narrow-panel example.

- A working service specimen with deterministic loading, empty, error and stale
  states, environment/search filtering, retry and native-dialog sample creation.
  Unavailable observations never retain an operational health claim. Product
  state stays in the specimen; no framework state machine or data dependency.
- Narrow-parent regression checks, including a comparison outside a report,
  annotation bounds, long action status, citation text and recovery journeys.
- A complete-screen migration recipe for explicit consumer typography overrides.

See [the migration guide](docs/migrations/0.10-to-0.11.md).

## 0.10.0 — 2026-09-08

### Changed

- **BREAKING: readable tool and report defaults.** The root respects the
  browser's default text size. Everyday headings, labels, navigation, tables,
  and controls use sans typography and sentence case. Explicit `ui-display`,
  glyphs, and readouts retain the dot identity. Small text steps are 12/13/14px
  at a 16px root; corner tokens use a restrained 2/4/6/8px scale.
- Service panels remove redundant framing, overview metrics stay compact, and
  mobile navigation wraps. Inspectors adapt property rows to their own width.
  Reports use 1.125rem prose, a 68ch measure, and 11pt print text.
- ADR-0005 and composition recipes replace the shipped-duplicate admission
  rule with demonstrated task improvement. The default CSS budget is now
  100,000/18,000 bytes raw/gzip, restoring deliberate maintenance headroom.
- The example CI matrix derives from the shared registry. The complete browser
  suite runs in three required shards, with unchanged cross-engine coverage.
  Local Playwright uses an owned server on 8124 (`BRONTO_UI_TEST_PORT` override)
  and refuses to reuse a live specimen server.

### Removed

- **BREAKING:** the five framework adapter subpaths and optional framework
  peers, deprecated in 0.7. Initialize vanilla behaviors in the host lifecycle.
- **BREAKING:** the controlled modal initializer, detail type, data attribute,
  event, and `ui.modal({ open: true })` option. Use native dialog plus
  `initDialog`; native modal and drawer styling remain.
- Unused adapter parity machinery and Solid/Qwik/Vue packed examples. React
  and SvelteKit examples verify vanilla behavior mounting and cleanup.

### Added

- `ui-timestrip` in the opt-in state leaf: the host supplies normalized event
  positions and Bronto paints the rail, now marker, and severity marks. Old
  events can remain visible at the window edge through `data-outside`.
- `ui-severity-tone` exposes the existing severity mapping without painting a
  background, including for SVG consumers. `severity(level, { part: 'tone' })`
  returns the corresponding class and level.
- `ui-chip--dense` and `ui.chip({ dense: true })` for static pane-header labels,
  with pointer target floors when used on controls.
- A collapsed `details.ui-report__toc` recipe that keeps the decision first.

### Fixed

- Single-column stacks and prose shrink inside narrow parents; the showcase's
  nested legend grid fits its available width. Range inputs no longer add
  browser margins outside their containing width.
- The workbench specimen stacks panes when narrow, provides native file-row
  selection, and separates badge tone from row metadata styling.
- The service and report examples use favorable tones for decreasing latency.
  Report annotations leave room for their stroke outside the drawing bounds.
- Development URI, YAML, and color parsers receive compatible security updates.

### Internal

- Releases publish to npm by trusted publishing (OIDC) instead of a stored
  access token. The token that authenticated previous releases expired
  silently and surfaced only as a misleading `E404` after every gate and the
  publish approval had passed; there is now no credential to expire. The
  publisher identity is the repository, the workflow filename `release.yml`,
  and the `npm-publish` environment — see
  [ADR-0006](docs/adr/0006-trusted-publishing.md). The only packaged change is
  that ADR joining the published documentation set.

See [the migration guide](docs/migrations/0.9-to-0.10.md).

## 0.9.0 — 2026-08-11

Two additions and one correction, all from the same source: migrating a
downstream workbench onto 0.8.1 and watching where adoption stalled.

### Added

- **`.ui-row` — the dense selectable row, in the default bundle.** Every
  workbench grows a list of these: a search result, a file in an explorer, an
  outline entry, a backlink, a commit. Bronto already had the shape and it was
  imprisoned — `.ui-menu__item` was exactly it, minus the persistence — so a
  consumer building an explorer either claimed to be a menu or wrote the row
  again. The one that migrated had **63 families of it, about 1,200 lines**,
  each re-deciding the selected state, the hover, the ellipsis and the coarse
  floor.

  `__title` is the part that truncates and `__meta` is the part that does not;
  that asymmetry is the contract. Selection reads `aria-selected` /
  `aria-current` — the attributes a host already sets for assistive tech — so
  the visual state cannot disagree with the announced one. `--stacked` for a
  row with a snippet, `--ruled` for a list that reads as one object.

  Selection: use **`aria-current`** for a row that is merely the current one —
  that is the common case and valid anywhere. `aria-selected` is only legal on a
  role that accepts it (`option` in a `listbox`, or `row`/`tab`/`gridcell`/
  `treeitem`); on a bare `<button>` it is invalid ARIA and axe rates it
  *critical*. This release's own demo shipped that mistake, three a11y specs
  went red, and a new unit gate now catches the same class of error in
  milliseconds rather than twenty minutes of browser matrix.

  `min-inline-size: 0` is on the row itself and is not optional: a row is
  usually a flex or grid item, whose automatic minimum is its min-content width,
  and `__title` is `white-space: nowrap` — so without it a long title stops the
  row shrinking and pushes its container past the viewport instead of
  truncating. The demo proved that at 320/360/390px before release.

  It is in **core**, unusually for a new surface, because `.ui-menu__item` now
  composes it and a core component cannot depend on an opt-in leaf. It also
  earns the roadmap's stronger argument for a core addition on two counts:
  universal application chrome, and it *reduces duplicated core markup*. A unit
  proof asserts the menu item does not restate what the row says, so the two
  cannot drift back apart.

### Changed

- **`.ui-menu` no longer welds placement into the surface.** It used to declare
  `position: absolute` plus a trigger-relative offset, so a menu opened at a
  pointer — a canvas context menu, a long-press — could not use it at all, and
  consumers redeclared the panel, border, radius and shadow to get a surface.
  The dropdown placement is now `--dropdown` (unchanged behaviour, opted into)
  and `--at-pointer` takes a menu out of flow for a host that computes its own
  position.

  **Migration:** add `ui-menu--dropdown` to an existing `ui-menu` inside a
  `ui-menu-host`. Without it the surface renders in flow.

### Fixed

- **`data-density` claimed more than it does, and the guard could not tell.**
  `docs/theming.md` said the preset applies "on any element". Measured: it
  re-points the `--space-*` scale, so it reaches the ~40 components whose
  padding is expressed in that scale and **none** of the rest — `ui-alert` and
  `ui-menu__item` among them. The remaining paddings are tuned pairs like
  `0.5rem 0.55rem` that a seven-step scale cannot express, so tokenising them
  would change how they look at the default density; exactly one declaration in
  the whole catalog mapped cleanly and was converted.

  The docs now say which components respond and why the others cannot, and a
  test pins the responding set. The previous guard checked that the preset
  changed the token *family*, which is true and useless — it would have passed
  with every component hardcoded. That is the third contract in three releases
  whose gate validated the mechanism instead of the effect, after the 43.5px tap
  floor and the shipped-docs list.

### Notes

- Bundle budget raised to 95,600 B / 16,400 B for `.ui-row` (+1,234 B raw /
  +83 B gzip). The gzip cost is small because the new rules compress against the
  menu rules they replaced.

## 0.8.1 — 2026-08-11

A packaging fix, plus dependency hygiene. **No published CSS, token, class, or
type artifact changed** — `dist/`, `css/`, `tokens/` and `classes/` are byte-for-byte
identical to 0.8.0. If you are on 0.8.0 and do not read the migration guide from
the package, there is nothing here for you.

### Fixed

- **`docs/migrations/0.7-to-0.8.md` was missing from the published package.**
  0.8.0's one breaking change is the colorway canvas re-point, `CHANGELOG.md`
  and `MIGRATIONS.json` both point at that guide, and it was not in the tarball —
  so a consumer following the pointer from an offline install got nothing. The
  guide was written, linked, and gated by `check:doc-links`; it was simply never
  added to `package.json`'s hand-maintained `files` array.
- **The gate that should have caught it was circular.** `check-pack.mjs` derived
  its set of shipped docs *from* `files`, so it proved every listed doc ships and
  could never prove a doc that exists is listed. It now asserts the other
  direction for the two directories the published package itself references —
  `docs/migrations/` (from `MIGRATIONS.json`) and `docs/adr/` (from
  `docs/architecture.md`) — where a missing file costs a reader a dead pointer.
  Other `docs/` pages stay opt-in: the package ships a curated subset by design.

### Changed

- **Dependency sweep** — ten Dependabot PRs landed as one reviewed change. Every
  advisory was **devDependency-scoped**; the package declares no runtime
  dependencies and `npm audit --omit=dev` was already clean, so none of them
  ever reached a consumer. `npm audit` now reports 0 across the whole tree
  (`fast-uri`, `js-yaml`, `nanoid`, `pdfjs-dist`, `postcss`, `shell-quote`,
  `tar`, `undici`), the toolchain moved (`jsdom` 30, `knip` 6.32, `prettier`
  3.9.6, `publint` 0.3.23, `react`/`react-dom` 19.2.8, `solid-js` 1.9.14,
  `stylelint` 17.14.1, `vega` 6.3.1, `@arethetypeswrong/cli` 0.18.5), the pinned
  action SHAs moved, and the Astro example moved to Astro 7 (verified building
  against the packed 0.8.0 tarball).

### Not taken

- **TypeScript 7.** Its default export is now `{ version, versionMajorMinor }` —
  the compiler-API namespace moved in the native port — so `ts.ScriptTarget` is
  `undefined` and `scripts/lib/import-policy.mjs` throws at `check:exports`.
  Adapting this repository's AST tooling to that API is real work with its own
  review, and there is no security driver: TypeScript is not in any advisory
  here. Pinned to `^6.0.3`; Dependabot's dev-group PR stays open for it.

## 0.8.0 — 2026-08-11

A single-consumer release. A full-surface audit of the largest downstream
consumer — a Yjs-collaborative spatial canvas workspace with roughly ten
thousand lines of its own CSS — found it using 26 of the 646 published classes
and hand-rebuilding much of the rest. Everything here comes from what that
consumer had to write because the framework did not provide it, or got wrong.

### BREAKING

- **A colorway now re-points the neutral canvas, so every skinned surface
  changes appearance.** No class, token, attribute, or export was removed or
  renamed, and `bronto-ui-check` will report nothing — the break is *visual*,
  which is why it is called out here rather than left in Changed. Until 0.7,
  `data-bronto-skin` moved `--accent` and nothing else, and ADR-0001 step 4 said
  so explicitly; a consumer could reasonably have relied on the canvas staying
  neutral. From 0.8 the ten `SKIN_CANVAS_TOKENS` (`--bg`, `--bg-elevated`,
  `--panel`, `--panel-strong`, `--panel-soft`, `--line`, `--line-strong`,
  `--text`, `--text-soft`, `--text-dim`) are re-pointed per skin per theme.
  - **If you want the old look**, redeclare those ten tokens after the skin
    import; they are ordinary custom properties on `:root[data-bronto-skin=…]`
    and un-layered app CSS wins.
  - **If you already hand-wrote a canvas** for a skin — the case this change
    exists to serve — delete it and check the result; yours was almost certainly
    not contrast-gated, and this one is.
  - **Contrast is not a regression risk.** Each neutral keeps the core token's
    OKLCH lightness exactly, and `check-contrast` re-measures all 21 gated
    pairings per skin per theme. Status colours are untouched by design.

### Fixed

- **The coarse-pointer tap target was 43.5px, not 44px.** The floor was written
  as a bare `2.9rem` at 23 sites across 8 stylesheets, and `css/base.css` sets
  `html { font-size: 0.9375rem }` — so every control the framework floats on
  touch landed half a pixel under WCAG 2.5.5 and both platform HIGs, and drifted
  further under any host that shrank the root. Several comments asserted
  "≈ 44px". Both floors are now published tokens clamped in px
  (`--tap-target: max(44px, 2.9rem)`, `--tap-target-min: max(24px, 1.6rem)`) —
  the same `max()` shape the 24px floor already used correctly in five places.
  The e2e that should have caught this was derived from the token (`2.9 * rem`),
  so it agreed with the bug; it now asserts the external 44px standard, and a
  unit gate walks every `@media (pointer: coarse)` block and fails on any floor
  written as a bare length. That gate immediately found one more:
  `.ui-table__sort`.

### Added

- **`.ui-button__label`** — the label slot for icon buttons. Wrap a button's
  text in it and `--icon` decides whether the words are painted; they stay in
  the accessible name and in text-based test selectors either way. One markup
  shape serves both the labelled and icon-only forms, and no `aria-label` can
  drift out of sync with the visible wording. The slot also ellipsis rather
  than wrapping, so a labelled button in a tight bar shrinks instead of pushing
  its neighbours out. `ui.button()` is unchanged; `cls.buttonLabel` is new.
- **`.ui-button--dense`** — a size tier for bars whose *height* is the
  constraint (a pane title bar, a packed toolbar, a table row's actions). It
  lowers only the visual floor, to `--tap-target-min`; the coarse-pointer block
  still floats it to the full `--tap-target`, so a control shrunk for a mouse is
  never shrunk for a finger. `ui.button({ size: 'dense' })`.
- **A canonical severity ladder** (`css/state.css`, opt-in). Bronto shipped the
  *tones* long ago but never the *scale* — the tier names, their order, and the
  attribute carrying them — so every consumer invented the ladder and it drifted
  inside a single app: one surface saying `critical|error|warning|note`, the
  next `bad|warn`, a third `critical|warning|info|ok`, under two different
  attribute names, so findings did not sort against alerts. The ladder is
  `critical` › `error` › `warning` › `notice` › `ok`, carried on **one**
  attribute (`data-level`) across `.ui-severity`, `.ui-severity-dot` and
  `.ui-severity-row`, with `SEVERITY_LEVELS` and `severity()` exported so a host
  drives filters and sorts from the same list the CSS paints. `unknown` sits
  deliberately *outside* the ordering: it means "not measured", and collapsing
  it into `ok` is how a dead collector reads as a healthy system.
- **`.ui-pane`** (`css/workbench.css`, opt-in) — the window that `.ui-panel`
  (a padded card) and `.ui-inspector` (head plus body) are not: a grab header,
  a title that renames in place via `__title-input` without moving layout, and
  an `__actions` slot that **scrolls rather than pushing** its last control past
  the pane's clipped edge — the failure that leaves a Focus or Disconnect button
  present, in the a11y tree, and unreachable.
- **`.ui-toolstrip--pane`** — the app has one toolstrip; a workbench full of
  panes has one *per pane*, and those need different framing (no frame of their
  own, a rule against the content below) and must not wrap, since a second row
  would resize live content on every state change. `.ui-toolstrip__fill` marks
  the element that absorbs slack and gives it back first.
- **`--anchored` / `--anchor-block-start` / `--anchor-block-end`** on
  `.ui-selectionbar` and `.ui-toolstrip`. Both `--floating` bars were raised but
  position-less, so every consumer re-derived the placement — including the
  `max(offset, inset)` shape that keeps a bar out from under the home indicator.
- **Empty-state slots and an invite variant.** `.ui-empty-state` was a dashed
  box that styled a `<p>`, so every empty surface in an app re-invented the same
  three parts under a different name and they drifted. `__glyph` / `__lead` /
  `__hint` name them, and `--invite` is the different job: an empty state
  *reports absence*, an invite *offers the next action*, so it drops the dashed
  frame and centres in the space it is given.
- **Safe-area tokens** — `--safe-area-top / -right / -bottom / -left`, defaulting
  to `env(safe-area-inset-*, 0px)`. The framework had **no** `env()` awareness
  while shipping eight viewport-anchored surfaces; all eight now read them: the
  app rail and topbar, a sticky site header, the skip link, both toast stacks,
  the drawer modal and the lightbox. Every rule uses
  `max(<authored>, var(--safe-area-*))`, so desktop rendering is unchanged. The
  values are indirected through custom properties rather than called at the
  point of use because `env()` cannot be emulated by a desktop test runner —
  and because a host inside its own chrome (embedded webview, kiosk frame) needs
  to declare the real insets.

### Changed

- **ADR-0001 step 4 amended** to permit the canvas re-point above (see
  BREAKING). "Amber CRT" used to leave the surface grey, which is what drove the
  consumer to hand-write an amber canvas in raw hex — dark theme only, outside
  OKLCH, outside the contrast gate, with `e-ink` silently un-tinted. The canvas
  is **derived rather than picked**: every neutral keeps the core token's OKLCH
  *lightness* exactly and moves only hue and a small role-scaled chroma.
  `check-skins` now rejects a partial canvas and a one-theme-only canvas, the
  two shapes the hand-written version had.
- **`check:recipe-types` no longer mis-attributes options.** It sliced the
  factory body to end-of-file, so the *last* recipe's chunk swallowed everything
  declared after the `ui` object — an unrelated helper exported below it had its
  string branches blamed on whichever recipe happened to be last. It now stops
  at the object's own closing brace.
- `docs/stability.md` records the audit and the correction it forces on this
  project's adoption model: "no inspected consumer imports the surface" has been
  measuring **discoverability, not demand**. Those thirteen zero-use primitives
  were never rejected; they were never found. Non-adoption of a leaf the
  consumer never imported is not evidence for retiring it at 1.0.

### Notes

- Anything that reads `tokens.dtcg.json` should know that six new scale tokens
  are **deliberately absent** from it, listed in the root extension's
  `omittedCssVariables`: the two tap-target floors are `max()` comparisons and
  the four safe-area insets are `env()` reads, neither of which has a conforming
  DTCG shape. `tokens.json` carries the authored CSS. This is the same treatment
  `--shadow` and the em trackings already get.

## 0.7.0 — 2026-07-20

A consumer-first contract-hardening release. Ten real applications and tools,
plus a web-platform and design-system landscape review, found more value in
repairing and pruning the existing surface than in adding components.

### BREAKING

- **DTCG 2025.10 values.** `tokens.dtcg.json` now emits portable structured
  color, dimension, duration, numeric, and cubic-bezier values. It no longer
  emits CSS strings for typed values or `$value: null` placeholders. Derived
  colors are resolved per light/dark theme; the original CSS expression remains
  under `$extensions["com.ponchia.css"].authoredValue`. CSS-only shadow
  expressions and em-based letter-spacing remain in `tokens.json` rather than
  pretending to be portable DTCG values; the DTCG root extension lists those
  deliberate omissions. Update JSON readers using
  [`docs/migrations/0.6-to-0.7.md`](docs/migrations/0.6-to-0.7.md).

### Added

- **Consumer contract checker.** The zero-dependency `bronto-ui-check` binary
  scans consumer source for literal `ui-*` classes absent from `classes.json`
  and unresolved Bronto-like `var(--*)` references. It understands local token
  definitions, strips source comments, ignores Markdown prose and build/vendor
  directories, supports explicit allowlists, and can emit JSON.
- **Non-drag splitter controls.** Buttons inside a splitter can use
  `data-bronto-splitter-adjust="-10"` / `"10"` to change the first pane by a
  signed percentage-point delta. This gives pointer users the same resize
  function without requiring a dragging gesture.
- **DTCG semantic gate.** `check:dtcg` validates every emitted typed value and
  rejects null placeholders or malformed structured values before publication.

### Changed

- **Meter ownership.** The already-public `ui-meter__row`, `__label`, and
  `__value` styling moves from optional `report.css` into core `feedback.css`.
  Core consumers now receive the layout the public class contract promised;
  report-kit output remains visually unchanged. Together with the explicit
  24×24px coarse-pointer utility-link floors below, this intentionally adds
  1,121 B raw / 164 B gzip to the default bundle versus 0.6.12 (now 89.9 kB raw /
  15.4 kB gzip) and raises the hard budget only enough to admit those contracts.
- **Release evidence.** CI and release documentation now treat real-consumer
  literal validation, packed-tarball upgrades, and payload reporting as the
  evidence for 1.0 readiness.

### Fixed

- **Live dot composition.** `ui-dot--live` now describes motion only. A
  standalone live dot defaults to success, while an explicit accent, success,
  warning, danger, or info tone controls both the dot and its pulse ring,
  including forced-colors mode.
- **OS theme synchronization.** An OS `prefers-color-scheme` change now emits
  the existing `bronto:themechange` event when no explicit theme is set, keeping
  consumer-rendered charts, icons, and labels synchronized with the CSS theme.
- **Named dialogs.** `initDialog()` now warns once when an opened native dialog
  has no `aria-label`, `aria-labelledby`, or `title`. All shipped framework
  examples provide a name.
- **Touch target floors.** Breadcrumb and footer utility links reach the WCAG
  2.5.8 24 CSS-pixel floor under a coarse pointer.

### Deprecated

- **Framework adapter subpaths.** `@ponchia/ui/react`, `/solid`, `/qwik`,
  `/svelte`, and `/vue` remain compatible in 0.7 but are scheduled for removal
  no earlier than 0.8. None of the ten inspected consumers imports them; use the
  vanilla behavior initializers in each framework's mount/cleanup lifecycle.
- **Controlled non-native modal.** `initModal()`, its adapter bindings, and
  `data-bronto-modal` remain compatible in 0.7 but are scheduled for removal no
  earlier than 0.8. Prefer native `<dialog>` with `initDialog()`.

### Verified

- Unit, type, generated-artifact, package, schema, DTCG, class/token contract,
  browser, accessibility, packed-example, and real-consumer checks are release
  gates. The release evidence records consumer classes and imported surfaces
  without exposing private project details.

## 0.6.12 — 2026-07-10

A stabilization patch. It changes no public class, token name, behavior name,
or package path.

### Changed

- **Small-text readability.** The `--text-2xs` value increases from `0.68rem`
  to `0.72rem`. This raises the floor for form labels, table headings,
  provenance metadata, report captions, and other dense secondary text without
  changing the type scale's public names or the default CSS payload size.
- **1.0 stabilization mode.** Public catalog growth is frozen while real
  consumers move onto one current minor. The stability guide now distinguishes
  downstream-proven surfaces from package-only proof and unproven 1.0
  candidates. New public surface needs explicit maintainer approval to reopen
  the catalog.

### Fixed

- **Controlled-modal ownership.** `initModal()` now reconciles one stack per
  document instead of letting each modal own `inert` independently. Sibling
  portal modals no longer inert the active top modal, nested stacks restore the
  previous modal correctly, late-added background nodes join the trap, and a
  popover opened from the top modal remains interactive when its panel is
  portaled elsewhere. Cleanup still preserves author-owned `inert` state.
- **Maintainer documentation.** The architecture map now identifies
  `css/generated.css` as authored trust-surface CSS, and the roadmap reflects
  the current release and WOFF2 font payload.

### Verified

- Focused unit coverage exercises sibling and nested modal stacks, portaled
  popovers, late background nodes, focus restoration, and cleanup ownership.
  The non-pixel browser suite carries the same sibling-stack, portal, and
  late-node scenario.

## 0.6.11 — 2026-07-04

A correctness, accessibility, and performance release drawn from a multi-pass
review of the behavior layer and docs. No default-bundle contract change, no
public class/token/subpath renames, and the CSS payload is unchanged — this is a
non-breaking patch. Every change landed through PR CI, including the aggregate
`check` gate and cross-engine (Chromium/Firefox/WebKit) end-to-end coverage.

### Added

- **Localization hooks for behavior-authored text.** New opt-in attributes let
  hosts localize the strings behaviors set on enhanced elements —
  `data-bronto-carousel-roledescription` and
  `data-bronto-carousel-slide-roledescription` for carousel/slide
  `aria-roledescription`, plus localized error-summary title, toast dismiss
  label, combobox, command, and table hooks. Absent an override, the existing
  English defaults are preserved, and any author-provided value now wins over the
  behavior's default.
- **Root type resolution for `import '@ponchia/ui'`.** The package root now
  advertises a types condition (`index.d.ts`) so TypeScript consumers importing
  the CSS entry resolve types without reaching for a subpath.
- **Concepts guide.** `docs/concepts.md` is a new canonical mental-model page
  that consolidates the load-bearing concepts (CSS-first, the cooperative
  `@layer bronto` override model, the tiered color model, primitive ownership,
  identity-vs-inventory bundling, token projections, and the package-path
  contract) that were previously scattered across many pages.

### Fixed

- **Focus management.** `focusInto()` now skips hidden, `inert`, disabled, and
  non-rendered candidates instead of parking focus on them; the tablist roving
  set excludes hidden/disabled tabs so keyboard focus can always enter it; and a
  closed controlled (non-`<dialog>`) modal is hidden while closed, so its
  controls are no longer reachable by Tab.
- **Screen-reader announcements.** The assertive toast stack no longer
  re-announces earlier toasts when a new one arrives; the command palette's empty
  state is a polite live region; the combobox listbox now mirrors the input's
  accessible name (via `aria-labelledby`/`title`); and toast/command/combobox
  ARIA added at init is fully restored on cleanup.
- **Internationalization.** Combobox, command, and table filtering/sorting are
  locale-aware, and a Unicode minus in `data-sort-value` (e.g. `−3,5`) now parses
  correctly instead of sorting as `-35`.
- **Connector resilience and scoping.** A malformed enum on one connector no
  longer aborts its siblings, and a scoped connector can no longer resolve an
  endpoint to a same-id element outside its root.
- **Attribute parsing.** Splitter ARIA-range values are parsed strictly and
  rewritten to a canonical numeric form; splitter/glyph enum values and the
  dismissible target selector are trimmed, so trailing whitespace no longer
  changes behavior.
- **Theme following the OS.** When no explicit theme is set, an OS
  `prefers-color-scheme` change now updates the theme toggle's `aria-pressed`,
  and the render-blocking no-flash snippets in the integration and framework
  guides validate the stored value is `light` or `dark` before applying it.

### Performance

- **Behavior hot paths.** Connector redraw caches its records and endpoint map
  per init and batches scroll/resize/observer redraws through one animation
  frame; source backrefs build one island-scoped id map instead of rescanning
  per citation; the crosshair caches layout reads across `pointermove`; the table
  text sort decorates rows once instead of parsing inside the comparator; and a
  genuine user scroll during a carousel's programmatic-scroll suppression is now
  replayed instead of dropped. Observable behavior is unchanged.

### Verified

- PR CI passed for every change, including the aggregate `check` gate,
  cross-engine e2e, CodeQL, and the packed examples matrix. The default bundle
  and its size are unchanged; this release adds optional localization hooks, root
  type resolution, and documentation, and fixes accessibility, correctness, and
  performance defects without moving the public contract.

## 0.6.10 — 2026-06-23

### Added

- **Workbench toolstrip primitives.** `workbench.css`, the generated class
  registry, and the workbench docs now cover floating toolstrips, compact
  grouped actions, segmented button groups, contextual labels, and search/action
  slots for dense canvas or editor-style tools.
- **Annotation composition guidance.** The annotation docs now show how the
  dependency-free `@ponchia/ui` annotation surface composes with richer
  annotation engines in a workbench-style consumer, while keeping runtime
  package boundaries explicit.

### Verified

- **Release evidence.** PR CI passed for the workbench expansion, including the
  aggregate `check` gate, Chromium e2e, CodeQL, and the packed examples matrix.
  This release adds opt-in workbench CSS and generated class/docs artifacts; it
  does not move the default bundle contract.

## 0.6.9 — 2026-06-20

### Changed

- **Product doctrine and 1.0 readiness.** `CONTRIBUTING.md`, `ROADMAP.md`,
  `docs/architecture.md`, and `docs/stability.md` now codify the
  core-vs-opt-in surface boundary, the refusal list, registry-first gate
  maintenance, packed-tarball proof, a gate-backed 1.0 readiness ledger, and
  release evidence policy for public-safe downstream proof. The stability
  matrix now includes explicit rows for every exported opt-in CSS leaf plus
  the public machine-readable/theme subpath families.
- **Registry-backed gate ownership.** `check:report`,
  `check:component-matrix`, `check:exports`, and `check:consumer-surface` now
  share internal registries for the reporting toolbox, exported CSS leaves, and
  optional framework peers instead of carrying separate hand-maintained lists.
  The matrix gates also share one proof-owner helper for cached file reads,
  owner-file existence, word matching, and required text evidence.
- **Native complexity budget.** `check:complexity` is now part of the
  aggregate `npm run check` chain. It uses the existing TypeScript parser to
  keep function-level cyclomatic complexity at 12 or lower and function size
  under budget without carrying per-function exception baselines.
- **Gate-backed annotation package boundary.** The annotation docs now make the
  split with `@ponchia/annotations` explicit: `@ponchia/ui/annotations` remains
  the dependency-free Bronto static-helper compatibility surface, while richer
  placement, renderer, editing, and adapter work belongs in the sibling
  annotation package. `check:public-metadata` now guards that doctrine,
  `check:exports` rejects packed code or declaration references to the sibling
  package, and no runtime or public type dependency is added to `@ponchia/ui`.
- **Public source hygiene guard.** `check:public-hygiene` now rejects internal
  audit-ticket markers across repository source files, and still scans the
  packed public text files for private terms, local paths, and secret-looking
  assignments. `check:doc-links` also rejects executable URL schemes in public
  authoring docs. The pass also brought
  `behaviors/inert.js` under the generated declaration emit inputs so
  `check:dts-emit` covers its public `.d.ts` surface.

### Verified

- **Release evidence.** `npm run check` passed for `0.6.9`, including
  `check:pack`, `check:consumer-surface`, `check:consumer-types`,
  `check:examples`, `check:publint`, and `check:attw`. No default-bundle budget
  movement; this release is gate/docs/declaration hardening rather than a
  runtime surface expansion.
- **Downstream proof.** The packed current tarball installed into a disposable
  copy of a real React/Vite app consumer that imports `@ponchia/ui/classes`,
  `@ponchia/ui/behaviors`, `@ponchia/ui/tokens/resolved.json`, and
  `@ponchia/ui/vega`; its `typecheck` and production `build` both passed.

### Fixed

- **Report sidecar hash validation.** `report-claims.v1` now requires
  `contentHash` values to use exact SHA digest lengths (`sha256`, `sha384`, or
  `sha512`) instead of accepting any hex payload after a supported algorithm
  prefix.

## 0.6.8 — 2026-06-16

Patch release for the deep UI-framework audit: broader browser/package gates,
clean-consumer verification, and runtime fixes found while hardening the public
surface. No breaking changes, no `MIGRATIONS.json` entry.

### Added

- **Tarball and clean-consumer gates.** `check:consumer-surface` now imports
  public JS/JSON subpaths from the packed package, resolves concrete CSS/doc/
  font subpaths without optional peers, and verifies behavior initializers stay
  SSR-safe. `check:consumer-types` installs the tarball into a clean TypeScript
  consumer and compiles every typed `@ponchia/ui/...` package subpath.
- **Matrix ownership gates.** `check:component-matrix`,
  `check:behavior-matrix`, `check:helper-matrix`, and `check:binding-matrix`
  now require shipped CSS leaves, public behavior exports, helper modules, and
  framework bindings to have explicit docs, type, unit, or browser ownership.
- **Public docs and package hygiene gates.** `check:doc-links` validates local
  links and heading anchors across shipped docs, authoring docs, and the docs
  viewer route list. `check:schemas`, `check:visual-baselines`,
  `check:playwright-container`, and stronger `check:contract` / `check:report`
  coverage close stale public snippets, missing visual baselines, and invalid
  report/schema surfaces.
- **Broader browser coverage.** The Playwright suite now pins docs viewer deep
  links, cascade-layer behavior, source focusing, renderer geometry, behavior
  cleanup/idempotency, connector transforms, annotation motion/overflow,
  command interactions, crosshair payloads, responsive overflow, and more
  forced-colors/reduced-motion contracts.
- **Packed example smoke coverage.** The example runner now builds and smokes
  the packed examples from one registry, with richer runtime assertions,
  desktop/mobile visual health, and optional Chromium/Firefox/WebKit coverage.
  Astro joins the packed-tarball smoke matrix.
- **Renderer theme helper coverage.** Chart, Mermaid, D2, and Vega package
  helpers now have type/runtime coverage that checks default exports,
  theme-selection fallbacks, resolved colors, and `var()` leak prevention.

### Fixed

- **Standalone dot readouts survive `report-kit.css`.** `crosshair.css` now
  scopes pinned readout-chip styling to `.ui-crosshair .ui-readout`, so the
  core dot-matrix `.ui-readout` keeps its normal inline layout when a report
  imports the full report kit.
- **Rendered docs deep links work.** `docs/index.html` now preserves
  `doc.md#section` routes, generates deterministic heading IDs, keeps
  same-page anchors inside the current doc route, and drops the invalid
  meta-CSP `frame-ancestors` directive that browsers reported as a console
  error.
- **Command adapter docs match the shipped matrix.** `docs/command.md`, the
  stability matrix, package-contract provenance, and `llms.txt` now name the
  Svelte action and Vue directive/plugin paths alongside the React/Solid/Qwik
  bindings.
- **CodeQL review findings.** Docs slug helpers no longer use incomplete
  regex-based tag stripping, and wildcard package-subpath expansion replaces
  every placeholder rather than only the first one.

### Changed

- `npm run check` now owns the unit suite through `check:unit`; CI, release
  workflow validation, PR templates, release docs, and package-contract docs
  all describe the same aggregate gate instead of duplicating `npm test`.
- Release hygiene now verifies the aggregate check includes unit coverage and
  prevents duplicate release-workflow unit runs from drifting out of sync.
- `check:exports` now pins package-level CSS metadata: the top-level `style`
  field, root export targets, `files`, and CSS-preserving `sideEffects`.
- Type-only coverage now instantiates Svelte action and Vue directive
  declarations from consumer-shaped code, including invalid root-shape
  assertions.
- The default bundle budget is recalibrated to 91 kB raw / 15.65 kB gzip after
  the audited bundle landed below that ceiling at 87.9 kB raw / 15.0 kB gzip.

## 0.6.7 — 2026-06-15

### Added

- **Tailwind v4 bridge.** `@ponchia/ui/tailwind` / `@ponchia/ui/tailwind.css`
  ships a CSS-first `@theme inline` bridge, `bronto-dark` and `bronto-oled`
  custom variants, and source-registration guidance for Tailwind v4 projects.
  `docs/interop/tailwind.md` documents `@reference` / `@source`, and
  `examples/tailwind-vite` builds from the packed tarball and browser-smokes
  the emitted Bronto utilities.
- **Svelte and Vue lifecycle adapters.** `@ponchia/ui/svelte` exports
  dependency-free actions over the delegated behavior layer, and
  `@ponchia/ui/vue` exports directives, plugin helpers, and the shared
  imperative `useToast()` helper.
  The SvelteKit example now uses the Svelte adapter, and `examples/vue-vite`
  covers Vue consumer install/build/runtime smoke from the packed package.
- **Figma Variables handoff artifact.** `tokens/figma.variables.json` is
  generated from the token source, exported as
  `@ponchia/ui/tokens/figma.variables.json`, drift-checked in `check:fresh`,
  and documented as a local import/sync handoff alongside the DTCG token export.
- **Report-primitive batch** — four additive opt-in leaves for static reports
  and explanation surfaces: `ui-figure` (`css/figure.css`) for reusable
  chart/diagram/media stages with overlay, key and fallback-data slots;
  `ui-interval` (`css/interval.css`) for host-normalised low/high evidence
  windows; `ui-clamp` (`css/clamp.css`) for bounded excerpts with CSS-only
  show-more / show-less; and `ui-highlights` (`css/highlights.css`) for named
  CSS Custom Highlight API paints (`bronto-evidence`, `bronto-search`,
  `bronto-current`). All stay out of `dist/bronto.css`; `figure.css` and
  `highlights.css` join the analytical roll-up.
- **Background-job state primitive.** `state.css` now includes `ui-job` for
  persistent asynchronous work: written status, determinate progress via
  `--job-progress`, optional actions, compact mode, and queued/running/blocked/
  failed/complete tones. The host still owns polling, retries, cancellation,
  and announcements.
- **Workbench splitter primitive.** `workbench.css` now includes
  `ui-splitter` / `__pane` / `__handle` plus the optional `initSplitter`
  behavior (`data-bronto-splitter`) for focusable ARIA separator handles,
  keyboard and pointer resizing, `--splitter-pos` / `aria-valuenow` sync, and a
  `bronto:splitter:resize` event. React, Solid, Qwik, Svelte, and Vue adapters
  expose the matching hook/action/directive.
- **Service identity demo and report kit.** The public demo set now includes
  `demo/service.html`, a cross-service `ui-app-shell` specimen that keeps the
  default bundle centered on shared app identity. Static reports get the new
  opt-in `@ponchia/ui/css/report-kit.css` roll-up for the complete report
  vocabulary, plus `@ponchia/ui/schemas/report-claims.v1.schema.json` for
  claim/source sidecar validation.
- **Public-package hardening gates.** `check:public-hygiene`,
  `check:variables`, and `check:migrations` are now part of the aggregate
  `npm run check` chain, covering packed public text leaks, undefined CSS
  custom-property references, and migration-map/doc alignment.

### Fixed

- **Silent CSS token typos.** `css/workbench.css` now uses the real
  `--focus-ring` token for splitter focus outlines, and `css/report.css` uses
  `--text-base` for finding/evidence value text. The new variable-reference
  gate prevents the same class of no-op declaration from shipping again.
- **Print: stat tiles and table rows no longer slice across PDF page
  boundaries** — the report-layer `@media print` break-inside guard covered
  the report shell (`.ui-report__*`, `.ui-claim`, `.ui-evidence-item`) but
  not `.ui-stat` tiles, generic `.ui-statgrid` children, or `tr`, so a stat
  card landing on a page boundary printed half its value on one page and the
  delta on the next (observed in a consumer report's PDF export). Guarded at
  the item level — the tile / the row, not the container — so a long
  statgrid or table can still span pages.

- **Sidenote gutter contract actually works now** — `--sidenote-width` /
  `--sidenote-gap` are root-scoped instead of declared only on the notes.
  The documented host wiring (container
  `padding-inline-end: calc(var(--sidenote-width) + var(--sidenote-gap))`)
  referenced vars an ancestor could never see, so the calc was invalid and
  silently reserved NO gutter — the floated notes spilled past the page edge
  (caught by a real report's visual QA). The demo now uses the documented
  calc verbatim (it previously masked the bug with a hardcoded literal), and
  a wide-viewport e2e asserts the contract: gutter resolves, float stays
  on-page, no horizontal scroll. Overriding either knob on the container now
  re-sizes the notes and the gutter together.

### Changed

- `docs/d2.md` tokenize recipe covers the full embedded-style surface
  (`.color-*` / `.background-color-*` rules, not just `fill`/`stroke`) and
  warns that `tooltip:`/`|md` shapes embed GitHub-Primer styling that
  survives tokenization. `docs/sidenote.md` documents the root-scoped knobs.
- Local verification is now reproducible through named scripts:
  `npm run test:e2e:nonpixel` runs every non-screenshot Playwright spec across
  Chromium, Firefox, and WebKit, while `npm run test:examples` packs the real
  tarball, builds all examples in temp directories, and browser-smokes the
  runtime examples. `CONTRIBUTING.md`, `docs/release.md`, and
  `docs/architecture.md` describe when to use those local gates versus the
  pinned-container screenshot gate.
- `check:examples` keeps the example inventory, CI matrix, browser-smoke list,
  README rows, and preview ports aligned from one registry; `check:dead` now
  runs in the aggregate `npm run check` chain.
- README, package metadata, usage docs, workbench examples, and the docs index
  now frame `@ponchia/ui` as the shared UI identity layer for services, tools,
  sites, and reports, with reports as an opt-in consumer rather than the center
  of the package.

## 0.6.6 — 2026-06-10

Consolidation pass from the 2026-06-10 multi-agent audit: two real PDF-export
defects fixed and gated, the reporting hub routed to every shipped leaf, and
the drift-prone hand lists in the gates derived from registries. No breaking
changes, no `MIGRATIONS.json` entry.

### Fixed

- **Print overprint (the big one)** — Chromium-class print engines restart
  grid tracks at each page break, printing new rows over the running content.
  `css/report.css` `@media print` now demotes the vertical document-flow
  wrappers (`.ui-report`, `__section`, `__figure`, `__actions`,
  `.ui-evidence-ledger`) to block flow with margin-emulated gaps; the column
  grids (`.ui-compare`, `.ui-meter__row`, `.ui-report__decision-item`,
  `.ui-evidence-grid`) keep their tracks. Gated by a promoted multi-page
  fixture (`test/e2e/_report-print.fixture.html`) whose PDF is parsed with
  pdfjs-dist and asserted overlap-free (`test/e2e/report-print.spec.mjs`).
- **Silent module-figure drop in PDFs** — reports that render figures from
  relative `<script type="module">` imports lose them over `file://` (CORS).
  `scripts/render-pdf.mjs` gains `--serve` (loopback HTTP + the report's
  `data-report-ready` signal), forwards page/console errors to stderr, and
  warns when a module report is rendered over `file://`.
- **Demo class drift** — `demo/index.html` still used the renamed
  `ui-inspector__header` (now `__head`), and `demo/version-history-report.html`
  carried a never-registered cover-mark class; both were silent no-ops. The
  generated reference's analytical-leaves note now names `ui-annotation`
  (singular, the real class).
- `scripts/smoke-example.mjs` imports Chromium from `@playwright/test` (the
  installed devDependency) instead of the hoisting-dependent bare
  `playwright` specifier.

### Added

- **Reporting hub routing** — `docs/reporting.md`'s analytical-toolbox table
  now routes ALL report-relevant leaves (spark, bullet, diff, code, sidenote,
  textref, term, glossary, contents rail, tree, dot surfaces) and states
  precisely what `analytical.css` does and does not bundle. New convention in
  CONTRIBUTING: a new report-relevant leaf ships in the same PR as its
  routing row.
- **`npm run release:prep -- X.Y.Z`** — one-shot release prep: version + lock
  bump, CHANGELOG heading dating, and re-pinning every `@ponchia/ui@X.Y.Z`
  literal across the gated shipped docs AND the ungated `demo/*.html` pages
  (which had already drifted a version behind).
- **Cross-engine e2e on merge-to-main** — pushes to `main` that touch
  engine-sensitive surfaces (`css/`, `dist/`, `behaviors/`, `fonts/`,
  `tokens/`, `test/e2e/`, the Playwright config) now run the firefox/webkit
  pass too, instead of deferring it to the release tag.
- **`demo/dots.html`** — the dot data surfaces (waffle, activity, level,
  dotgauge, readout, spark--dots, halftone, dotfit) get a specimen page,
  axe/guard coverage in `demos.spec`, and render-geometry boundingBox
  assertions (their only possible executable gate — they ship no JS).
- jsdom coverage for `initDisabledGuard` (activation blocking, Tab
  pass-through, root scoping, cleanup).

### Changed

- `check:report` and `check:pack` derive their scan/entrypoint lists from
  registries (`package.json` `files`/`exports` via `scripts/lib/css-leaves.mjs`
  + `lib/shipped-docs.mjs`) instead of hand lists that had drifted behind the
  post-0.6.0 leaves. The broadened scan immediately caught the demo/reference
  class defects above. `core.css` imports are now closed over an explicit
  CORE_BUNDLE allowlist in both directions.
- `ROADMAP.md` / `docs/frontier-primitives.md` reconciled to 0.6.7: the
  report/provenance/explanation lane is named the proven core; ui-job,
  ui-conflict, drag/drop workbench follow-ons, the tree roving-focus kernel,
  and command follow-ons are explicitly dormant until a real app consumer
  exists; the 2026-06-09 scout keeps (ui-interval, ui-clamp, ui-highlights) are
  recorded as report-lane candidates gated behind the routing hub. The adoption
  stance is stated in ROADMAP.

## 0.6.5 — 2026-06-09

Patch release: the source-backref behavior, the report decision/evidence
grammar, and package/release hardening. No breaking changes, no
`MIGRATIONS.json` entry.

### Added

- **`initSources` behavior** — optional citation→source backref focus:
  inside a `data-bronto-sources` host, a `.ui-citation[href^="#"]` (or an
  explicit `data-bronto-source-ref`) click scrolls to its source card and
  marks it `is-source-active`, emitting a cancellable focus event with the
  `SourceFocusDetail` payload. Progressive enhancement over authored ids —
  no DOM is generated; framework bindings re-export it.
- **Report decision/evidence grammar** — adds public, static report primitives
  for decision blocks, severity-labelled findings, compact evidence packets and
  follow-up action rows.
- **Claim and action-register grammar** — adds claim status blocks, structured
  finding parts, evidence-method parts, evidence ledgers, decision detail rows,
  and action owner/due/criteria/source parts for more auditable reports.
- **Package contract doc** — `docs/package-contract.md` pins the published
  surface (exports map, files allowlist, generated-artifact freshness) that
  `check:pack`/`check:publint`/`check:attw` enforce.

### Changed

- **Report fixtures and local guardrails** — expands the standalone and full
  report demos with source/provenance examples, SVG accessibility details and a
  local-only public-boundary terms gate for `check:report`.
- **Report shape checks** — `check:report` now parses report demos with a DOM
  instead of regexes and validates the public claim/action contracts.
- **Release observability** — the release workflow gains a metadata gate and
  theme-axis verification; the examples consumer build broadens its smoke
  coverage.

## 0.6.4 — 2026-06-08

Patch release for the dot-matrix expansion and static report hardening shipped
in #116. No breaking changes, no `MIGRATIONS.json` entry.

### Added

- **Dot-matrix/glyph expansion** — adds the generated glyph metadata surface,
  docs, and contract tests for the expanded dot/readout vocabulary.
- **Report authoring guidance** — documents the static report grammar around
  captions, alert bodies, table wrapping, and local/CDN asset handling.

### Fixed

- **Static report print/layout behavior** — hardens report print margins and
  table wrapping so standalone HTML reports and PDF exports degrade more
  predictably across long tokens and normal prose.

## 0.6.3 — 2026-06-08

Patch release to publish the WebKit release fix after the `v0.6.1` and `v0.6.2`
tag runs failed before npm publish. No public API change, no class contract
change, no `MIGRATIONS.json` entry.

The 0.6.2 test-only diagnosis was incomplete: WebKit does not reliably honor
the unprefixed `user-select: none` on the generated line-number affordances.
The actual fix is CSS-side. `css/diff.css` and `css/code.css` now include the
legacy `-webkit-user-select: none` declaration alongside the standard property,
and the built `dist/css/diff.css` / `dist/css/code.css` leaves were regenerated.

### Fixed

- **Diff/code line numbers in WebKit** — `.ui-diff__ln`,
  `.ui-diff__code::before`, and `.ui-code--numbered .ui-code__line::before`
  now opt out of selection in WebKit as well as chromium/firefox.

### Internal

- **`test/e2e/diff.spec.mjs`** — reads the CSSOM `-webkit-user-select` value
  before the standard property so the assertion verifies the prefixed WebKit
  path directly.

## 0.6.2 — 2026-06-07

Release attempt: test-only WebKit e2e patch for the `0.6.1` publish blocker. The
tag was cut but the release still did not publish. The diagnosis here was
superseded by `0.6.3`, which fixes the CSS rule itself.

### Internal

- **`test/e2e/diff.spec.mjs`** — read `getComputedStyle(el).userSelect ||
  getComputedStyle(el).webkitUserSelect` for the line-number
  `user-select: none` assertion, so the test passes on WebKit (where
  `userSelect` is `undefined` even when the standard CSS rule is
  applied) as well as chromium + firefox.

## 0.6.1 — 2026-06-07

Dev-only patch: refreshes the SHA-pinned GitHub Actions used by CI
([`actions/checkout`](https://github.com/actions/checkout) 6.0.2 → 6.0.3 —
SHA-256 repository init fix, expanded SHA regex; and
[`github/codeql-action`](https://github.com/github/codeql-action) 4.36.0 →
4.36.2 — CLI version caching, exponential-backoff SARIF polling, CodeQL
bundle 2.25.6) plus a small DevDeps bump ([react](https://github.com/facebook/react)
/[react-dom](https://github.com/facebook/react) 19.2.6 → 19.2.7 — Server
Components `FormData` fix; [stylelint](https://github.com/stylelint/stylelint)
17.12.0 → 17.13.0). No public API, no published CSS/JS, no `MIGRATIONS.json`
entry. All bumps were authored by Dependabot and landed through #109 + #110.

### Internal

- Bump the **actions** Dependabot group: `actions/checkout` 6.0.2 → 6.0.3 and
  `github/codeql-action` 4.36.0 → 4.36.2 (CI only, SHA-pinned in
  `.github/workflows/*.yml`).
- Bump the **dev** Dependabot group: `react`/`react-dom` 19.2.6 → 19.2.7 and
  `stylelint` 17.12.0 → 17.13.0 (devDependencies only — the package itself
  ships zero runtime deps).

## 0.6.0 — 2026-06-03

Accumulates the post-0.5.0 work: a multi-agent audit pass (accessibility
hardening, a behavior/binding scope-safety fix, codegen/gate tightening) plus a
**breaking** charting realignment. The local static-bar renderer
(`.ui-chart*`) is **removed** — a chart needs scales + data binding, which the
analytical layer refuses to own. In its place, bronto becomes a themeable target
for **Vega-Lite** (`@ponchia/ui/vega`), the same tokens-as-data path as Mermaid
and D2. The data-viz **palette** (`--chart-*`, `tokens/charts.json`) and the
**legend** layer are unchanged. Pin `~0.5` → re-pin `~0.6`; see
[`MIGRATIONS.json`](./MIGRATIONS.json) (`0.5`→`0.6`).

### Added

- **`@ponchia/ui/vega`** (+ `vega.json`) — an on-brand Vega-Lite / Vega
  [`config`](https://vega.github.io/vega-lite/docs/config.html) resolved per
  theme (the idiomatic `vega-themes` shape): monochrome chrome + one rationed
  accent, `range.category/ordinal/ramp/heatmap/diverging` from the CVD-safe
  data-viz palette. `brontoVegaConfig(theme)`. Resolved hex (Vega bakes colours
  into SVG/canvas, can't read `var()`); gated structurally **and** by a headless
  render-probe that asserts the colours land on a rendered chart. Vega is the
  consumer's renderer — config only, not a dependency. See `docs/vega.md`.
- **`ui-delta`** — a standalone trend/change indicator (core primitive): an
  arrow glyph (the non-colour channel) plus the figure, with
  `--up`/`--down`/`--flat`, and `--invert` to swap only the tone when "up" is
  the bad direction (latency, error rate, cost). `ui.delta({ dir, invert })`.
- **`ui-compare`** — a fluid side-by-side / before-after layout for the report
  layer (`css/report.css`): `__col`, `__head`, and `--2up`.
  `ui.compare({ cols })`.
- **`@ponchia/ui/classes.json`** — the class vocabulary as language-neutral
  data (`groups`/`classes`/`states`/`customProperties`), so a non-JS/non-TS
  host or an external linter can validate emitted markup without executing the
  ESM `cls` map or parsing the `.d.ts`. Generated from `cls`; drift-checked and
  its `states`/`customProperties` gated against the stylesheet.
- **`tokens/resolved.json` `scale` block** — the resolved non-colour scales
  (spacing/radius/type/z/motion, `var()` chains flattened), completing the
  token contract for non-CSS hosts (previously colour-only).
- **`--display-weight` / `--display-weight-strong`** (700 / 800) — the weight of
  the Doto dot-matrix display face, now a token. Themes/skins can re-tune how
  heavy display text renders in one place.
- On-brand **Mermaid** (`@ponchia/ui/mermaid`, `mermaid.json`) and **D2**
  (`@ponchia/ui/d2`, `d2.json`) theme maps — resolved per-theme palettes
  projected from the same tokens, gated. Diagrams stay the consumer's renderer;
  these are config only.
- Annotation geometry options: `connectorElbow({ mid })` (turn position along the
  dominant axis), `notePlacement({ inset })` (reserve the title stroke-halo so a
  placement that "fits" doesn't clip), and a `spread` half-angle on both
  `connectorEndArrow` and the shared `arrowHead` kernel.
- **`brontoVegaAccent(theme)` / `brontoVegaNeutral(theme)`** (`@ponchia/ui/vega`)
  — the exact per-theme hexes for `range.category`'s accent (series 1) and
  neutral (last series), so spending the accent on one emphasised mark needs no
  palette-index reverse-engineering.
- **`--on-accent`** token — the readable ink for a label on **any accent fill**
  (button, badge, themed chart bar, a Vega/D2 node). Resolves to `--button-text`
  (white on the light accent, black on the dark) and is gated ≥ 4.5:1 in
  `docs/contrast.md`. Use it instead of `--accent-text`, which is the inverse
  (accent-coloured text for a *neutral* background, ~1.3:1 on an accent fill).
- **`.ui-src`** standalone trust pill (`cls.src`, `css/sources.css`) — wears a
  `.ui-src--*` tone (verified / reviewed / generated / unverified / stale /
  conflict) on its own, for a bare trust label outside a citation or source card.
  Previously the `.ui-src--*` modifiers only painted a `--src-tone` with no
  standalone host, so a lone pill validated against `classes.json` yet rendered
  nothing.

### Removed

- **BREAKING: the local static bar-chart renderer (`.ui-chart`, `.ui-chart__plot`,
  `__bar`, `__label`, `__track`, `__fill`, `__fallback`, `__caption`).** A chart
  needs scales and data binding — out of scope for a CSS-first analytical layer
  (ADR-0002). Replace with a Vega-Lite chart themed via `@ponchia/ui/vega`, or a
  hand-authored token-themed inline `<svg>`, inside a `.ui-report__figure` with a
  `.ui-report__caption` and a `.ui-legend` key. The `--chart-value` inline knob
  is gone; the `--chart-color`/`--chart-pattern` swatch knobs remain (legend).
  See `MIGRATIONS.json` (`0.5`→`0.6`) and `docs/vega.md`.

### Changed

- **Annotation connectors are crisper.** `connectorEndArrow` now defaults to a
  sharper head (half-angle 0.32 ≈ 37°, size 8 vs the former blunt 0.45 / 7).
  Author-facing geometry only; the `arrowHead` kernel default is unchanged, so
  node-connector arrowheads don't move.

### Accessibility

- **Coarse-pointer tap-target floors extended to navigation.** The 2.9 rem
  touch floor (already on primitives/forms/feedback) now also covers
  `.ui-sitenav a`, `.ui-app-nav a`, `.ui-sitemenu > summary`, and
  `.ui-themetoggle__button` under `@media (pointer: coarse)` — the primary nav
  affordances were below the 44 px target on touch.
- **App shell uses dynamic viewport units.** `100vh` → `100dvh` (shell/body) and
  the scrolling rail → `100svh`, so the rail and its pinned account/footer no
  longer fall under the mobile URL bar.
- **Forced-colors status dots stay distinct.** `.ui-dot--success/--warning/--danger/--info`
  and `.ui-dotmatrix__cell--hot/--accent` now map to distinct system colors
  under Windows High Contrast instead of collapsing to one — the only signal
  these carry is colour.
- **Keyboard affordance parity.** `.ui-menu__item:focus-visible` gets the same
  row highlight as hover; the segmented control's focus ring is now inset so the
  container's `overflow: hidden` no longer clips it.
- **Reduced-motion skeleton.** `.ui-skeleton` flattens to a solid placeholder
  under `prefers-reduced-motion` instead of freezing mid-shimmer.

### Fixed

- **Published-type drift.** `ui.meter({ tone: 'info' })` and
  `ui.bracketNote({ tone: 'success' })` emit real classes at runtime, but the
  generated `.d.ts` tone unions (hand-mirrored in `gen-dts.mjs`) omitted them, so
  a TS consumer got a spurious type error for a value that renders. The unions
  now match the factory; a new `check:recipe-types` gate cross-checks every
  factory's string-literal options against its `*Opts` union so this whole class
  of drift fails CI.
- **Component-library audit (16-agent dogfood pass) — the validates-but-no-ops
  cluster.** A whole-surface audit found the meter-style trap (a class/token that
  validates and paints but silently does nothing without an undocumented
  precondition) recurring across components. Fixed:
  - `aria-disabled="true"` on `.ui-button` / `.ui-link` now sets
    `pointer-events: none` — it looked dead but a real `<a>` still navigated.
  - Disabled affordance reaches the controls that wrap a native input
    (`.ui-switch` / `.ui-check` / `.ui-segmented__option` via `:has(input:disabled)`,
    plus `.ui-range` / `.ui-file`) — they previously looked operable and their
    label kept `cursor: pointer`.
  - Bare `[aria-current]` selectors (`.ui-sitenav`, `.ui-breadcrumb__item`) now
    scope `:not([aria-current='false'])`, so a correctly-authored
    `aria-current="false"` link is no longer styled as current.
  - The active-tab forced-colors re-assert moved from `base.css` to
    `disclosure.css` (after the default rule) — an earlier bundle leaf let the
    accent default override it, so the selected tab lost its only HC cue.
  - `.ui-meter__fill` / `.ui-progress__bar` get a system colour under
    `forced-colors`, so the measured proportion stays visible.
  - `.ui-search` gains a 2px keyboard focus ring to match every sibling input
    (it had only a 1px border-colour shift).
  - `.ui-prose` gets `overflow-wrap: break-word` — long tokens in
    machine-generated Markdown forced horizontal page scroll.
  - `.ui-mark--draw` is scoped to fill styles (`:not(--underline, --box, --strike)`)
    so it no longer looks applied while doing nothing.
  - `.ui-cq` hardcodes its container-name (the `@container bronto` collapse
    queries hardcode it, so a `--cq-name` override silently killed the collapse).
  - `initPopover()` seeds resting ARIA (`aria-haspopup`, `aria-controls`,
    `aria-expanded`) and syncs `aria-expanded` when the UA closes a native
    popover; `toast()` validates `tone` (an unknown string rendered an unstyled
    neutral toast) and warns; the combobox listbox gets an accessible name.
  - `.ui-error-summary__title` uses the legible sans, not the low-legibility Doto
    display face. `.ui-input` / `.ui-search` autofill stays on-theme.
  - `.ui-reveal` hidden state is gated on `scripting: enabled` (genuinely degrades
    visible with no JS; the prior comment lied) — and `ui-scroll-reveal` is the
    documented zero-JS path.
  - Parity modifiers added: `.ui-meter--info`, `.ui-bracket-note--success`.
- Responsive/mobile hardening across the framework: `rem`-rooted type for WCAG
  1.4.4, coarse-pointer tap-target floors, combobox/tour-note viewport clamps,
  and `@media (hover)` gating — with a new responsive e2e sweep.
- **Faint numbers on stat cards.** `.ui-stat__value` / `.ui-app-metric__value`
  (and the report cover/section titles, rail brand, panel titles, `.ui-display`,
  `.ui-quote`) set the Doto display face but no weight, so they rendered at the
  thinnest cut (400). They now apply `--display-weight(-strong)` — visibly bolder
  and more legible, on screen and in print.
- **Painted data surfaces dropped in the PDF.** Headless-Chromium print drops
  backgrounds by default, silently blanking the data-bearing fills. Dot-matrix
  cells, the segmented meter, status dots, masked glyphs, highlight marks,
  connector lines, and progress/meter fills now carry `print-color-adjust: exact`
  so they survive the A4 print/PDF that the report kit targets.
- **Dark-theme cards/tables printed dark-on-white.** The dark→ink token remap was
  scoped to `.ui-report`; it is now lifted to the print `:root` (in the exempt
  token-definition file), so a bare `.ui-card` / `.ui-statgrid` / `.ui-table` —
  the markup an external LLM emits — also prints legibly.
- Inline `ui-citation` no longer dumps its full URL mid-sentence when printed
  (the reference list carries the URL); `ui-legend--with-values` values are
  right-aligned for a clean tabular column.
- **Annotation elbow connector was a 45° chamfer, not a dogleg.**
  `connectorElbow` turned by `min(|dx|,|dy|)`, drawing a diagonal stub the
  `stroke-linejoin` bevel never matched. It now delegates to the connectors
  geometry kernel's right-angle `elbowPath` (H/V/H), so an annotation leader and
  a node connector draw the same elbow.
- **Scoped behaviors no longer hijack the whole document on a null root.**
  `init*({ root })` with an explicitly-provided-but-unready root (a framework
  ref still `null` at mount, a conditional that hasn't rendered) now no-ops
  instead of silently widening to document-wide delegation. The react/solid/qwik
  bindings emit `root: null` for the not-ready case so the distinction survives
  the boundary; passing no `root` still delegates from `document` exactly as
  before. Affects every delegated behavior (dialog, menu, combobox, …).
- **`--report-width` / `--report-padding-block` are now declared defaults** on
  `.ui-report` — they were read with inline fallbacks but never declared, so the
  override surface was undiscoverable and `--report-measure` looked like the
  width knob when it isn't.
- Carousel's IntersectionObserver is now set up and torn down in lockstep with
  its event binding, removing a one-tick window where a re-init left two
  observers on the same slides.
- **`ui-meter` / `ui-progress` fill painted a 0×0 box.** `.ui-meter__fill` and
  `.ui-progress__bar` set `block-size`/`inline-size` but no `display`, so on the
  documented `<span>` fill (an inline box ignores width/height) the bar rendered
  empty — a "validates-but-renders-nothing" trap the registry and docs both
  hid. They are now `display: block`. Found by a second multi-agent dogfood
  pass; guarded going forward by a render-geometry e2e (below).

### Documentation

- LLM-authored static reports: a prominent CSS-loading note (bundler vs
  `node_modules` vs CDN) and a copy-pasteable CDN report in
  `docs/reporting.md`; clarified that `dist/bronto.css` does **not** include the
  opt-in report/chart/legend/annotation layers; number/date formatting
  guidance; and a standalone, no-build report reference
  (`demo/report-standalone.html`).
- Resolved the `is-*` self-contradiction: the framework's own
  `is-num`/`is-pos`/`is-neg`/`is-key`/`is-open` state hooks are valid even
  though they deliberately live outside `cls` (documented in
  `docs/reference.md` and `classes.json`).
- Clarified two standing contracts in `docs/architecture.md`: `css/analytical.css`
  is the roll-up of exactly the seven figure leaves (annotations, legend, marks,
  connectors, spotlight, crosshair, selection) — `sources`/`state`/`generated`/
  `workbench`/`command` are adjacent leaves imported individually — and the root
  `.` export is CSS-only (no runtime JS at the root). Pre-1.0 stability/pinning
  spelled out in `docs/stability.md`. `docs/workbench.md` notes that
  `.ui-selectionbar` is unrelated to the `.ui-sel--*` selection-emphasis classes.
- Honest JSDoc limits: combobox/command read options from the DOM at init
  (re-run after replacing them); popover restores focus on Escape but not on
  outside-click; the table sorter is locale-naive display-text; mask-mode glyphs
  are single-tone.
- **Foreign-renderer recipes hardened after a multi-agent dogfooding pass**
  (build five real reports across the whole stack, review from every POV). The
  Vega CDN recipe now pins the `/build/*.min.js` UMD bundles and `renderer:'svg'`
  (a bare `cdn.jsdelivr.net/npm/vega@6` tag has no `window.vega`, so the previous
  recipe rendered nothing); the `file://`-portable path (inline the config — an
  imported/fetched config is CORS-blocked from disk) is now explicit. New
  `docs/reporting.md` recipes: "Theming a live report" (the theme-toggle/re-embed
  foot-guns — clear the host, container-width-while-hidden, Mermaid source vs
  output), live charts are `ui-screen-only` while the table prints (a kept live
  chart bakes the on-screen theme), `ui-meter`/`ui-quote` markup, and the
  sequential/diverging frozen-figure ramp. `docs/d2.md` gains a frozen
  inline-`<svg>`-from-slots recipe and on-accent-ink guidance; `docs/vega.md`
  documents the theme-inverting ramp and the OKLCH-vs-d3 gradient-key drift.
- `docs/annotations.md` states the rule in both directions: a data annotation
  must stay readable (not `aria-hidden`), a decorative one must be hidden.
- **A second dogfood pass closed the foreign-renderer/contract gaps it found.**
  `docs/sources.md` + `llms.txt` now document the standalone `.ui-src` trust
  pill and state that a `ui-src--*` tone class **needs a host** (a bare
  `<span class="ui-src--verified">` validates but renders nothing), and name the
  source-card body part as `__excerpt` (not `__detail`). `docs/mermaid.md`:
  `gantt`/`timeline` are **not** covered by the base `themeVariables` (they
  render with Mermaid's own defaults — prefer the native `ui-timeline` for a
  report). `docs/mermaid.md` + `docs/d2.md` gain the same `file://` CORS caveat
  Vega carries (inline the map or pre-render). `docs/vega.md`: select the themed
  ramp with `scale: { range: 'heatmap' }` — **not** `scheme:`, which throws — and
  the accent/neutral series map to `--chart-1` / `--chart-8`, so a legend keys
  them with `ui-legend__swatch--1`/`--8` (`docs/legends.md`). `docs/reporting.md`:
  the live-theme recipe now `finalize()`s the prior Vega view before re-embed
  (was leaking a view per toggle), and notes `ui-meter --value` clamps at 100
  (put an over-target figure in the written label). `docs/marks.md`: `ui-mark`
  is a behind-text highlight (contrast-safe; never needs `--on-accent`).

### Internal

- **New `check:versions` gate** — every `@ponchia/ui@X.Y.Z` literal in a shipped
  doc (`llms.txt`, `docs/reporting.md`, …) must equal `package.json`, so a stale
  CDN pin can't ship to LLM/copy-paste consumers on the next bump.
- **Dev-dependency Vega bumped to the v6 stack** — the render-probe now runs on
  `vega@^6.2.0` + `vega-lite@^6.4.3` (Vega-Lite 6 peers Vega 6; a Vega-Lite-6 ÷
  Vega-5 mix is incoherent). The theme `config` is version-independent resolved
  hex, so the artifacts and the probe assertions are unchanged; the documented
  CDN recipe is re-pinned to the matching majors (`vega@6.2.0` / `vega-lite@6.4.3`
  / `vega-embed@7.1.0`, all still shipping a UMD `/build/*.min.js`). Vega remains
  the consumer's renderer, not a runtime dependency.
- **New `check:doc-recipes` gate** — a `<script src>` CDN recipe in a shipped doc
  must pin a jsDelivr `/build/*.min.js` UMD bundle, never a bare
  `cdn.jsdelivr.net/npm/<pkg>@N` redirect (which serves a module bundle with no
  global and renders nothing). Docs are otherwise an untested surface; this is
  the structural guard that closes the broken-recipe class the dogfood pass
  found. `<link href>` CSS and prose mentions are exempt.
- **`classes.json` `customProperties` expanded** to cover the load-bearing,
  no-op-without-it knobs the audit found undocumented: the **required**
  `--icon-mask` (a bare `.ui-icon` paints a solid square without it) and
  `--ui-vt-name` (`.ui-vt` is inert without it), plus `--icon-size`. The
  `states` manifest comment now explicitly names the runtime-managed hooks it
  deliberately excludes (`is-leaving`/`is-visible`/`is-in`/`is-on`) so the
  omission reads as intentional, not a gap. `--on-accent` is annotated at its
  token source as a read-only export for foreign renderers (in-DOM ink is
  `--button-text`). `contrast.md` now prints APCA `Lc` to one decimal so an
  advisory shortfall (e.g. `Lc 44.9`) no longer rounds to a passing-looking `45`.
- Raw bundle budget 81 → 82 kB for the accessibility/state
  blocks (gzip held ~14.1 kB — the additions are repetitive media-query and
  `:has()`/`:not()` rules that compress well).
- **Code-health pass — two new gates + targeted dedup, no churn.**
  A code-health pass (complexity / duplication / AI-slop / missing-best-practice)
  that deliberately left working, gate-protected code alone. Added:
  `check:recipe-types` (factory↔`.d.ts` option parity, above) and `check:chain`
  (every `check:*` script is wired into the aggregate `check` chain — closes the
  silent-coverage-drop class; it would have caught a forgotten gate). Reconciled
  a latent bug — `clamp()` had drifted between `connectors` and
  `annotations`; the two now share one scalar/geometry kernel (the guarded form).
  Dedup that removed real duplication: a shared `collectHosts()` /
  `scrollIntoViewSafe()` / `wrapIndex()` in `behaviors/internal.js` (~9 behaviors),
  a `freshnessErrors()` helper reused by 7 drift gates, the shared `CSS_COLOR`
  regex across the 3 foreign-renderer gates, `check-report`'s opt-in list as a
  loop, `check-pack`'s shipped-docs derived from `pkg.files`, and a looser
  `check-classes` recipe-scrape. README hero de-densified; `srcTone` matched to
  `stateTone`'s idiom; the intentional badge accent-mix (45% vs 40%) documented.
- **`check:dist` now asserts source-coverage** — every `css/*.css` leaf must be
  bundled, an opt-in `EXTRA_LEAVES` entry, or a roll-up; an orphaned leaf that
  would ship nothing now fails loudly (the inverse of the existing stale-dist
  guard).
- **`check:dts-emit` now compares `.d.ts.map`** mapping data (volatile `sources`
  path normalized), closing a drift hole the code comment had acknowledged.
- DTCG export types `--display-weight*` as the spec `fontWeight` type (was
  `number`). Corrected stale `check-tokens.mjs` doc references (the real gate is
  `check:fresh`).
- Tests: binding hook-surface parity is now **derived** from the modules (the old
  hard-coded list silently omitted the five analytical hooks); a new
  `analytical-boundary` test makes the "no scales/state/fetch/global-hotkey"
  contract executable; a new behavior test pins the null-root no-op.
- Removed four dead keyframes (`scan`/`growBar`/`drawLine`/`pulseNode`) from
  `motion.css`. Raw bundle budget 80 → 81 kB for the accessibility blocks (gzip
  held ~14.0 kB).
- **`classes.json` `--value` retargeted** to `.ui-meter__fill, .ui-progress__bar`
  (was the `.ui-meter, .ui-progress` track parent) — the custom property is read
  on the fill child, so the machine-readable manifest now matches where an author
  actually sets it.
- **New render-geometry e2e** (`test/e2e/render-geometry.spec.mjs`) — launches a
  browser at the demo's real report primitives and asserts the `.ui-meter__fill`
  / `.ui-progress__bar` fills and the standalone `.ui-src` pill paint a non-zero
  box (via `getBoundingClientRect`, not the inline-box-lying
  `getComputedStyle().inlineSize`). Closes the validates-but-renders-nothing
  category that hid the meter regression. The demo gains a standalone `.ui-src`
  pill row to exercise it.

## 0.5.0 — 2026-06-02

A **minor** that builds out the "analytical & generated-report UI" identity: a
full suite of opt-in **communication primitives** — SVG annotations, legends,
text/evidence marks, leader-line connectors, a guided-focus spotlight, a
crosshair/readout, a selection-state vocabulary, label declutter + direct labels
(`declutterLabels`/`directLabels`), and a source/citation/provenance **trust
layer** — plus a consolidation pass over them. Each owns its visual grammar and
pure geometry and refuses to own scales/state/hit-testing (no chart engine).

Per the project's versioning policy, breaking changes ship in the minor. This
release carries three: the opt-in report kit's chart data key moved into the new
legend layer (`.ui-chart__legend`/`__swatch` removed — see Changed and
[`MIGRATIONS.json`](MIGRATIONS.json)), annotation arrowheads now render via
the shared connectors geometry kernel (a small path-shape change), and the
opt-in marks' rationed-accent tone was renamed `evidence`→`accent` to match the
rest of the analytical tone vocabulary. Everything else is additive and opt-in,
save for the tiny `.ui-shortcut` keyboard-hint primitive that joins the core
layer; the rest of the default `dist/bronto.css` is unchanged. Also folds in the
0.4.x maintenance hardening that had not yet been released.

### Added

- **SVG annotations** (`@ponchia/ui/css/annotations.css`,
  `@ponchia/ui/annotations`, `.ui-annotation*`, `ui.annotation()`): an opt-in
  annotation layer for charts, reports, and analytical figures, following the
  d3-annotation grammar (a **subject** marks the thing, a **connector** points
  away, a **note** carries the text). Ships a class grammar (variants for
  label/callout/elbow/curve/circle/rect/threshold/badge/bracket/band/slope/
  compare/cluster/axis/timeline/evidence, six tones, and opt-in
  `draw`/`reveal`/`pulse`/`focus` motion that respects `prefers-reduced-motion`)
  plus tiny geometry helpers that return SVG strings only — they own no chart
  scales, mutate no DOM, and provide no edit mode. Documented in
  [`docs/annotations.md`](docs/annotations.md) and gated by `check:report`.
- **Legends / data keys** (`@ponchia/ui/css/legend.css`, `.ui-legend*`,
  `ui.legend()`/`ui.legendItem()`/`ui.legendSwatch()`, `initLegend`): an opt-in,
  standalone data-key layer that reads the `--chart-*` palette tokens.
  Categorical, continuous gradient (sequential + `--diverging`), threshold, and
  pattern keys; swatch colour set inline (`--chart-color`) or via
  `.ui-legend__swatch--1..8` index helpers; vertical/compact/with-values
  layouts. WCAG 1.4.1 by construction (the text label is the non-colour
  channel), with `forced-colors` and print care. Optional interactive
  (series-toggling) entries are `<button aria-pressed>` controls: `initLegend`
  flips `aria-pressed`/`.is-inactive` and emits `bronto:legend:toggle`
  (`{ series, active }`) — the host owns hiding the series and any `aria-live`
  announcement (it is never a chart engine). Optional `useLegend` hook in the
  React/Solid/Qwik bindings. New `check:legend` gate proves swatch colours are a
  subset of `tokens/charts.js` and never a raw hex. Documented in
  [`docs/legends.md`](docs/legends.md).
- **Text marks / evidence** (`@ponchia/ui/css/marks.css`, `.ui-mark*`,
  `.ui-bracket-note*`, `ui.mark()`/`ui.bracketNote()`): an opt-in layer of
  sober, report-grade emphasis for running prose — the counterpart to SVG
  annotations (annotations call out a figure, marks call out a sentence). Inline
  `.ui-mark` (highlight/underline/box/strike; `--accent` + status
  tones; `--draw` reduced-motion-safe sweep) for use on `<mark>`, and
  `.ui-bracket-note` for bracketing a whole passage. Pure CSS on semantic
  tokens, monochrome by default, with `forced-colors` care. Documented in
  [`docs/marks.md`](docs/marks.md).
- **Connectors / leader lines** (`@ponchia/ui/css/connectors.css`,
  `@ponchia/ui/connectors`, `.ui-connector*`, `initConnectors`, `ui.connector()`):
  an opt-in layer that draws a line between two DOM elements (the
  page-coordinate cousin of annotations). Pure geometry helpers
  (`connectRects`/`connectorPath`/`arrowHead`/…) that return SVG strings and own
  no DOM, an `.ui-connector` overlay grammar (straight/elbow/curve, arrow/dot
  ends, tones, dashed, `--draw`), and an optional `initConnectors` behavior that
  draws + tracks on resize/scroll. `useConnectors` in the bindings. Documented in
  [`docs/connectors.md`](docs/connectors.md).
- **Spotlight / guided focus** (`@ponchia/ui/css/spotlight.css`, `.ui-spotlight*`,
  `.ui-tour-note*`, `initSpotlight`, `ui.spotlight()`): an opt-in guided-focus
  overlay — a box-shadow cutout over a target element, optional ring, and a
  callout note. `initSpotlight` positions the cutout (`--spot-x/y/w/h`) and
  re-places on resize/scroll and when `data-target` changes. Deliberately **not**
  a tour engine — the host owns step order/advancing/visibility. `useSpotlight`
  in the bindings. Documented in [`docs/spotlight.md`](docs/spotlight.md).
- **Crosshair / readout** (`@ponchia/ui/css/crosshair.css`, `.ui-crosshair*`,
  `.ui-readout`, `initCrosshair`, `ui.crosshair()`): an opt-in plot ruler +
  pinned readout. `initCrosshair` tracks the pointer over a
  `[data-bronto-crosshair]` plot, sets `--crosshair-x/y`, and dispatches
  `bronto:crosshair:move` with px + 0–1 fractions — it reports position only and
  never maps pixels to data (that needs the host's scales). `useCrosshair` in the
  bindings. Documented in [`docs/crosshair.md`](docs/crosshair.md).
- **Selection states** (`@ponchia/ui/css/selection.css`, `.ui-sel*`,
  `ui.sel()`): a tiny cross-cutting selection-emphasis vocabulary
  (`--on`/`--off`/`--maybe`) reusable on chart marks, table rows, list items, or
  map regions. The carve-out from brush/lasso — Bronto styles the states; the
  host owns the selection/hit-test logic. Documented in
  [`docs/selection.md`](docs/selection.md).
- **Sources, citations & provenance** (`@ponchia/ui/css/sources.css`,
  `.ui-citation`/`.ui-source-card`/`.ui-source-list`/`.ui-provenance`,
  `ui.citation()`/`ui.source()`/`ui.provenance()`): an opt-in, CSS-only **trust
  layer** for generated reports and AI output — the grammar for "where did this
  come from?". A cross-cutting `.ui-src--*` state (verified/reviewed/generated/
  unverified/stale/conflict) sets a rationed tone, always paired with an
  author-written label (never colour alone). Bronto owns the grammar + states;
  the host owns fetching, citation numbering, and trust. The first
  frontier-primitive beyond the analytical suite. Documented in
  [`docs/sources.md`](docs/sources.md).
- **Keyboard-shortcut hint** (`.ui-shortcut` + `.ui-shortcut__sep`, core): a tiny
  universal-chrome primitive that lays out one or more `.ui-kbd` keys as a chord
  (`⌘`+`K`) or sequence (`G` then `I`) with a dim connective. The command tier's
  smallest piece, broadly useful outside a palette (menu items, buttons,
  tooltips). Class-only, like `.ui-kbd`.
- **Lifecycle / system state** (`@ponchia/ui/css/state.css`, `.ui-state`
  (+`__label`/`__detail`/`--busy`) with canonical state modifiers
  (saving/saved/queued/offline/stale/conflict/error/locked/reviewed/
  needs-review), `.ui-syncbar`, `ui.state()`): an opt-in, CSS-only vocabulary for
  the states apps actually live in — a labelled state object with a rationed tone
  and a page/document sync bar. The label is the state (never colour alone);
  `--busy` pulses the indicator (reduced-motion-safe). Bronto ships the visual
  states + canonical wording; the host owns the state machine, retry, and
  persistence. Frontier candidate #2. Documented in [`docs/state.md`](docs/state.md).
- **Generated content & AI trust** (`@ponchia/ui/css/generated.css`,
  `.ui-generated`/`.ui-origin-label`/`.ui-reasoning`/`.ui-tool-log`/`.ui-tool-call`,
  `ui.originLabel()`): an opt-in, CSS-only set of **trust surfaces** for AI /
  system-generated content — a marked region, an origin label, and quiet
  native-`<details>` reasoning + tool-call logs. Not a chat kit; no
  fabricated-confidence widget. Bronto styles disclosure/origin/trace, the host
  owns model metadata, redaction, and safety. Pairs with the source layer.
  Documented in [`docs/generated.md`](docs/generated.md).
- **Workbench** (`@ponchia/ui/css/workbench.css`, `.ui-inspector`/`.ui-property`/
  `.ui-selectionbar`): an opt-in, CSS-only core for tool UIs — a selected-object
  inspector panel, denser property rows, and a raised selection action bar.
  Layout + affordances only; resizable split panes and drag handles are
  deferred. Documented in [`docs/workbench.md`](docs/workbench.md).
- **Command palette** (`@ponchia/ui/css/command.css`, `.ui-command` (+
  `__input`/`__list`/`__group`/`__item`/`__shortcut`/`__meta`/`__empty`),
  `initCommand`, `useCommand`): an opt-in CSS shell + behavior — filter +
  keyboard-navigate a DOM-authored command list (roving focus, group hiding,
  full keyboard), emitting `bronto:command:select` ({ value, label }) and
  `bronto:command:close`. Bronto navigates; the host owns the action registry,
  routing, and execution. No global Cmd/Ctrl+K. Completes the command tier
  (frontier #3) atop the shipped `ui-shortcut`. Documented in
  [`docs/command.md`](docs/command.md).
- **Label declutter** (`@ponchia/ui/annotations` `declutterLabels`): a
  deterministic, order-preserving **1-D** label de-overlap helper (sort, push
  apart by `size + gap`, slide to fit `max`) — pure, no DOM/scales. Not a 2-D
  collision solver. Documented in [`docs/annotations.md`](docs/annotations.md).
- **Direct labels** (`@ponchia/ui/annotations` `directLabels`): the
  direct-labeling companion to `declutterLabels` — it declutters labels along an
  axis **and** draws the leader from each anchor to its placed label, reusing the
  connectors geometry kernel. Returns `[{ x, y, anchor, key, d }]` (the `d` feeds
  a `ui-annotation__connector`). Deterministic and pure: no scales, no DOM, no
  2-D placement (the 1-D core of Labella, completed with leaders). Documented in
  [`docs/annotations.md`](docs/annotations.md).
- **Connectors** (`@ponchia/ui/connectors`, `@ponchia/ui/css/connectors.css`,
  `initConnectors`, `useConnectors`, `ui.connector()`) and **Spotlight**
  (`css/spotlight.css`, `initSpotlight`, `ui.spotlight()`) — leader lines between
  DOM elements and a guided-focus overlay; both opt-in, geometry/visual only
  (the host owns layout/tour state).
- **Crosshair / readout** (`css/crosshair.css`, `initCrosshair`,
  `ui.crosshair()`) and **selection states** (`css/selection.css`, `ui.sel()`) —
  a plot ruler that reports pointer position (not data), and a cross-cutting
  `.ui-sel--on/off/maybe` emphasis vocabulary (the host owns brush/hit-test).
- **`@ponchia/ui/css/analytical.css`** — a convenience roll-up that bundles the
  seven analytical leaves (annotations, legend, marks, connectors, spotlight,
  crosshair, selection) into one import. Add `dataviz.css`/`report.css`
  separately as needed.

### Fixed

- The optional Qwik binding (`@ponchia/ui/qwik`) is now built from the packed
  tarball in CI **and** release, alongside React/Solid — closing a coverage gap
  (it was documented as optimizer-proven but no job actually built it). Also
  covered by `check:pack`, the size report, and the dead-code config.

### Changed

- **Breaking (opt-in report kit):** the chart data key moved out of
  `css/report.css` into the standalone `css/legend.css`. `.ui-chart__legend` →
  `.ui-legend` (now with `.ui-legend__item`/`.ui-legend__label` rows) and
  `.ui-chart__swatch` → `.ui-legend__swatch`. Import `@ponchia/ui/css/legend.css`
  beside the report kit; see [`MIGRATIONS.json`](MIGRATIONS.json) and
  [`docs/legends.md`](docs/legends.md). The `--chart-color`/`--chart-pattern`
  swatch contract is unchanged, so the rename is mechanical.
- **Breaking (opt-in marks):** the rationed-accent **tone** on `.ui-mark` and
  `.ui-bracket-note` was renamed `evidence` → `accent` (`ui-mark--evidence` →
  `ui-mark--accent`, `ui-bracket-note--evidence` → `ui-bracket-note--accent`;
  `ui.mark({ tone: 'accent' })` / `ui.bracketNote({ tone: 'accent' })`) so the
  accent tone reads the same across every analytical primitive (it already was
  `accent` on `ui.connector`/`ui.annotation`). `.ui-annotation--evidence` is
  **unchanged** — it is a marker _variant_ (a proof/source shape), not a tone.
  Mechanical whole-token rename; see [`MIGRATIONS.json`](MIGRATIONS.json).
- **Consolidation:** the SVG geometry is single-sourced in the `connectors`
  kernel — `@ponchia/ui/annotations` now builds its connectors on it, so a
  line/curve/arrow/dot is drawn one way across both. `connectorLine`/`Curve`/
  `EndDot` output is byte-identical; **`connectorEndArrow` is the one
  (minor-breaking) shape change** — the arrowhead now matches the connectors
  arrowhead. New `check:helpers-dts` gate keeps the hand-maintained
  `annotations`/`connectors` `.d.ts` in parity with their runtime exports.
- The Doto webfont now ships as **woff2 only** (Brotli) instead of uncompressed
  TTF: ~5.7 kB per weight vs ~137 kB, cutting the six-weight payload from ~823 kB
  to ~35 kB (the dot-matrix glyphs compress ~96%) and shrinking the unpacked
  tarball by roughly the same. No TTF fallback is carried — woff2 is supported by
  the entire browser floor (ADR-0002: Chrome 125 / Safari 18 / Firefox 129).
  `@font-face` is internal, so this is transparent to consumers; only self-hosts
  that referenced `fonts/doto-*.ttf` directly need to point at `*.woff2`.
- `docs/architecture.md` now ships in the package, so the offline rationale the
  shipped ADRs link to resolves inside the tarball.
- `docs/stability.md` clarifies that `data-surface`/`data-density`/
  `data-contrast` are **convenience presets**, not part of the stability
  contract; `data-theme` (light/dark) remains the contractual base.

### Internal

- Token values are single-sourced in `tokens/index.js` (`cssVars`); the
  `css/tokens.css` palette is generated from it, so the dark palette is authored
  once instead of in three places (the shipped CSS is byte-identical).
- `behaviors/index.js` is split into per-behavior modules behind the same public
  barrel (no surface change).
- Drift-gate consolidation (`assertFresh`), a Qwik type smoke + stronger
  class-recipe wiring test, the APCA advisory widened to the accent text across
  the core palette and every colorway (still advisory; WCAG 2.1 AA stays the
  hard gate), an OLED computed-style smoke test, and several doc reconciliations.
- New `demos.spec` e2e sweep runs the console-error / uncaught-exception /
  failed-response guards **and** an axe scan over every per-feature demo page
  (annotations, legends, marks, connectors, spotlight, crosshair, selection,
  report) in both themes and cross-browser — previously only `/demo/` was
  guarded, so a throw or 404 on those SVG-heavy pages could not fail CI.
- The `check:dist` payload ceiling was raised to 80 kB raw / 14.5 kB gzip (from
  78 kB / 13.5 kB). The default bundle was sitting ~21 bytes under the old gzip
  gate — the analytical primitives are opt-in leaves and stay out of it, so this
  is residual prior growth; the bump restores a real ~3% raw / ~7% gzip margin
  so an ordinary token addition no longer trips an unrelated PR.

## 0.4.1 — 2026-06-01

Patch hardening for the public framework surface, plus the first step of the
modern-platform motion direction (see [ADR-0002](docs/adr/0002-scope-and-2026-baseline.md)).

### Added

- **Static report kit — `@ponchia/ui/css/report.css` + `docs/reporting.md`.**
  An opt-in, PDF-first report layer for LLM-authored and hand-authored HTML:
  report covers, headers, section numbering, summaries, findings, evidence
  blocks, source/appendix/footnote blocks, chart wrappers/legends/fallback
  tables, and print utilities (`ui-print-only`, `ui-screen-only`,
  `ui-break-before`, `ui-break-after`, `ui-keep`, `ui-print-exact`). It stays
  out of the default bundle, ships with the offline LLM docs, and is covered by
  a report fixture, package/export checks, and class-contract validation.
  The layer also includes compact covers, unnumbered report sections, simple
  static chart-bar primitives, and evidence-table framing rules so generated
  reports need less private CSS.
- **Zero-JS enter _and_ exit motion for native-`<dialog>` overlays.** Modal and
  drawer (and their backdrop) now fade/scale **both ways** via `@starting-style`
  + `transition-behavior: allow-discrete` — previously they only animated in and
  vanished on close. Pure CSS, reduced-motion-aware (snaps with no flash), scoped
  to `dialog.ui-modal` so the controlled `.is-open` path is unchanged.
- **Enter/exit motion extended to popover, toast, and accordion** (ADR-0002
  "next, same approach"):
  - **Popover** (`.ui-popover`) fades + slides both ways via the same
    `@starting-style` + `allow-discrete` recipe, covering both the native
    `[popover]` top-layer path and the `.is-open` fallback. Zero JS,
    reduced-motion-aware.
  - **Toast** (`.ui-toast`) now plays a CSS fade-out on dismiss instead of being
    yanked from the DOM. The `toast()` behavior adds `.is-leaving` and removes
    the node on `transitionend` (with a timeout fallback); it falls back to
    instant removal under reduced-motion or where no transition is computed, so
    the persistent `aria-live` region is undisturbed.
  - **Accordion** (`.ui-accordion`, native `<details>`) animates auto-height
    open/close via `::details-content` + `interpolate-size: allow-keywords` +
    `content-visibility … allow-discrete`. Strict progressive enhancement —
    gated on `@supports selector(::details-content)`; engines without it (today,
    Firefox/Safari) simply snap, exactly as before.
- **Scroll-driven motion (progressive enhancement).** `.ui-scroll-progress` (a
  reading-progress bar on a `scroll(root block)` timeline, RTL-aware) and
  `.ui-scroll-reveal` (a JS-free, IntersectionObserver-free reveal on a `view()`
  timeline). Both are gated on `@supports (animation-timeline: …)` and
  `prefers-reduced-motion: no-preference`, so engines without scroll timelines
  (today, Firefox/Safari) keep a static end-state and reduced-motion users get
  no movement.
- **View Transitions (progressive enhancement).** A `.ui-vt` helper
  (`view-transition-name: var(--ui-vt-name)`) to morph an element across a
  same-document `startViewTransition()` or a cross-document navigation, an
  on-brand default for the `::view-transition-*(root)` cross-fade, and a
  **reduced-motion kill-switch** for the `::view-transition-*` pseudo-tree
  (which the platform does *not* quiet automatically). Cross-document nav stays
  a documented one-liner you add yourself (`@view-transition { navigation: auto }`
  is document-global, so it can't be layered or scoped by the framework).
- **Optional Qwik bindings — `@ponchia/ui/qwik`.** Same thin-adapter shape as
  the React/Solid bindings (`useDialog`, `useToast`, … `useBrontoBehavior`, plus
  the `cls`/`ui`/`cx` + `applyStoredTheme` re-exports), wrapping the SSR-safe
  behaviors in Qwik's `useVisibleTask$` (run on visible, cleanup on dispose) so a
  resumable page stays zero-JS until interaction. Scope a behavior with a Qwik
  signal: `useDialog({ root: useSignal() })`. `@builder.io/qwik` is an **optional**
  peer dependency, so the core stays zero-dependency. New `examples/qwik-vite`
  builds it through the real Qwik optimizer.
- **OLED true-black surface variant — `data-surface="oled"`.** The dark base is
  now a readable elevated near-black (see Changed); this opt-in root attribute
  restores pure black for OLED power-saving and the original "Nothing" look.
  CSS-only preset (like `data-density`/`data-contrast`), scoped to the dark
  theme. Documented in `docs/theming.md`.
- **APCA advisory for dark text.** `check:contrast` now emits a non-failing
  warning when a dark text pairing falls below its perceptual APCA target (WCAG
  stays the hard gate) — the early-warning that would have caught the illegible
  dim text. The kitchen-sink demo gains a unified theme picker (theme × colorway
  × surface, all persisted).
- **[ADR-0003](docs/adr/0003-theme-model.md)** records the theme model: a binary
  light/dark base × one-knob derivation × orthogonal axes (colorway, surface,
  contrast, density), and why a flat named-theme catalog is rejected.
- React and Solid Vite examples, CI/release matrix coverage for those examples,
  runtime binding tests, public API stability docs, a release runbook, and
  `npm run size:report`.

### Changed

- **Dark theme re-tuned for readability.** The dark base moved off pure `#000`
  to an elevated near-black (`--bg #121212`, panels `#1c1c1c`/`#222`/`#242424`,
  lines `#383838`/`#555`); body text eased `#f2f2f2 → #e6e6e6` (APCA Lc 99 → ~91,
  removing halation) and **dim/meta text raised `#858585 → #a0a0a0`** (APCA
  Lc ~36 → ~49 — the actual "hard to read" fix). WCAG 2.x over-rates contrast on
  pure black, so pairings "passed" while reading poorly; the re-tune clears WCAG
  AA on every pairing and lifts perceptual (APCA) contrast. Accent and status
  colours are unchanged; true black stays available via `data-surface="oled"`.
- **Browser floor raised to Chrome/Edge 125+, Safari 18+, Firefox 129+**
  (early–mid 2025). A deliberate greenfield stance (ADR-0002) so the framework
  can build natively on `@starting-style`, `transition-behavior: allow-discrete`,
  `oklch()`/relative color, and `light-dark()`. No fallbacks ship below the
  floor; not-yet-cross-engine features (View Transitions, scroll-driven
  animations) are enhancement-only and degrade to a static end-state.
- Bundle budget nudged for the new motion: gzip 13.0 → 13.5 kB (for the dialog
  enter/exit work) and raw 76 → 77 kB (for the popover/toast/accordion motion
  plus the scroll-driven + view-transition CSS). Gzip held at ~13.1 kB — it
  compresses well — so the compressed payload still has headroom.

### Fixed

- React and Solid bindings now resolve scoped roots on mount, so `{ root: ref }`
  and resolver callbacks work after framework refs are assigned. Nullish resolver
  results normalize to default behavior instead of crashing destructuring
  behavior initializers.
- Scoped behavior roots now resolve controlled ids root-first, then
  document-wide. This keeps existing body/portal-mounted dialogs, popovers, and
  disclosure panels working while preventing earlier duplicate ids outside an
  island from shadowing the in-root target.
- `data-bronto-dismiss="<selector>"` ignores malformed selectors instead of
  throwing during event handling.
- The one-node glyph mask path now includes a WebKit-prefixed mask declaration,
  and the OKLCH accent ramp uses an explicit white/black neutral endpoint for
  cross-engine browser parity.

## 0.4.0 — 2026-05-31

The color-system release — [ADR-0001](docs/adr/0001-color-system.md) steps 1–8.
A governed evolution beyond pure monochrome: the tier model is written down and
**enforced** (`check:color-policy`), and the "Nothing" look is proven to be a
_skin, not the architecture_ — opt-in **colorways** (amber CRT · phosphor green ·
e-ink), a **data-viz palette** for dashboards (colourblind-safe, gated under
simulated protan/deutan/tritan vision), Tier-3 dot-matrix display tokens, OKLCH
authoring, and an APCA advisory contrast track all ship. Plus a reframed README
+ a rendered docs site, and the curated CHANGELOG is now the GitHub Release body.

**The default build is unchanged** — the red accent, every retained token name,
and both theme palettes render identically; colorways and data-viz are opt-in
entrypoints, never in the default bundle. The only breaking change is the
removal of the orphan `--orange` token (undocumented, unused) — see below.

**BREAKING (orphan token removed)** — **`--orange` / `--orange-soft`** are
removed. They were defined in every token mirror (`css/tokens.css`,
`tokens/index.js`, `tokens.dtcg.json`, `resolved.json`, `index.d.ts`,
`reference.md`) but **referenced by no component and documented nowhere**, and
untiered under the new color model. As a provably-unreferenced token this is
removed in a single minor under the CONTRIBUTING.md deprecation-policy
exception (no working call-site to wind down). Removed rather than adopted; if
categorical color lands later it ships as a governed, opt-in data-viz module
(ADR-0001 tier 4), not a stray top-level token. _Migration:_ if you referenced
`--orange`/`--orange-soft`, define it yourself in a consumer override.

### Added

- **The `--accent-1..6` ramp is now perceptually even (OKLCH).** Steps 1–4 mix
  the accent `in oklch` instead of using the old sRGB ramp (ADR-0001 step 8), so
  the ramp reads as evenly-spaced. `scripts/gen-resolved.mjs` learned to resolve
  `color-mix(in oklch,…)` → hex with the same one-channel tolerance browsers
  show, so `tokens/resolved.json`, the DTCG export, and `docs/reference.md` all
  carry the new values. These are
  token **values** (non-contractual under the 0.x policy) and the ramp is not
  consumed by any shipped component, so there is **no change to any component's
  rendering** — only consumers using `var(--accent-1..4)` directly see the
  (intended) shift.
- **Framework bindings — `@ponchia/ui/react` + `@ponchia/ui/solid`.** Optional,
  thin hooks over the SSR-safe `init*` behaviors (run on mount, clean up on
  unmount/dispose): `useDialog`, `useTabs`, `useMenu`, `useCombobox`,
  `usePopover`, `useDisclosure`, `useFormValidation`, `useTableSort`,
  `useCarousel`, `useDismissible`, `useThemeToggle`, `useDotGlyph`,
  `useToast()`, and the generic `useBrontoBehavior(init, opts)`; `cls`/`ui`/`cx`
  re-exported for one-import DX. `react` / `solid-js` are **optional peer
  dependencies** — the core stays zero-runtime-dependency. Thin adapters over
  the canonical CSS/behaviors layer, not a component library (architecture ADR).
- **Glyphs: a one-node icon-at-scale render path + a `.ui-icon` wrapper + 5
  circle-family glyphs.** `renderGlyph(name, { render: 'mask', size })` now
  returns a **single** `.ui-icon` element masked by the glyph bitmap (one DOM
  node instead of 256 cells) — for an icon in every table row. It scales with
  the text (`--icon-size`, default `1em`) and inherits `currentColor`. New
  `.ui-icon` CSS primitive (`cls.icon`) drives it. Vocabulary grows to 48 with
  `circle`, `check-circle`, `x-circle`, `plus-circle`, `minus-circle`.
- **Data-viz colour module — `@ponchia/ui/css/dataviz.css` + `@ponchia/ui/charts.json`.**
  An opt-in Tier-4 chart palette for dashboards (ADR-0001 step 7). **Hybrid
  accent-led**: series 1 is the live `var(--accent)` (the brand stays series 1);
  series 2–8 are the Okabe-Ito colourblind-safe set; plus a sequential ramp
  (heatmaps) and a diverging ramp (gains/losses). **Colour is never the sole
  signal** — each series ships a matching `--chart-pattern-*` dot-matrix fill
  (WCAG 1.4.1). The categorical palette is **gated for distinguishability under
  normal + simulated protan/deutan/tritan vision** (`check:charts`, OKLab ΔE),
  not just eyeballed. Resolved hex per theme in `tokens/charts.json` for
  canvas/SVG/charting libs; typed `ChartTokenName` (`./charts`). **Charts-only,
  never UI chrome** (`check:color-policy` fails on `var(--chart-*)` in core CSS)
  and **never in the default bundle** (separate entrypoint). Sourced from
  `tokens/charts.js` (generated → drift-checked by `check:charts`).
- **Display colorways — `@ponchia/ui/css/skins.css`.** Opt-in
  `data-bronto-skin="amber-crt | phosphor-green | e-ink"`, a **root-level**
  choice like `data-theme` (apply on `<html>`). Each re-points the one accent
  to a different single hue — the derived accent family, focus ring, dot-matrix
  and glyphs follow automatically; status colours and the neutral canvas are
  untouched. Authored in **OKLCH**, per-theme (a dark + light accent, like the
  core red), and **every skin accent is contrast-gated** to the same WCAG AA /
  3:1 floors as the core (see `docs/contrast.md`). Shipped as a **separate
  entrypoint, never in the default `dist/bronto.css`** — zero cost unless you
  import it. Sourced from `tokens/skins.js` (generated → drift-checked by
  `check:skins`).
- **Tier-3 dot-matrix "display expression" tokens** (`css/dots.css`):
  `--dotmatrix-glow` (phosphor bloom on lit cells), `--dotmatrix-pulse-min`
  (the `--pulse` floor), documented `--dotmatrix-reveal-step` (scan cadence).
  All default to a **no-op**, so the default render is unchanged; the phosphor
  colorways use the glow in dark.
- **APCA advisory contrast** in `docs/contrast.md`. Every pairing (core **and**
  every colorway) now shows an APCA-W3 `Lc` column beside the WCAG ratio — a
  perceptual cross-check. **Advisory only**: WCAG 2.1 AA stays the hard gate
  (`check:contrast`). Implemented dependency-free, alongside an OKLCH→sRGB
  converter so OKLCH-authored values can be measured.
- **`check:color-policy` gate** (`scripts/check-color-policy.mjs`, wired into
  `npm run check`). Enforces the color constitution: every color-defining token
  (across `global`/`light`/`dark`) must be classified into a tier (this is what
  would have caught `--orange`); the `--chart-*` / `--cat-*` / `--data-*`
  data-viz namespace is reserved; and component CSS may not use raw chromatic
  color (only tiered tokens, with neutral grays / `color-mix()` endpoints).
- **`check:skins` gate** — `css/skins.css` can't drift from `tokens/skins.js`,
  every skin defines `--accent`, and colorways stay out of the default bundle.
- **[ADR-0001 — Color system](docs/adr/0001-color-system.md)** — the five-tier
  color constitution and the backward-compatible roadmap. Steps 1–8 are
  implemented in this release: gate + colorways + Tier-3 tokens + OKLCH for new
  work + APCA advisory + data-viz + OKLCH core accent ramp.

## 0.3.6 — 2026-05-31

Display glyphs: a small `@ponchia/ui/glyphs` subpath of dot-matrix bitmaps
rendered on the existing `.ui-dotmatrix` primitive — no SVG, no icon font,
re-skinned by the same `--field-dot*` tokens. All additive — a new optional
JS subpath plus one backward-compatible CSS var → a patch under the 0.x
policy.

### Added

- **`@ponchia/ui/glyphs` — a 43-glyph dot-matrix display-icon set.** A frozen
  16×16 bitmap registry (`GLYPHS`, `GLYPH_NAMES`, `GLYPH_SIZE`) with `glyph()`,
  `glyphCells()`, and `renderGlyph(name, { label, grid, solid, anim, dot, gap })`
  — an SSR-safe HTML string, decorative (`aria-hidden`) by default and
  `role="img"` when labelled. Covers navigation (`arrow-*`, `chevron-*`),
  actions (`check`, `close`, `plus`, `minus`, `search`, `menu`, `gear`,
  `edit`, `trash`, `download`, `upload`, `link`, `refresh`), media (`play`,
  `pause`), state (`eye`, `eye-off`), theming (`sun`, `moon`), objects
  (`mail`, `file`, `folder`, `clock`, `home`, `user`, `heart`, `star`,
  `bell`, `lock`, `info`, `warning`), layout (`grid`, `more-vertical`,
  `more-horizontal`) and `spark` (the two-tone accent demo). Three knobs from
  one source: the default dot look for **display** sizes; **`solid: true`**
  (or `data-bronto-glyph-solid`) which fuses the cells into a square, gapless
  pixel glyph legible as an **inline icon down to ~16px**; and opt-in
  **`anim`** (`reveal` powers the cells on in a scan, `pulse` makes the glyph
  breathe) — decorative only, disabled under `prefers-reduced-motion`, with
  the meaning kept in the static frame + label. `renderGlyph` returns a
  `<span>` (valid inline / inside a `<button>`) and accepts any string for
  dynamic dispatch (`GlyphNameInput = GlyphName | (string & {})`) — unknown
  names hit the documented `''`/`[]`/`undefined` fallback — while the
  `GlyphName` union itself stays strict (typos in annotations are errors). The
  union is generated and CI-drift-checked from the runtime, like the
  `cls`/token maps.
- **`initDotGlyph()` behavior.** Expands `[data-bronto-glyph]` placeholders
  into a `.ui-dotmatrix` grid in place (optional `data-bronto-glyph-label`),
  idempotent, with a cleanup that fully reverts — the DOM counterpart to
  `renderGlyph`.

### Changed

- **`.ui-dotmatrix` gains `--dotmatrix-dot` and `--dotmatrix-dot-radius`
  knobs** — the former for intrinsic dot sizing (`grid-template-columns`
  falls back to the previous `minmax(0, 1fr)` when unset), the latter to
  square off the cells (`--dotmatrix-dot-radius: 0`) for the solid pixel-glyph
  look. Both default to the prior behaviour, so existing matrices are
  unchanged. Adds opt-in `ui-dotmatrix--reveal` / `ui-dotmatrix--pulse`
  animation modifiers (`cls.dotmatrixReveal` / `cls.dotmatrixPulse`), both
  reduced-motion-aware; `--dotmatrix-reveal-step` tunes the reveal scan speed
  (per-cell delay, default `3ms`).

## 0.3.5 — 2026-05-29

A consumer-evidence pass: six small primitives that adopters were
repeatedly hand-rolling, added as pure CSS in
`@layer bronto` reusing existing tokens. All additive — no new token, no
new gated contrast pairing, no breaking change → a patch under the 0.x
policy.

### Added

- **`ui-pagehead` — the in-page title bar.** `ui-pagehead` +
  `__title` + `__actions`: eyebrow/breadcrumb + title on the start,
  an actions cluster on the end, one hairline beneath. Composes the
  existing `ui-eyebrow` / `ui-breadcrumb` rather than duplicating them.
  Parts-only (no recipe), like `ui-panel`.
- **`ui-steps` — a stepper for multi-step flows.** `ui-steps` (an `<ol>`)
  + `__item` + `__item--done`, auto-numbered by CSS counter with hairline
  connectors. State is ARIA-driven: the active step is `aria-current="step"`
  (no class); completed steps take `--done`.
- **`ui-timeline` — a vertical event list.** `ui-timeline` (an `<ol>`) +
  `__item` + `__time`, on a hairline spine with a per-item marker dot;
  `aria-current` marks the live event (accent marker).
- **`ui-meter` — a labelled proportion / threshold bar.** `ui-meter` +
  `__fill` + tone modifiers `--accent` / `--success` / `--warning` /
  `--danger`, with a `ui.meter({ tone })` recipe + `MeterOpts`. Distinct
  from `ui-progress` (task progress, can be indeterminate): a meter shows
  a measured static value (coverage, capacity, a KPI). Width is the same
  shared `--value` knob progress uses; author `role="meter"` +
  `aria-valuenow/min/max`.
- **`ui-kbd` — an inline keyboard-key glyph.** Wrap a `<kbd>`; one per key
  for a shortcut.
- **`ui-input-icon` — a leading/trailing icon *inside* a control.**
  `ui-input-icon` + `__icon` + `--end`, with a `ui.inputIcon({ end })`
  recipe + `InputIconOpts`. Distinct from `ui-input-group` (an *adjacent*
  affix): the icon overlays inside one `.ui-input`, which keeps full width
  and gains padding on the icon side. Closes the "no framework primitive"
  gap consumers were filling with a bespoke absolute overlay.
- **`ui-carousel` + `initCarousel` — an image gallery / carousel, and a
  lightbox built on it.** `ui-carousel` (`__stage` / `__viewport` /
  `__slide` / `__prev` / `__next` / `__thumbs` / `__thumb` / `__status`)
  is a **scroll-snap** track, so touch + trackpad swipe (with momentum)
  are the browser's, not hand-rolled. `initCarousel` adds prev/next,
  keyboard (Arrow/Home/End), a thumbnail strip with `aria-current` sync,
  the position counter, and carousel ARIA, keeping a JS index in sync with
  the scroll position both ways (`IntersectionObserver` where available);
  `data-bronto-carousel-loop` wraps; it emits `bronto:change`
  (`{ index }`). A **full-screen lightbox** is the same markup inside a
  native `<dialog class="ui-lightbox">` (+ `ui-lightbox__close`) opened by
  the existing `initDialog` — so the top layer, focus-trap, Escape and
  focus-return come from `<dialog>` with zero new focus code. Closes the
  gallery/lightbox/carousel gap consumers were filling by hand (one
  adopter had shipped three overlapping hand-rolled versions). SSR-safe,
  idempotent, returns a cleanup; declared in `behaviors/index.d.ts` and
  demoed.
- Demo gains a **"0.3.5 additions"** kitchen-sink section; `docs/usage.md`
  gains meter-vs-progress and steps/timeline/kbd/input-icon guidance; the
  generated `docs/reference.md`, `classes/index.d.ts`,
  `classes/vscode.css-custom-data.json` and `dist/*` pick the new surface
  up automatically.

### Changed

- **Bundle gzip budget 12 kB → 13 kB** (raw cap unchanged at 76 kB). The
  six new primitives plus the carousel/lightbox grew `dist/bronto.css` to
  ~73 kB raw / ~12.7 kB gzip (against the 76 kB / 13 kB caps); the gzip cap
  is recalibrated per the bump-deliberately rule in
  `scripts/build-dist.mjs`.

### Packaging & docs

- **`fonts/OFL.txt`** — the bundled Doto font is now shipped with its SIL
  Open Font License 1.1 text and attribution (© 2024 The Doto Project
  Authors), as required to redistribute it; README gained a font-license
  note. No code change.
- **README rewritten** for the npm package page (de-duplicated, accurate
  install/quick-start/theming, absolute links); `package.json` gained a
  compelling `description` + `keywords`; `ROADMAP.md` reconciled to 0.3.5.
- **Visual regression reworked** to component-scoped per-section snapshots
  (`data-shot` slugs, auto-discovered) so adding a primitive no longer
  drifts every baseline; the `visual-baselines` workflow's change-gate now
  detects brand-new (untracked) baselines.

## 0.3.4 — 2026-05-17

A review-driven accessibility + adoption pass (three external reviews →
independent Opus review → AgentMix deep multi-POV review). All additive:
new gated artifact, two new shipped docs, one new token. No breaking
change → a patch under the 0.x policy.

### Added

- **`docs/contrast.md` — a published, CI-gated WCAG 2.1 contrast
  matrix.** Every contractual token pairing, the conformance level it is
  *held to*, and its measured sRGB ratio per theme. Generated from the
  resolved token model (`scripts/gen-contrast.mjs`) so it cannot drift
  from the palette, and **gated**: the new `check:contrast` fails
  `npm run check` (and so release) if any gated pairing drops below its
  floor. Text pairings are held to AA 4.5:1; non-text UI to 3:1;
  decorative hairlines are reported but WCAG-1.4.11-exempt by design
  (the low ratio is published, not hidden). New `exports`/`files`
  entry; indexed in `llms.txt` + README.
- **`docs/usage.md` — the decision guide.** When to use which primitive
  (badge vs chip vs status dot), the unset density default and its two
  presets, prose-in-card, when to reach for a behavior. Hand-written and
  stable (contract, like `theming.md`); ships in the tarball for offline
  agents/consumers.
- **`info` status tone — token wired through the full status family.**
  New `--info` / `--info-soft` tokens (+ `--bronto-color-info` semantic
  alias), dual-theme, **and** the consumers that make it real:
  `ui-badge--info`, `ui-alert--info`, `ui-toast--info`, `ui-dot--info`,
  with `ui.badge`/`ui.alert`/`ui.dot` recipes, `BadgeOpts`/`DotOpts`/
  `Tone` types, the toast `ToastOpts` tone, the generated reference and
  the demo. This deliberately reverses the 0.3.2 `ui-badge--info`
  deferral: shipping a token no component consumes would be the exact
  dead-token defect this release exists to remove. Its gated contrast
  row is now a real indicator (measured ≥3:1 on surface — light 5.77:1,
  dark 8.41:1). Status hues (`success`/`warning`/`danger`/`info`) are
  outside the rationed-accent rule by design.
- **`docs/theming.md` — a full re-skin recipe.** Demonstrates that the
  "Nothing" identity is a token skin, not the architecture: a per-theme
  override block (measured AA-passing accents) restyles the whole system
  with no fork.

### Changed

- `.ui-dotbar i` radius `1px` → `var(--radius-sm)` (byte-identical
  render — `--radius-sm` *is* `1px` — but now responds to a radius
  re-skin; no visual-baseline drift).
- **`--dot-font` is now a live knob.** It and `--display` were defined
  identically and nothing consumed `--dot-font`, yet README / Astro
  guide / `fonts.css` all advertised it as a self-host/re-skin override
  (a documented dead token — the exact defect this release exists to
  remove). `--display` now derives from `--dot-font`
  (`--display: var(--dot-font)`): byte-identical render today, but
  overriding `--dot-font` as documented finally works. Removing the
  token instead would have been breaking (names are contractual), so
  the patch-safe fix is to make the advertised knob real.
- Contrast gate now also covers text on `--surface-muted`
  (`--text-soft`, `--text-dim`) — that surface is rendered as a fill by
  forms/disclosure and `--text-dim` there is the tightest real margin
  (~4.7:1). Passing today; now gated so a future palette nudge can't
  silently regress it. (Found by the full-codebase audit.)
- ROADMAP reconciled against shipped reality: the entire original 0.3.1
  checklist has been delivered for several releases. CHANGELOG is now
  the stated source of truth.

### Fixed

- `scripts/gen-contrast.mjs` colour parser now rejects non-finite
  channels and handles percent alpha, so a `NaN` ratio can no longer
  silently pass the gate (`check:contrast` also fails explicitly on a
  non-finite ratio). Found by the AgentMix review.
- `demo/theme-playground.html` WCAG linearisation threshold aligned to
  the canonical `0.04045` (was `0.03928`); points at the gated
  implementation as the source of truth.
- `check-classes` now strips CSS comments before scraping selectors. It
  is the one contract check that scrapes CSS rather than diffing a
  generator, so a `.ui-*` named only inside a comment could previously
  satisfy the `cls`⇄selector contract — a real (if currently
  unexercised) drift hole. Closed by the full-codebase audit.
- **`ToastOpts` was runtime-public but type-private.** `toast()` has
  always accepted (and tested) `assertive` / `closable`, but the typed
  `ToastOpts` stopped at `tone`/`title`/`duration`, so a TypeScript
  consumer couldn't pass documented options. Added both (+ a type
  test). `check:dts` covers only the *generated* classes/tokens `.d.ts`,
  not the hand-written behaviors one — this drift was invisible to the
  gate.
- **`initFormValidation` suppressed native bubbles too late.**
  `form.noValidate` was set inside the submit/blur handlers, so the
  *first* invalid real-browser submit of a pre-existing form showed the
  UA bubble instead of the Bronto error summary (the demo masked it
  with authored `novalidate`). It is now set at init for matched forms
  and restored on cleanup; dynamically-added forms stay covered by the
  in-handler set.
- **Combobox could select a filtered-out option.** Typing a query that
  hid the active option left `aria-activedescendant` stale; Enter then
  selected the hidden option. `filter()` now clears stale active state
  and Enter is visibility-guarded (WAI-ARIA APG correctness).
- `check-dist` now also asserts the on-disk `dist/**/*.css` set exactly
  matches the generated bundles — since the whole `dist/` ships, a
  stale/renamed leaf would otherwise reach npm undetected.
- README bundle-size figures refreshed (~64 kB raw / ~11 kB gzip; the
  enforced ceiling remains `check-dist`'s budget). Prior `~54/~10`
  prose had drifted since 0.3.2.

## 0.3.3 — 2026-05-16

A second consumer-evidence pass ("felt it twice") plus
agent-discoverability. All additive — new classes/recipes, a new
generated artifact, a widened input type, a freed primitive with a
permanent alias. No breaking change → a patch under the 0.x policy.

### Added

- **`@ponchia/ui/tokens/resolved.json`** — every colour token resolved
  to a static `#rrggbb` / `rgba(...)` per theme, with `var()` and sRGB
  `color-mix()` evaluated at build time. For render targets that cannot
  read CSS custom properties: MapLibre GPU paint, `<canvas>`, WebGL,
  SVG, server-side image gen. Generated + drift-checked (new
  `check:resolved` gate); no new colours introduced (`check:shiki`
  unaffected). Resolves the deferred charts-palette gap now that a 2nd,
  non-charting consumer has hit it.
- **`ui-button--sm` / `ui-button--lg`** — token-driven size scale for
  dense tooling (toolbars, pagination, table actions) and hero CTAs.
  `ui.button({ size: 'sm' | 'lg' })`. Default size unchanged.
- **`ui-empty-state`** — the empty-state primitive is now
  shell-agnostic; `ui-app-empty-state` remains a permanent grouped alias
  (byte-identical render, no baseline drift). Same playbook as 0.3.2
  `ui-stat`.
- **`ui-app-rail__account`** — a framework-blessed identity / sign-out
  slot, so admin shells stop hand-rolling it; stays visible when the
  rail collapses on mobile.
- **`ui-modal` controlled (`is-open`) path** — a portal/React modal that
  can't be a native `<dialog>` wears the same skin + open layout via
  `is-open` (`ui.modal({ open: true })`). Backdrop and focus-trap remain
  the consumer's responsibility (the `<dialog>` path still gets them
  free).
- **`llms.txt`** at the package root — a self-contained agent entrypoint
  (typed contract, the `@layer bronto` override rule, import surface,
  shipped offline references).
- **`docs/reference.md` and `docs/theming.md` now ship in the npm
  tarball** — an offline coding agent gets the full class catalog and
  token contract from `node_modules/@ponchia/ui/`. The reference also
  now documents the table-local `is-num`/`is-pos`/`is-neg` state classes
  and the ARIA-driven composition/state model. New `exports` subpaths:
  `./llms.txt`, `./docs/reference.md`, `./docs/theming.md`,
  `./tokens/resolved.json`.

### Changed

- **`ClassValue` widened to clsx parity** (`string | number | boolean |
  null | undefined | …`). The idiomatic React `reactNode && 'cls'` guard
  (where the node may be `0` / `''`) now type-checks; the runtime `cx`
  already skipped every falsy value, so this is a non-breaking type
  relaxation.
- `check-pack.mjs` relaxed from a blanket `docs/` ship-block to a
  **curated allowlist** (`docs/reference.md`, `docs/theming.md` only); a
  consistency assertion fails if that set drifts from `package.json`
  `files`. The rest of `docs/` stays dev-only by design — a deliberate,
  documented narrowing of the earlier runtime-only stance.

### Deferred (deliberately not shipped)

- **React/Solid binding layer.** The duplicated form/badge "glue" two
  consumers feel is ARIA-driven by design (`aria-invalid`, `aria-busy`,
  `aria-describedby`), so the agnostic surface is already complete; the
  remaining duplication is the framework-binding layer, still deferred.
  The reference now documents the composition model explicitly.
- **`ui-app-rail__foot` mobile `display:none`.** Whether the foot should
  survive the mobile breakpoint is a visual/possibly-breaking call; the
  new `__account` slot (which does survive) covers the actual need
  without changing existing behaviour.

## 0.3.2 — 2026-05-16

Re-skin-proven adoption pass: a real content-site consumer rebuilt
several idioms bespoke because a primitive was missing or shell-locked.
This promotes the genuinely generic, token-only ones upstream. **All
additions are non-breaking** (additive classes/recipes; the metric tile
keeps its admin-shell name as a permanent alias) — a patch per the 0.x
policy.

### Added

- **`ui-stat` / `ui-statgrid`** — the metric tile is now shell-agnostic
  (label + display value + signed delta). The admin-shell
  `ui-app-metric*` / `ui-app-metrics` names remain as permanent aliases
  grouped on the same rules (byte-identical output, no baseline drift).
- **`ui-link--cta`** — the eyebrow-faced action link (accent · display ·
  uppercase + arrow glyph), composed from the same tokens as
  `ui-eyebrow`. `ui.link({ cta: true })`.
- **`ui-badge--dot`** — leading state dot; composes with any badge tone
  (`is`-tone tints the dot). `ui.badge({ tone, dot: true })`.
- **`ui-eyebrow--sm`** — restores the dense size step (was
  `eyebrow--tight` in 0.2.2). `ui.eyebrow({ sm: true })`.
- **`ui-container--wide`** — documented wide preset (`--container-wide`,
  default 82rem) for app/marketing shells. `ui.container({ wide: true })`.
- **`ui-siteheader--sticky`** — structural sticky only (the floating-card
  skin stays consumer identity, deliberately not shipped).
- **`ui-dotmatrix`** (+`__cell`, `--hot`, `--accent`) — data-bound dot
  grid, the on-brand counterpart to the decorative `ui-dotgrid`; the
  data→cell mapping stays the consumer's.
- **`ui-num`** (+`--pos`, `--neg`, `--muted`) — the tabular / end-aligned
  / P&L-tone numeric vocabulary the table has shipped since 0.1.0, freed
  from `.ui-table` so cards, stats and inline figures share one
  contract. `ui.num({ tone })`. (Two independent admin consumers were
  reinventing `.text-green`/right-align outside a table.)
- **`ui-badge--muted`** — the idle / unknown / "no signal yet" status
  tone, distinct from the default tinted badge. `ui.badge({ tone:
  'muted' })`. Token-safe, no new hue.

### Fixed

- **`ui-card--interactive`** had no keyboard cue (cards typically wrap a
  single link): added `:focus-within` border + a `--shadow-raised` ring
  on hover. a11y completeness fix.

### Changed

- dist raw size budget recalibrated 64 kB → 76 kB (post-0.3.2 bundle is
  ~64.5 kB raw / ~11.1 kB gzip; ~18% raw headroom restored). The gzip
  cap (12 kB) is unchanged — the wire-size contract still holds.

## 0.3.1 — 2026-05-16

Adoption + gap-closing pass driven by a 12-perspective review (two Opus
analyses + two five-model AgentMix deep runs). **All additions are
non-breaking** (additive classes/tokens/behaviors; short token names
kept as permanent aliases) — so this is a patch, per the 0.x policy
(only breaking changes bump the minor); the 0.3.0 legacy removal
stands. Tracked scope: ROADMAP.md.

### Added

- **Components/behaviors** (all SSR-safe, idempotent, cleanup-returning,
  dependency-free): `ui-combobox` + `initCombobox` (WAI-ARIA APG
  combobox); `ui-popover` + `initPopover` (collision-aware, native
  top-layer when available); `initFormValidation` (Constraint
  Validation → `aria-invalid`/`aria-describedby` + error summary);
  `initTableSort` (sortable `aria-sort` headers + row selection,
  `bronto:selectionchange`); `.ui-button[aria-busy]` loading state;
  toast dismiss button + separate assertive region for `danger`.
- **Forms:** `ui-input-group`(+`__addon`), `ui-file`, `ui-range`,
  `ui-error-summary`.
- **Layout:** `ui-sidebar`, `ui-switcher`, `ui-center`, `ui-ratio`;
  opt-in `ui-cq` container queries for `ui-grid` / `ui-app-metrics`.
- **Tokens:** semantic `--bronto-color-*` tier, `--accent-1..6` /
  `--surface-1..6` ramps, `--z-*` stacking scale (every framework
  `z-index` now resolves through it; values unchanged).
- **DX:** generated drift-checked `docs/reference.md`; VS Code
  `classes/vscode.css-custom-data.json` (token IntelliSense, exported
  as `./vscode.css-custom-data.json`); per-framework integration guides
  + Tailwind interop recipe; `examples/{vanilla-vite,astro,sveltekit}`
  + consumer-smoke CI matrix; README badges; `ROADMAP.md`;
  `MIGRATIONS.json` + `docs/migrations/0.2-to-0.3.md`;
  `demo/theme-playground.html` (live contrast checker).
- **a11y:** `role="switch"` contract + forced-colors switch cues;
  `@supports(anchor-name)` tooltip un-clipping.

### Fixed

- **Release hygiene:** the `0.3.0` section was still labelled
  `unreleased` after `v0.3.0` shipped to npm `latest`. Dated it
  correctly and added a `check:release` gate (in `npm run check`) that
  fails when `package.json`'s version maps to an `unreleased` changelog
  heading, so a published version can never again be marked unreleased.

### Changed

- `npm run check` is now 14 gates (+`check:release`, `check:reference`,
  `check:vscode`); `prepack` regenerates the reference + VS Code data.
- CONTRIBUTING.md documents a deprecate-one-minor policy so
  "minor may break" is predictable.

## 0.3.0 — 2026-05-16

### Multi-POV review hardening

A six-perspective review (DX/contract, CSS architecture, a11y,
release/supply-chain, plus AgentMix) drove this pass.

**BREAKING (token / contract level)**

- **New `--accent-text` token** (= `var(--accent-strong)`). Everywhere
  the accent was used as _foreground text_ (links on hover, active
  nav/tab/accordion, prose markers, eyebrows, chips, breadcrumb,
  pagination) now resolves through `--accent-text`, not raw `--accent`,
  so a pale re-brand no longer silently fails text contrast.
  _Migration:_ none for default themes (visually identical). If you
  re-brand `--accent` to a light hue, also set `--accent-text` to a
  dark-enough value (see docs/theming.md).
- **`--focus-ring` is now solid `var(--accent)`** (was an unused
  50%/55% transparent mix) and every focus outline is wired to it.
  Default focus appearance is unchanged; the `[data-contrast=high]` /
  `prefers-contrast` promotion and per-theme `--focus-ring` overrides
  now actually take effect. _Migration:_ none unless you relied on the
  (previously dead) token value.
- **`classes/index.d.ts` / `tokens/index.d.ts` are now generated
  literal types.** `cls` exposes literal keys+values; token views use
  `ColorKey`/`ScaleKey`/`*TokenName` unions; `themeColor` takes
  `ThemeName`. Mistyped keys are now compile errors. _Migration:_ fix
  any code that relied on the old `Record<string,string>` (e.g. reading
  a non-existent key and getting `string` instead of an error). JS
  token keys are kebab-case — `themeColor('dark')['accent-soft']`.

**Fixed**

- **a11y (WCAG AA):** `.ui-chip--accent`, legacy `.eyebrow` group and
  `.tag-list--compact` first child no longer use raw `--accent` as
  small text (was ~3.9:1 in light).
- **a11y:** native `<dialog>` returns focus to its trigger on _every_
  close path (Esc, close button, backdrop light-dismiss, programmatic).
- **a11y:** the toast `aria-live` stack is a persistent region — no
  longer created-then-destroyed per drain, fixing dropped first /
  post-drain screen-reader announcements.
- **a11y:** `.ui-tab:focus-visible` is now visually distinct from the
  active-tab underline (inset ring).
- **DX:** the `.` export is conditional (`style`/`default`) instead of a
  bare `.css` string, so type-aware tooling no longer mis-resolves the
  package root. The root is CSS-only (documented).

**Changed (non-breaking)**

- Behavior initializers (`initThemeToggle`, `dismissible`, `initDialog`,
  `initDisclosure`, `initTabs`) are now idempotent — re-init (HMR,
  framework remount, repeat calls) replaces rather than stacking
  duplicate listeners. Tab ids use a module-global counter so separate
  islands never collide on `bronto-tab-1`.
- New drift gate `check:dts` (generated `.d.ts` ⇄ JS runtime), wired
  into `npm run check` and `prepack`. `docs/architecture.md` drift table
  and release-gating section corrected to the real four-job DAG
  (`validate` + `e2e` → `publish-npm` → `release-notes`).
- README: explicit "do not mix a bundle with a raw leaf import" hazard
  warning; a Versioning section; size/`@import`-depth prose de-drifted.
- Tests: +3 unit tests (dialog focus-return, initializer idempotency,
  global-unique tab ids); +5 e2e a11y tests (RTL axe pass, dialog
  focus-return on Escape, persistent toast live region, disclosure
  toggle, modal computed-contrast instead of a blanket rule disable);
  demo gained a `[data-bronto-disclosure]` instance (was untested).
  _Release note:_ the visual-snapshot baselines (`test/e2e/__screenshots__`)
  are intentionally stale after the contrast / focus-ring / legacy-removal
  changes (cross-OS rasterisation means they can only be authored in the
  pinned container, not on a dev machine). Regenerate them with one click:
  run the **“Update visual baselines”** workflow (`workflow_dispatch`,
  `.github/workflows/visual-baselines.yml`) from this branch — it rebuilds
  them in `mcr.microsoft.com/playwright:v1.60.0-jammy` and commits them
  back, after which the `e2e` gate goes green on its own. Red by design
  until that runs.

**BREAKING (legacy vocabulary removed / migrated)**

The whole non-`ui-*` surface is gone; everything shipped is now under the
`.ui-*` contract and the `check-classes` drift gate.

- **Deleted (had `ui-*` equivalents):** `css/layout.css`, `css/cards.css`,
  `css/typography.css` and their entire vocabulary — `.hero`,
  `.project-*`, `.post-card`, `.essay-*`, `.metric-tile`, `.callout`,
  `.eyebrow`, bare `.button`, `.section-head`, `.tag-list`,
  `.profile-link-list`, `.page-*`, `.home-*`, `.signal-panel`,
  `.worklog-summary`, … _Migration:_ use the `ui-*` content layer —
  `.ui-prose`/`.ui-quote` (long-form), `.ui-card`, `.ui-eyebrow`,
  `.ui-button`, `.ui-tag`/`.ui-tags`, `.ui-grid`/`.ui-stack`, the
  `ui-site*` shell. `.skip-link` → `.ui-skiplink`; `.site-nav` →
  `.ui-sitenav`; `.site-menu` → `.ui-sitemenu` (responsive nav) or the
  new `.ui-menu-host` (a `<details>` + `.ui-menu` dropdown wrapper).
- **Renamed → first-class:** the admin shell `.app-*` → **`.ui-app-*`**
  (`ui-app-shell`/`-rail`/`-topbar`/`-toolbar`/`-nav`/`-panel`/
  `-content`/`-main`/`-metrics`/`-metric`/`-empty-state`, with the same
  `__part` / `--mod` suffixes). The theme toggle `.theme-toggle__*` →
  **`.ui-themetoggle__*`**. _Migration:_ rename these class strings in
  consumer markup (or use the new `cls.app*` / `cls.themetoggle*` /
  `cls.menuHost` entries). They are now typed and drift-checked.
- **Bundle collapsed:** `css/responsive.css` and `css/index.css` removed.
  `ui-*` components own their breakpoints, so there is no core/full
  split: `@ponchia/ui/css` now resolves to `css/core.css` (one bundle),
  `@ponchia/ui` → the single `dist/bronto.css` (~54 kB / ~10 kB gzip,
  was ~70/12). _Removed exports:_ `./css/index.css`,
  `./css/responsive.css`, `./dist/bronto-core.css`, and the deleted
  leaves' `./css/{layout,typography,cards}.css`. _Migration:_ import
  `@ponchia/ui` or `@ponchia/ui/css`.

**BREAKING (per-leaf imports are now layer-safe)**

- Every `@ponchia/ui/css/<leaf>.css` export now resolves to a
  self-`@layer bronto`-wrapped build (`dist/css/<leaf>.css`), so a
  direct leaf import is layered by default and safe to mix with the
  bundle — the silent cascade-inversion footgun is gone. The raw,
  full-specificity source is still available as a deliberate escape
  hatch at the explicit **`@ponchia/ui/css/unlayered/<leaf>.css`**
  path. _Migration:_ none if you import the bundle. If you imported a
  raw leaf *expecting* unlayered/full-specificity behaviour, switch
  that import to the `css/unlayered/*` path; otherwise the now-layered
  leaf is the correct (safe) default. Drift-checked by `check-dist`.

### Tooling (external-review triage)

Adopted what fits the CSS-first / zero-runtime-dep / curated-artifact
ADR; declined what doesn't (recorded so the decision isn't re-litigated).

**Added**

- **TypeScript type gate** — `tsconfig.json` + `test/types.test-d.ts`
  + `check:types` (in `npm run check`). Compiles the published `.d.ts`
  and asserts, via `@ts-expect-error`, that the generated literal
  `cls`/token types reject typos and `themeColor` rejects non-`ThemeName`.
  Completes the review's "auto-generate .d.ts, kill drift" item
  (generation + `check-dts` landed earlier this minor; this proves the
  result). `typescript` is devDep-only — no runtime/types-export change.
- **Prettier** — `.prettierrc` + `check:format`/`format`, in
  `npm run check`. Scoped to hand-authored non-CSS source; CSS stays
  Stylelint-owned, generated artifacts and the curated Markdown/`demo`
  are `.prettierignore`d so formatters never fight generators.
- **GitHub issue/PR templates** — collect `@ponchia/ui` version,
  consuming framework, and surface; PR template carries the
  contract/SemVer/a11y checklist.
- **Bundle-size budget tightened** — `check-dist` `BUDGET` recalibrated
  90 kB→64 kB raw / 16 kB→12 kB gzip (bundle is ~54/~10 post-cleanup),
  so regrowth is gated, not just catastrophic blowouts. (Dependabot was
  already added earlier this minor.)

**Declined (rationale)** — Storybook (heavy React/Vite toolchain vs the
framework-agnostic zero-dep ADR; `demo/index.html` is the self-driving
surface); Style Dictionary as a dependency (the shipped
`tokens.dtcg.json` *is* the deliberate bring-your-own platform interop;
no native consumers exist — consumers run SD themselves);
standard-version/auto-changelog (the curated narrative CHANGELOG is an
asset; commits are already conventional); Renovate (Dependabot chosen);
Lighthouse CI (the axe a11y gate + size budget already cover the
regression vectors).

### Post-review fixes (independent Opus + AgentMix pass on this branch)

- **Fixed (HIGH, regression introduced here):** the persistent-toast
  rAF deferral could resurrect an already-dismissed first toast into the
  `aria-live` region (dismiss within the first frame). Now guarded by a
  `dismissed` flag; `dismiss()` is idempotent. +2 unit tests polyfilling
  rAF (the jsdom env had no `requestAnimationFrame`, so the path was
  previously untested).
- **Fixed (HIGH, regression introduced here):** the layered per-leaf
  `@ponchia/ui/css/fonts.css` (now `dist/css/fonts.css`) referenced
  `url(../fonts/*)`, which from `dist/css/` resolves to the unshipped
  `dist/fonts/`. `build-dist` now rewrites `../fonts/` → `../../fonts/`
  for the deeper per-leaf files (the flattened bundle at `dist/` is
  unaffected and unchanged). `check-dist` now also resolves every
  `url(...)` in each generated file against its own location, so this
  class of depth bug can't recur.
- **Fixed (docs):** README SemVer guidance was wrong — at `0.x` npm
  resolves `^0.3.0` and `~0.3.0` identically (`>=0.3.0 <0.4.0`); both
  hold back the breaking `0.4.0`. Corrected. Removed the stale
  "legacy `site-*`/`.tag-list` kept as back-compat" line (they were
  deleted this release). De-duplicated the `check-dist` paragraph in
  architecture.md.
- **Hardened:** the release `publish-npm` step uses
  `npm publish --ignore-scripts`, so `NODE_AUTH_TOKEN` is never exposed
  to the prepack/prepublishOnly lifecycle; it ships the artifacts already
  byte-verified by `validate` on the same commit.

### Further discovered-issue cleanup

Bounded, sensible items surfaced by the reviews, now closed:

- **a11y:** new `initMenu` behavior — Escape / outside-click /
  close-on-activate (with focus return to `<summary>`) for a native
  `<details data-bronto-menu>` `.ui-menu` dropdown. Deliberately a
  disclosure of buttons, not an over-claimed ARIA menu (review M3).
  Wired in the demo; unit-tested.
- **a11y:** the active tab's selected state is re-asserted under
  `forced-colors: active` (`border/colour: Highlight`) — it was
  invisible in Windows High Contrast (review L3).
- **a11y (demo):** the pagination "previous" control is now a real
  `disabled` button (was a focusable/clickable `aria-disabled`,
  misleading on the axe-gated integration surface); arrow controls
  gained accessible names; active page uses `aria-current="page"`
  (review M1).
- **CI:** GitHub Pages now deploys only after the `CI` workflow
  concludes **successfully** on `main` (`workflow_run` trigger, not a
  bare push) — a red-e2e/broken demo can no longer be published
  independently of the gates.
- **docs:** theming.md documents the one accent surface the framework
  can't tune — native control `accent-color` under a pale re-brand
  (review css M2).

### Content-site layer

Promotes the proven, hand-rolled site shell into the first-class typed
contract so consumers stop reimplementing it. (The legacy `site-*` /
`.tag-list` back-compat classes referenced here were **removed** in the
same release — see the "legacy vocabulary removed / migrated" BREAKING
section above; they are not shipped.)

- **`site.css`**: `ui-container` (+`--narrow`), `ui-siteheader`
  (`__brand`/`__actions`), `ui-sitenav` (active via `aria-current`, dot
  cue, responsive collapse into `ui-sitemenu` — native `<details>`, no
  JS), `ui-sitefooter` (`__links`), `ui-skiplink`, `ui-tags`/`ui-tag`
  (`--accent`; neutral content labels, distinct from interactive
  `ui-chip`), `ui-meta` (dot-separated meta row).
- **`ui-quote`** (+ `__cite`) in `content.css`: a pull-quote companion
  to `.ui-prose` — emphasis by scale + a short accent rule, not a box.
- **Shiki**: `@ponchia/ui/shiki/nothing.json` — a documented optional
  VS Code/TextMate theme (rationed: brand accent + greyscale),
  drift-checked (`check:shiki` keeps it on-palette and in sync with the
  dark `--accent`). Bring-your-own-highlighter, like `tokens.dtcg.json`.
- Full contract treatment: 18 classes + `ui.container`/`ui.tag` recipes
  + `.d.ts` (guarded), cascade/exports/dist wired, demo + docs. The
  `not-a-gap` items from the source review (theme-toggle CSS already
  exists in `navigation.css`; `ui-timeline` too consumer-specific) were
  deliberately excluded.

## 0.2.2 — 2026-05-15

Component + mobile expansion, then a framework-grade hardening pass
(RTL, a11y, theming contract, Markdown content layer). Additive — no
existing token/selector values changed except documented WCAG fixes.

### Framework hardening

- **RTL / logical properties**: every `css/*` physical property is now
  logical (`*-inline/-block-*`), enforced by `stylelint-use-logical`
  (`csstools/use-logical: always`). Render-neutral in LTR; RTL mirrors
  cleanly (verified, incl. the drawer).
- **A11y**: `initTabs` behavior (WAI-ARIA Tabs keyboard pattern);
  `forced-colors` (Windows High Contrast) support in `base.css`; WCAG
  contrast fixes — light `--text-dim`/`--warning`, dark `--text-dim`
  now ≥ 4.5:1. Badge variants drop tone-on-tinted-tone text (failed AA
  at small bold) — tone now rides the border + tint, text inherits the
  high-contrast neutral. `.ui-eyebrow` uses `--accent-strong` so it
  clears 4.5:1 on soft surfaces too. (All surfaced by the new axe gate.)
- **Theming contract**: `--accent` is one knob — the whole accent family
  is `color-mix`-derived (ratios tuned to the prior hex, ≈ zero default
  drift). `data-density` (compact/comfortable) and `data-contrast=high`
  + `@media (prefers-contrast: more)` presets. See `docs/theming.md`.
- **content.css**: `.ui-prose` (+ `--compact`) styles raw
  Markdown-renderer HTML — headings, lists, quote, code, tables, media,
  figures — with **zero per-element classes**, keeping documents
  semantic and machine-readable.
- **CI regression safety**: Playwright visual snapshots of the demo
  (dark / light / RTL / modal) + `@axe-core/playwright` WCAG 2.1 A/AA
  gates (both themes, modal, tab keyboard pattern), as a new `visual`
  CI job pinned to the Playwright container the baselines were authored
  in (byte-stable, no cross-OS font flake). Catches CSS/markup
  regressions structure-checks can't.
- **Prebuilt bundles**: `dist/bronto.css` + `dist/bronto-core.css` —
  the `@import` graph flattened + conservatively minified into one
  `@layer bronto` file (~62 kB / ~11 kB gzip), no load waterfall.
  Exposed as `@ponchia/ui` (`.`) and `./dist/*`; `check:dist` keeps it
  byte-fresh and in a size budget; built in `prepack`. README documents
  the evergreen support floor (Chrome 111+/Safari 16.4+/Firefox 121+).

### Multi-agent review response

Acted on a deep read-only review (AgentMix `deep`), verifying each
finding against the code first:

- **Public TS contract**: `classes/index.d.ts` was stale — added the 9
  missing `ui.*` recipes (`alert`, `toast`, `progress`, `dotspinner`,
  `dotbar`, `modal`, `tab`, `avatar`, `prose`) + option types, and a new
  drift guard in `check-classes` so a recipe without a declaration now
  fails `npm run check`.
- **RTL completeness**: `[dir='rtl']` mirrors for the cases the lint
  plugin can't convert — switch thumb, theme-toggle thumb, `<select>`
  marker (`background-position`), arrow-link hover nudge.
- **Bug**: `.ui-progress__bar` transitioned the (now non-existent)
  physical `width`; animate `inline-size`.
- **Release gating**: `release.yml` now runs the containerised
  visual/a11y suite and `publish-npm`/`release-notes` depend on it — a
  tagged release can't skip what every branch push runs.
- **Hardening**: `initTabs` scopes to its own group (nested-safe);
  `initDialog` root semantics documented (dialogs are document-global
  by design); `scripts/serve.mjs` binds loopback + strict path
  containment; fixed an invalid selector in `docs/theming.md`.

### Second review pass (PR #3)

Independent Opus review said SHIP; the AgentMix `deep` mix flagged
more — verified each, fixed the real ones:

- **Release hygiene**: `package-lock.json` synced to `0.2.2`;
  `release.yml` `release-notes` now `needs: publish-npm` (no GitHub
  Release for a version that failed to publish — no split-brain).
- **RTL**: `.ui-toast-stack` used physical `inset` → logical
  `inset-block`/`inset-inline` so the stack mirrors in RTL.
- **`initTabs`**: a `root` that *is* the `[data-bronto-tabs]` element is
  now initialised (querySelectorAll only sees descendants); tabs are
  cross-linked to panels via `aria-controls`/`aria-labelledby`
  (APG-complete), ids minted only where absent.
- **Toast**: dropped the per-item `role="status"` nested inside the
  `aria-live` stack (double-announcement risk).
- **DTCG**: `--shadow: none` was typed `color` (name matched the colour
  regex, value failed the shadow test) — shadows are now classified by
  name first → `$type: "shadow"`.
- **Tests**: new `ui.*` recipe-output coverage + a `.d.ts`-declaration
  assertion; `initTabs` root-self/APG test; `tokens.dtcg.json` shape
  test (shadow typing + the null-+-extension invariant); `serve.mjs`
  `safePath` traversal unit test. The RTL e2e now waits for the
  mirrored end-state instead of racing a fixed sleep (the CSS was
  correct; the old test read mid-transition).

### Discovered follow-ups

Closed the remaining items surfaced across the review/verification:

- **Regression tests for the review fixes**: `serve.mjs` refactored to a
  pure exported `safePath()` with a traversal/sibling-prefix unit test;
  `initTabs` nested-isolation test — which **caught a real bug in the
  first nested fix** (the outer group's delegated click still fired for
  nested tabs); now gated on owned membership, not DOM containment. New
  e2e assertion that RTL truly mirrors the switch transform + select
  marker (not just box model).
- **Print** (`@media print` in `base.css`): ink-on-white, chrome hidden,
  `break-inside` guards, prose link URLs surfaced, `@page` margin.
- **prefers-reduced-data**: points `--display`/`--dot-font` at the mono
  stack so the Doto webfont is never fetched for data-saver users.
- **DTCG export**: `@ponchia/ui/tokens.dtcg.json` (W3C Design Tokens
  format) generated from the model, drift-checked (`check:dtcg`), built
  in `prepack`. Runtime-derived tokens are spec-shaped with
  `$value:null` + `$extensions` rather than fabricated numbers.

### Earlier in this cycle

Component + mobile expansion. No token/selector changes to existing
classes — purely additive; existing consumers are unaffected.

- **Dot loaders**: new orbital `ui-dotspinner` (the Nothing-signature
  ring loader, `--sm`/`--lg`), `ui-dotbar--indeterminate` sweep, and a
  linear `ui-progress` (determinate via `--value`, plus
  `ui-progress--indeterminate`).
- **feedback.css**: `ui-alert` / callout (tones + dismissible), `ui-toast`
  + `ui-toast-stack`, CSS-only `ui-tooltip`.
- **overlay.css**: `ui-modal` + `ui-modal--drawer` on native `<dialog>`
  (bottom-sheet on mobile), `ui-menu` dropdown.
- **disclosure.css**: `ui-tabs` (ARIA + `.is-active` contract, scrollable
  on mobile), `ui-accordion` (styled `<details>`), `:has()`-driven
  `ui-segmented`, `ui-breadcrumb`, `ui-pagination`, `ui-avatar` /
  `ui-avatar-group`.
- **Behaviors**: `initDialog` (native `<dialog>` open/close + backdrop
  light-dismiss) and `toast()`. SSR-safe; covered by the test suite.
- **Mobile**: 44px touch targets for buttons/inputs/checkboxes on coarse
  pointers; component-level breakpoints for modal/drawer/menu/tabs.
- **Contract**: 53 new classes added to the typed `cls` registry + recipes
  (`ui.alert`, `ui.toast`, `ui.progress`, `ui.dotspinner`, `ui.modal`,
  `ui.tab`, `ui.avatar`); `npm run check` and the 20-test suite stay green.
- **Docs**: removed the stale "not published yet" install note (the
  package is live on npm); documented the new layers and behaviors.

## 0.2.1 — 2026-05-15

- Remove private project names and personal paths from docs and CSS
  comments (no code/selector/token changes). Supersedes 0.2.0, which
  carried those references in shipped CSS comments.

## 0.2.0 — 2026-05-15

Architecture: keep plain CSS as the universal substrate, add thin optional
layers on top (see `docs/architecture.md`). No `css/*` selector/token values
changed — existing consumers are visually unaffected.

- **Cascade**: the whole framework now ships inside `@layer bronto`, so
  un-layered consumer CSS overrides it without specificity fights. Applied
  only at the bundle entrypoints (`core.css`, `index.css`); source files
  unchanged. _Behavioural change for consumers that override via specificity._
- **Fonts**: `@font-face` moved from `tokens.css` to `css/fonts.css` with
  package-relative URLs (`../fonts/*`) — no more absolute `/fonts`
  assumption. Bundled into `core.css`/`index.css`; exported standalone.
- **`@ponchia/ui/tokens`**: design tokens as data (`index.js` canonical,
  `index.json` generated, `themeColor()` helper, typed).
- **`@ponchia/ui/classes`**: typed class-name contract — `cls` registry,
  `ui.*` recipe builders, `cx()`. Framework-agnostic, returns strings.
- **`@ponchia/ui/behaviors`**: vanilla, SSR-safe, dependency-free helpers —
  `applyStoredTheme`, `initThemeToggle`, `dismissible`, `initDisclosure`.
- **Drift control**: `npm run check` adds `check-tokens` and
  `check-classes`; the demo now drives itself via the shipped modules.
- **Packaging**: `exports` for the new entrypoints (with `types`),
  `sideEffects` for tree-shaking, `files` widened.
- **Distribution decided**: published to npm as **`@ponchia/ui`** (the
  `@bronto` scope isn't ownable; the `@layer bronto` / `data-bronto-*`
  namespace is unchanged). `private` removed, `publishConfig`
  (`access: public`, `provenance: true`), `repository`/`homepage`/`bugs`
  added. `release.yml` now gates on a real `npm publish` job
  (`validate` → `publish-npm`). Rationale + pre-publish blockers
  (LICENSE, `NPM_TOKEN`, version bump) in `docs/architecture.md`.

## 0.1.0 — 2026-05-15

First standalone release. Extracted into its own standalone package.

- Re-skinned to a Nothing-inspired design language: monochrome dual
  light/dark palette, single red accent, Doto dot-matrix display type, flat
  hairline surfaces, sharp radii, no soft shadows.
- New `motion.css` — keyframes (migrated from the old responsive layer so
  core-only consumers keep their animations) plus reveal / stagger /
  skeleton / spinner / caret utilities and full reduced-motion handling.
- New `dots.css` — dot-grid surfaces, dotted rule, status dot (with live
  pulse), dot loader, dot progress bar, matrix-reveal.
- New `forms.css` — input, select, textarea, search, switch, checkbox.
- New `table.css` — `ui-table` with dense / comfortable / lined variants and
  numeric helpers for admin dashboards.
- Expanded `app.css` into a full admin shell: sidebar rail with dot nav,
  sticky blurred topbar, toolbar, panel, metric tiles, empty state, mobile
  rail collapse.
- Re-skinned primitives, navigation (dot active indicator), and the
  semantic typography/eyebrows.
- Renamed `theme.css` → `tokens.css`; dropped the `components.css`
  indirection; `core.css` now bundles the full set; `index.css` =
  core + responsive. `package.json` exports updated accordingly.
- Doto fonts vendored into `fonts/` as the canonical home.
- Added `demo/index.html` kitchen sink.
