# Changelog

## 18.4.0

### Minor Changes

- cd2c1e9: `<lr-export-button>`: expose the trigger's paint as public custom properties.
  
  The component resolved nine paint values — resting, hover and active fill/label/border — but only
  through private `--_lr-export-button-*` names, so the one and only way to repaint a trigger was to
  reach for `::part(trigger)`. Its sibling `lr-button` has layered the same nine over public
  `--lr-button-*` names for a long time; this is that pattern, applied here.
  
  `--lr-export-button-background`, `--lr-export-button-color`, `--lr-export-button-border` and the
  matching `--lr-export-button-hover-*` / `--lr-export-button-active-*` triples each layer over
  whatever the current `appearance` resolves to and leave the other paints untouched.
  
  That last point is the reported case. `appearance="outlined"` paints the label `--lr-color-brand`,
  and `lr-export-button` — unlike `lr-button` — has no `variant` of its own to return it to neutral
  text, so an outlined trigger sitting next to neutral-text buttons could not be matched through
  documented properties at all. `--lr-export-button-color` is now that escape hatch.
  
  Nothing changes for a consumer who sets none of them: every `appearance` still resolves exactly the
  values it did, and the private defaults are untouched.

## 18.3.0

### Minor Changes

- f652e93: Add compact sizing and outlined or quiet appearances to `lr-export-button`, plus a public PNG export
  method for `lr-heatmap` that captures the painted canvas and frozen label bands.
- 29e5202: Add an opt-in fit-to-viewport policy to `lr-lightbox`, reusing the image viewer fit vocabulary for landscape and tall images.

## 18.2.0

### Minor Changes

- 3c1e25c: `<lr-map>`: add `legendControlRole` for checkbox legend semantics.
  
  Every `legendInteractive` row rendered as a `<button aria-pressed>`, which reads as a set of
  pressed/unpressed actions rather than a checklist of independent show/hide toggles. The new
  `legendControlRole: 'button' | 'checkbox'` property (default `'button'`) lets a consumer switch the
  presentation without changing anything else: under `'checkbox'` the SAME `<button>` element renders
  `role="checkbox"` and `aria-checked` (both `"true"`/`"false"`, inverted from `hiddenCategories`
  exactly as `aria-pressed` was) in place of `aria-pressed`. The swatch, the label, the click handler
  and the platform's own Enter/Space activation are unchanged, so the cancelable
  `lr-map-legend-toggle` veto, `hiddenCategories` round-tripping, `group` sections and
  `legendCollapsible` all compose with either role. With `legendControlRole` unset, the rendered
  legend row is unchanged.
- 2a25254: Fix `--lr-icon-button-size-scope` being inert for most consumers.
  
  18.1.0 introduced `--lr-icon-button-size-scope` as the ancestor-scoped icon-button size
  input, resolving it as
  `var(--lr-theme-icon-button-size, var(--lr-icon-button-size-scope, 2.5rem))`. That order
  made the new token dead on arrival for anyone loading the shipped `design-tokens.css`,
  which declares `--lr-theme-icon-button-size` on `:root`: a `var()` chain only falls
  through when the referenced property is unset **everywhere**, not merely shadowed nearer
  the element, so the theme tier always answered first and the subtree override never
  resolved. Silently — which is precisely the failure mode the token was added to remove.
  
  The chain now reads the scope input first. That is also the correct precedence on its own
  merits: a subtree override should beat an application-wide default. `--lr-icon-button-size`
  keeps its element-scoped meaning, the coarse-pointer floor still applies to whichever value
  wins, and the resolved default is unchanged at `2.5rem`.
  
  If you worked around this by declaring `--lr-theme-icon-button-size: initial` at `:root`,
  that workaround is no longer needed and can be removed.
  
  The library's own tests missed this because their fixtures compose only the base token
  layer and never load `design-tokens.css`. A regression test now declares the theme tier on
  `:root`, reproducing a real consumer's setup.

### Patch Changes

- 3c1e25c: Document `<lr-map>`'s `legendControlRole` property in the authored `llms/media.md` reference: its
  `'button' | 'checkbox'` values, the `'button'` default, the `aria-checked` inversion (a hidden
  category renders unchecked), and when to prefer each role.

## 18.1.0

### Minor Changes

- 6137062: `<lr-filter-bar>`: under `activeFiltersDisplay="changed"`, the reset button now keys its enablement
  on `hasChangedFilters` instead of `hasActiveFilters`.
  
  `'changed'` exists to say that a filter sitting at its own declared `defaultValue` is not something
  the user applied, and it already suppressed that filter's chip. The reset button next to the empty
  chip row stayed enabled anyway, so a defaults-only bar rendered "nothing is applied" and "press here
  to clear what's applied" side by side, and pressing it was a no-op that still emitted `lr-input` and
  `lr-reset`.
  
  Nothing else changes: enablement under `activeFiltersDisplay="all"` (the default) and `"hidden"` is
  byte for byte what it was, `disabled`/`loading` still win in every mode, `hasActiveFilters` keeps its
  meaning, and `reset()` itself is untouched — it still restores every declared `defaultValue`.
- 6137062: `<lr-filter-bar>`: add a public read-only `hasChangedFilters`.
  
  `hasActiveFilters` answers "does any filter hold a value", which is deliberately `true` for a bar
  whose filters were declared with their own non-empty `defaultValue` and have never been touched —
  so a host that wanted to show "3 filters applied", enable an "Apply"/"Save search" action, or badge
  a collapsed filter panel had to re-derive the default comparison itself from `filters` and `value`.
  `hasChangedFilters` exposes it directly, using the exact equality `activeFiltersDisplay: 'changed'`
  already filters its chip row on: a `readonly string[]` default compares positionally, everything
  else compares with `Object.is`, and a filter with no declared `defaultValue` counts as changed the
  moment it holds any value at all. Clearing a filter that *does* declare one also counts as changed,
  since `reset()` would restore it — which is the one case where it differs from the `'changed'` chip
  row, whose entries are non-empty values by construction.
  
  `hasActiveFilters` is unchanged, and so is every existing behaviour keyed on it.
- 6137062: `<lr-filter-bar>`: a filter definition's `labelVisibility` accepts a third value, `'auto'`.
  
  `'visible'` and `'hidden'` were an all-or-nothing choice made once, at definition time, for a
  component that is as likely to sit in a 320px side panel as across a full page — so a bar authored
  for a dashboard lost a whole row of vertical space in a drawer, and one authored for the drawer
  shipped unlabelled fields to the dashboard. `'auto'` renders exactly what `'visible'` does (same
  stacked label element, same accessible name computed from it, no `aria-label` and no placeholder
  fallback) and visually clips that label once the bar's **own** allocation — a container query on the
  host, not the viewport — drops below `30rem`. The label element stays in the DOM at every width, so
  the field's accessible name is identical in both states; the previous workaround, visually hiding
  `::part(filter-control-label)` from a consumer stylesheet, removed the name along with the text.
  
  `'checkbox-menu'` participates through its own trigger label run, the same one `'hidden'` already
  clips there.
  
  `'visible'`, `'hidden'` and an unset `labelVisibility` are unchanged at every allocation. The host
  does become an inline-size query container (with the library's standard
  `contain-intrinsic-inline-size` fallback), which is a no-op for a bar in an ordinary block or flex
  allocation.
- 3f4be41: `<lr-map>`: add `legendCollapsible`/`legendOpen` and the cancelable `lr-map-legend-panel-toggle`.
  
  The legend panel could not be collapsed at all, so a large key permanently covered part of the map.
  `legend-collapsible` renders a native disclosure button inside the panel whose visible localized
  text is its accessible name, whose `aria-expanded` renders the literal `"true"`/`"false"`, and whose
  `aria-controls` names the row list in the same shadow root. `legendOpen` defaults **open**, so adding
  only `legendCollapsible` never hides an existing key; it is a `true`-defaulting boolean, so
  `legend-open="false"` parses and the open default reflects as an absent attribute.
  
  Activation emits the cancelable `lr-map-legend-panel-toggle` (`detail: { open }`). `preventDefault()`
  is a real veto — nothing is written and nothing re-renders — so a host can own the open state, and a
  programmatic `legendOpen` assignment reconciles without emitting, so a controlled host cannot loop.
  Collapsing hides the gradient, the rows, the `legend-limit` summary and the trailing `legend` slot;
  the `legend-start` slot and the disclosure stay visible. New `legend-disclosure` and
  `legend-disclosure-icon` parts. With `legendCollapsible` unset the rendered legend is unchanged.
- 3f4be41: `<lr-map>`: add an optional `group` to `LyraMapLegendEntry`, splitting the legend into sections.
  
  `legend` was one flat array rendered as a single list, so a key covering two layers could not say
  which rows belonged to which. Consecutive entries sharing an identical `group` now render as one
  section — a visible `legend-group-heading` plus a `role="group"` that heading names through
  `aria-labelledby`. The grouping rule is pinned rather than inferred: an entry with no `group` keeps
  its **declared** position (never hoisted or sunk), and a `group` that reappears after an interruption
  opens a second section rather than reordering rows to merge them.
  
  `group` is caller-supplied data, so it renders verbatim and is never passed through the locale
  catalog; it is trimmed, bounded to 256 characters, and a non-string, empty or whitespace-only value
  means "ungrouped" rather than an empty heading. Sections are not rows: the 100-row cap and the
  `legend-limit` summary still count rows. A grouped legend gives each run its own `role="list"`
  (a `list` may only own `listitem`s), while a legend with no groups renders exactly as before. New
  `legend-group` and `legend-group-heading` parts.
- 3f4be41: `<lr-map>`: add a `legend-start` slot that renders at the top of the legend panel.
  
  The panel's only host extension point was `slot="legend"`, which renders after the gradient bar and
  every projected row — so a host-authored panel header (a title, a source note, a control) could only
  ever be a footer. `legend-start` renders ahead of both. Content in it alone opens the panel, exactly
  as `legend` content alone already did, and it is never made interactive by `legendInteractive`. The
  existing `legend` slot keeps its position, so an unset map's legend markup is unchanged.
- 42af229: `<lr-select>`: `loading` now covers the whole pending state, not only its committed-value half.
  It already rewrote an unresolved committed value's label to the localized `loading` text and
  suppressed the "not in catalog" badge; with nothing selected at all -- a create form whose option
  catalogue is still being fetched, or an edit form whose saved selection is legitimately empty --
  `labelFor()` was never reached and the trigger fell through to the consumer's own `placeholder`.
  Covering that half meant hand-writing a conditional placeholder bound to the same flag and
  re-localizing, in the consuming app's own catalogue, the exact string this control already owns.
  
  The trigger now renders the same localized text in place of `placeholder` while `loading` is
  `true` and the selection is empty -- the existing `loading` message key, so both halves always read
  the same words and a `registerLyraLocale()` translation reaches them both. With `loading` false an
  empty selection renders the consumer's `placeholder` exactly as before, `value` is never touched,
  and the trigger's accessible name is unchanged: a host `aria-label` still wins, then `label`, then
  `placeholder`, then the localized `select` fallback.
- 42af229: `<lr-select>`: new `sync` property, closing the shared anchored-surface sizing vocabulary
  `<lr-popup>`, `<lr-popover>`, `<lr-dropdown>` and `<lr-combobox>` already spell. It takes the same
  `'width' | 'height' | 'both'` type (`PlaceSync`), the same unset default, and is wired through the
  same `place()` option, so `sync="width"` copies the rendered trigger width onto the listbox: a
  full-width select with short option labels no longer opens a listbox visibly narrower than, and
  floating centred under, its own trigger. Changes reposition an already-open listbox without closing
  it, and unsetting it releases the inline width the positioner wrote.
  
  With `sync` unset the listbox renders exactly as before -- `inline-size: max-content` between
  `--lr-size-12rem` and `min(--lr-popover-viewport-clamp, --lr-size-28rem)`. A synced listbox is
  capped on `--lr-positioner-available-inline-size` alone, adopting the corrected clamp rather than
  the viewport-clamp shortfall it would otherwise have inherited, so it matches its trigger at any
  width while the measured available space still keeps it on screen.
- b22fdbb: Design tokens: add `--lr-icon-button-size-scope`, an ancestor-scoped input for the icon-only
  control size.
  
  `--lr-icon-button-size` is element-scoped and has to stay that way: the shared token layer
  re-declares it on every `lr-*` host so the coarse-pointer touch-target floor can apply per element.
  The side effect is that a wrapper setting `--lr-icon-button-size` is replaced at the first
  component in between, so it never reaches an icon button composed inside another component — a
  `<lr-icon-button>` slotted through `<lr-popover>`, or the copy affordance inside `<lr-code-block>`.
  The only ancestor lever was `--lr-theme-icon-button-size`, which is application-wide by design, so
  "make just this toolbar denser" meant reaching for the global theme input and scoping it by hand.
  
  `--lr-icon-button-size-scope` is declared nowhere, so it inherits the whole way down and reaches
  every icon-only control below the wrapper that sets it:
  
  ```css
  .message-toolbar {
    --lr-icon-button-size-scope: 1.75rem;
  }
  ```
  
  Precedence is `--lr-theme-icon-button-size` → `--lr-icon-button-size-scope` → the `2.5rem` default,
  and the coarse-pointer floor still raises a resolved value below `2.75rem` back to it, so the new
  knob is not a route around WCAG 2.2 SC 2.5.8. Purely additive: `--lr-icon-button-size` keeps its
  current element-scoped behaviour exactly, and the resolved default is unchanged.

### Patch Changes

- 6137062: Docs: `<lr-filter-bar>`'s consumer reference now covers `hasChangedFilters` (and how it differs from
  `hasActiveFilters` on a defaults-only bar), the `activeFiltersDisplay="changed"` reset-enablement
  coupling, and `labelVisibility: 'auto'` with its `30rem` container-query threshold.
- 3f4be41: Document `<lr-map>`'s collapsible legend, `legend-start` slot and legend sections in the authored
  `llms/media.md` reference: `legendCollapsible`/`legendOpen` (including the `true` default and the
  `legend-open="false"` parse), the cancelable `lr-map-legend-panel-toggle`, the `group` field and its
  pinned ordering rule, and the four new parts.
- 42af229: `<lr-combobox>`: a `sync="width"`/`sync="both"` listbox is no longer shortened by
  `--lr-popover-viewport-clamp`. The synced rule capped the listbox at
  `min(var(--lr-popover-viewport-clamp), var(--lr-positioner-available-inline-size, 100vw))`, and the
  clamp's 92vw default resolves below the width of exactly the full-width trigger `sync` exists to
  serve -- a 738px trigger in an 800px viewport got a 736px listbox, and the shortfall grows with the
  trigger. The synced rule now caps on `var(--lr-positioner-available-inline-size, 100vw)` alone,
  matching `<lr-popup>`: a width-synced listbox is anchored to an element already on screen and
  measured against the space actually available beside it, so the extra viewport ceiling had nothing
  left to protect against, while the available-space term still keeps an over-wide anchor from
  pushing the listbox off-screen. An unsynced listbox is unchanged and keeps
  `min(--lr-popover-viewport-clamp, --lr-size-28rem)`.
- 42af229: Docs: `llms/forms.md` now documents `<lr-select>`'s new `sync` property (same vocabulary note as
  `<lr-combobox>`'s, stating its unset default), records that a width-synced listbox on **both**
  controls is capped on `--lr-positioner-available-inline-size` alone rather than also by
  `--lr-popover-viewport-clamp`, and describes `loading`'s coverage of an empty selection alongside
  the `placeholder` entry it now takes precedence over.
- b22fdbb: Design tokens: derive the dark-mode overlay surface from the page surface instead of pinning it.
  
  `--lr-color-surface-overlay` is the panel colour behind every floating surface — dropdowns,
  listboxes, menus, toasts, popovers, dialogs, and the `lr-app-rail` mobile drawer. In light mode it
  already resolves straight to `--lr-color-surface`, so one `--lr-theme-color-surface-default`
  override carries all of them with it. Dark mode pinned a literal instead, because panel and page
  resolving to the same near-black makes an open dialog read as a scrim with text floating on it and
  no panel at all. The cost was that re-skinning the dark base surface left every floating surface at
  the stock colour — a mismatched panel rather than a themed one.
  
  It is now derived, keeping the elevation delta the literal existed to provide:
  
  ```css
  --lr-color-surface-overlay: var(
    --lr-theme-color-surface-overlay,
    color-mix(in srgb, var(--lr-color-surface) 85%, #8bade2)
  );
  ```
  
  At the built-in dark base the pair resolves to the same panel colour it always has, so no existing
  dark theme moves. Light mode is untouched, and an explicit `--lr-theme-color-surface-overlay` still
  wins outright.
- b22fdbb: Docs: state the icon-button size scopes and the derived dark overlay surface in `llms/shared.md`.
  
  Three properties now resize icon-only controls and they differ only in how far they reach, which is
  not something a consumer can infer from the names: `--lr-theme-icon-button-size` is
  application-wide, `--lr-icon-button-size-scope` covers one subtree, and `--lr-icon-button-size` is
  element-scoped because the shared token layer re-declares it on every host. The reference now says
  so in one table, with the precedence order and the coarse-pointer floor spelled out, instead of
  leaving "my wrapper rule does nothing" to be rediscovered per project.
  
  The overlay-surface entry records that `--lr-color-surface-overlay` follows
  `--lr-theme-color-surface-default` in dark mode as well as light, so a single base-surface override
  re-skins every dropdown, listbox, menu, toast and drawer, and explains why dark derives the value
  rather than resolving straight to the page surface the way light does.
- 90449c6: `<lr-virtual-list>`: `row-height="auto"` driven by an external `scrollElement` no longer raises the
  uncaught window error "ResizeObserver loop completed with undelivered notifications." when the
  scroller jumps to its end and the newly revealed window measures. Nothing was ever dropped -- the
  browser re-delivers on the following frame -- but the notice arrives as an uncaught `ErrorEvent` on
  `window`, so it landed on whatever was running at the time and failed consumers' clean-console e2e
  gates with a message that named no component.
  
  The offending write is `[part="spacer"]`'s height, the list's whole virtual extent. `onRowsResized()`
  folded each newly measured row height into the offsets and asked for a render; Lit flushes that
  render on the microtask checkpoint that follows the observer callback, which is still inside the
  browser's resize-observation delivery. Under an external `scrollElement` that write resizes an
  *observed* box, because `[part="base"][data-external-scroll]` takes its own block size from the
  spacer and is watched by the container `ResizeObserver` -- and `[part="base"]` sits shallower in the
  tree than the rows just broadcast, so the browser records the resize as a skipped observation and
  ends the loop with the error. Reproduced deterministically in WebKit and, with a large enough first
  measurement delta, in Chromium too.
  
  Reading inside the delivery is fine; folding the result into the offsets is what resizes the box.
  So the measurement callbacks now only stash what they observed, and the offsets rebuild -- with the
  scroll-anchor correction that belongs to it, so neither is ever painted without the other -- runs on
  the animation frame this component already used to defer `observe()` calls out of the same delivery.
  A render that still lands mid-delivery then re-reads offsets nothing has changed and writes the
  extent already in the DOM, which resizes nothing. Renders themselves are never held: this element's
  `updateComplete` keeps settling in the same microtask run as before, which is what every component
  composing it relies on when it reads rendered rows back after its own update. Only the
  external-`scrollElement` case defers at all -- with the list's own viewport scrolling,
  `[part="base"]` takes its block size from `--lr-virtual-list-height` rather than from the spacer, so
  the extent write reaches no observed box and every measurement there stays as immediate as it was,
  fixed numeric `row-height` included. The error message itself is neither suppressed nor filtered
  anywhere.

## 18.0.0

### Major Changes

- 555b974: **Major only because several exported TypeScript unions and maps gained members. No runtime
  behaviour changes, no attribute or event was removed or renamed, and no default moved.** If you do
  not perform exhaustive type-level matching over the members listed below, this release is a
  drop-in upgrade and you can stop reading here.
  
  The public-API gate classifies widening an exported union as breaking, and it is right to: a
  consumer with an exhaustive `switch` over one of these, or a mapped type keyed by one, stops
  compiling until the new member is handled. Nothing else about them changed.
  
  **`lr-filter-bar`**
  - `LyraFilterBarControlType` gains `'chip'`.
  - `LyraFilterBarFilterDefinition` gains `LyraFilterBarChipDefinition`.
  - `LyraFilterBarDefinitionValue<D>` gains an arm resolving a chip filter to the full
    `LyraFilterBarFieldValue`, exactly as `'custom'` already does. Every other `D` resolves as before.
  
  An exhaustive `switch (definition.type)` over a filter schema now needs a `'chip'` case, and a
  `Record<LyraFilterBarControlType, T>` needs a `chip` key.
  
  **`lr-map`**
  - `LyraMapLegendEntry` gains an optional `value` category key.
  - `LyraMapEventMap` gains `'lr-map-legend-toggle'`.
  - New exported `LyraMapLegendToggleDetail`.
  
  A `Record<keyof LyraMapEventMap, T>` needs the new key. `LyraMapLegendEntry` is only widened by an
  optional property, so constructing one is unaffected; only code that enumerates its keys is.
  
  **`lr-virtual-list`**
  - New `rowProjection` property and `row-projection` attribute, new `projectedRows` getter, new
    exported `LyraVirtualListRowProjection`, and two exported constants,
    `VIRTUAL_LIST_ROW_ATTRIBUTE` and `VIRTUAL_LIST_STICKY_ATTRIBUTE`.
  
  **Generated framework declarations.** `./custom-elements-jsx`, `./vue` and `./svelte` are
  regenerated from the manifest, so the prop types for the components above widen accordingly. This
  is the bulk of the gate's reported diff and needs no action.
  
  **Migration:** add the new members to any exhaustive union handling; otherwise upgrade directly.

### Minor Changes

- 49eb3ef: `<lr-combobox>` gains `sync` (unset by default), spelled and shaped identically to
  `<lr-dropdown>`/`<lr-popup>`/`<lr-popover>`'s property of the same name. Setting `sync="width"`
  copies the trigger's rendered width onto the listbox and drops the previously fixed 12rem-28rem
  content-based clamp (keeping only the outer viewport/available-space ceiling), so a full-width
  trigger with short option labels gets a listbox that aligns to its own edges instead of floating
  narrower in the middle. Left unset, the listbox continues to size to its own content exactly as
  before.
- 204aeb4: **`<lr-filter-bar>`: a control-less `type: 'chip'` filter, for a value owned by a widget elsewhere
  on the page.**
  
  A calendar heatmap cell, a map selection, a chart brush — the filter is real, but the control that
  sets it is not in the toolbar. Until now the only way to surface such a filter in the bar was a
  `type: 'custom'` definition whose renderer drew something inert just to occupy the cell it was
  forced to claim.
  
  `type: 'chip'` renders **no control and no toolbar cell**: no `field` wrapper is emitted for it, so
  `lr-filter-bar::part(field)` and `::part(field-<filterId>)` never match one, and a bar whose filters
  are *all* chip-only paints no empty column — its `controls` row still renders the reset button (the
  "clear all" action such a bar needs), the `end` slot and the loading spinner, exactly like a bar
  declaring no filters at all.
  
  Everything else is unchanged from any other filter type. The value lives in `value` under its own
  filter ID, rides every `lr-input`/`lr-reset` detail, counts toward `hasActiveFilters` (so it enables
  the reset button) and toward `invalidFilterIds` when `required`, renders a removable active-filter
  chip subject to `activeFiltersDisplay`, and is cleared both by removing that chip and by `reset()`.
  A `required` chip-only filter is honoured in bookkeeping only — it joins `invalidFilterIds`, fails
  `checkValidity()` and moves `lr-validity-change` — but renders no inline error, because the bar
  renders no element of its own for it; the owning widget keeps its own error affordance. The
  inherited `placeholder` is inert here for the same reason it already is for `type: 'custom'`.
  
  **Chip text.** An optional `formatValue(value, locale)` produces it, and `locale` is the bar's
  `effectiveLocale` — the same locale every built-in type's own chip formatting and a custom adapter's
  `formatValue` already receive, so the caller localizes its own data. Omitted, the fallback ladder is
  the one a custom adapter's omitted `formatValue` uses: a string array renders as a localized
  conjunction list, anything else renders verbatim through `String(value)`, and an unset value renders
  empty. Verbatim is exact — a chip-only value never passes through the date branch that localizes a
  `'date'`/`'date-range'` chip, so an ISO day is not silently reformatted and a value containing a
  slash is not mangled.
  
  **Clearing.** An optional `clearValue` (default `''`, what every non-multi built-in type writes) is
  what a chip removal writes; declare `[]` for an array-valued chip-only filter. An optional `isEmpty`
  overrides the built-in emptiness rule. A domain sentinel must pair the two: a sentinel clear value
  with no matching `isEmpty` leaves the bar reading the "cleared" value as still set and still
  rendering a chip for it — the identical pairing a custom adapter's own `clearValue`/`isEmpty`
  already documents. With the pair declared, the sentinel is never stored in `value` and an absent key
  reads back as the sentinel for the owning widget.
  
  **Additive only.** A schema declaring no `'chip'` filter renders byte-identical shadow DOM: the
  render path keys on `type === 'chip'` exactly, never on "not a known control type", so an
  unrecognized `type` still falls back to `<lr-select>` as before. At the type level, a `'chip'` filter
  keeps the full unconstrained field value in `LyraFilterBarValueFor<Defs>`, like `'custom'`. New
  exported type: `LyraFilterBarChipDefinition`.
- af5a12e: `<lr-map>` gains an opt-in interactive legend, so a categorical point layer's key can double as its
  layer switch instead of needing a hand-built checkbox group positioned over the canvas.
  
  `legendInteractive` (attribute `legend-interactive`, default `false`) turns every legend row that
  carries the new `LyraMapLegendEntry.value` category key into a keyboard-operable toggle button.
  `value` is the same string a `point.colors` / `point.icons` record matches against
  `point.field` / `point.iconField`, so one set of category records can feed both the paint and the
  key and the two cannot drift. It is distinct from — and does not derive from — the `value` on a
  row's `icon` record, which is still dropped from the canonical readback as before. Rows without a
  `value` stay inert even when `legendInteractive` is set.
  
  `hiddenCategories: readonly string[]` is the controlled state, mirroring `<lr-chart>`'s
  `hiddenDatasets`, and is honoured on first render rather than only after a user toggle. Activating a
  toggle emits a cancelable `lr-map-legend-toggle` carrying the activated key, its proposed
  visibility, and the complete proposed hidden set; `preventDefault()` is a real veto — nothing is
  written, the row's `aria-pressed` does not change, the map is not repainted, and no announcement is
  made — so a host can own the state itself. A programmatic `hiddenCategories` assignment reconciles
  silently and emits nothing.
  
  A hidden category's points, point icons and stroke are muted in the rendered MapLibre layers
  through the new `--lr-map-hidden-category-opacity` (default `0.15`), resolved live from the cascade
  because MapLibre paints to a WebGL canvas that never sees `var()`. The legend row itself dims only
  its decorative swatch (`--lr-map-legend-hidden-swatch-opacity`, default `0.5`) and re-colors its
  label through the quiet-text token, so the label keeps WCAG AA contrast rather than fading with the
  swatch. New parts: `legend-toggle` and `legend-toggle-hidden`.
  
  Each toggle is an ordinary tabbable `<button>` with `aria-pressed` rendering both `"true"` and
  `"false"`, matching `<lr-chart>` and `<lr-graph-legend>` rather than introducing a third legend
  vocabulary; a long interactive legend therefore contributes one tab stop per keyed row. Show/hide
  state changes are announced through the shared light-DOM live region.
  
  With `legendInteractive` left unset, the legend DOM and every MapLibre paint property are
  byte-identical to before: no button is rendered and `circle-opacity` is never written at all.
- 3024e74: Added a complete Romanian (`ro`) translation catalog, the twelfth full locale to ship with the
  package. Like every other locale, it is split into twelve side-effect-only per-family slices
  (`@aceshooting/lyra-ui/translations/ro/<family>.js`) plus the back-compat aggregate
  `@aceshooting/lyra-ui/translations/ro.js`, covering all 1,291 keys in `LYRA_DEFAULT_STRINGS`
  (including the 14 pluralized entries). Romanian's CLDR plural category set is `one`/`few`/`other`
  rather than English's `one`/`other` — `few` covers `0` and `2`-`19` (and the `101`-`119`-per-hundred
  band), while `other` is the `de`-requiring form from `20` upward (`"20 de rezultate"` vs.
  `"2 rezultate"`) — so every pluralized message was authored with all three categories rather than
  widening `few` to `other`.
- 9db9165: `<lr-select>` gains `loading` (default `false`, reflected). A committed value whose catalog hasn't
  arrived yet -- `<lr-option>`s still being fetched/mounted asynchronously -- previously had no way to
  distinguish itself from a genuinely stale value: both rendered the raw value string badged
  dashed/italic `notInCatalog`. Setting `loading` while the matching option is still pending instead
  renders the localized `loading` placeholder, with no `unknown-value` badge and no synthetic
  `showUnknownOption` listbox row, in the trigger label or the relevant `multiple`-mode tag. Once the
  matching `<lr-option>` mounts, the real label renders automatically on the next render, with or
  without also flipping `loading` back to `false`. A value that already matches a live option, and
  every existing `loading`-unset unknown-value/`getUnknownLabel` behavior, is unchanged.
- 1942512: `<lr-streaming-text>`/`<lr-streaming-text-core>` now forward their composed
  `<lr-markdown>`/`<lr-markdown-core>`'s documented CSS parts (`content`, `heading`, `paragraph`,
  `list`, `code-block`, `inline-code`, `link`, `table`, `blockquote`, `img`, `math`) through
  `exportparts`, reusing each name verbatim since none collides with either wrapper's own `base`/
  `cursor` parts. A host-level `lr-streaming-text::part(link)`/`::part(img)` (or the `-core` variant's
  equivalent) rule now reaches the rendered `<a>`/`<img>` the same way it already does applied
  directly to `<lr-markdown>`/`<lr-markdown-core>` -- previously that styling surface was unreachable
  from outside either wrapper's own shadow boundary.
- c78f78c: `<lr-virtual-list>` gains `row-projection`, an opt-in light-DOM row projection mode, so an
  application whose list rows are already styled by its own global stylesheet can adopt virtualization
  without rehoming every row rule.
  
  Until now `renderItem`'s output was stamped inside the component's shadow root, so document CSS
  could not reach row content: adopting virtualization meant moving a dozen descendant rules per row
  into a new custom element or a growing set of custom properties — a stylesheet refactor, paid
  exactly by the lists long enough to need virtualizing. With `row-projection="light"` the windowed
  rows are rendered into the host's own light DOM and assigned into the shadow viewport through
  internal named slots. The component keeps owning windowing, measurement, spacer sizing,
  `scrollToIndex()`, the external-scroller mode and the ARIA contract; the consumer keeps owning the
  row markup and its cascade, and ordinary document CSS styles a virtualized row exactly as it styles
  the same row unvirtualized.
  
  Positioning stays on the shadow-side `[part="row"]` wrapper, which the document cannot select, so
  windowing is not overridable by consumer CSS and the whole existing part vocabulary — `base`,
  `spacer`, `row`, `group`, `sticky-group` — keeps matching in both modes.
  
  Costs, stated plainly rather than hidden:
  
  - Each projected row sits inside one component-owned wrapper element carrying the reserved
    `data-lr-virtual-list-row` attribute (exported as `VIRTUAL_LIST_ROW_ATTRIBUTE`; the sticky band's
    counterpart is `VIRTUAL_LIST_STICKY_ATTRIBUTE`). Descendant selectors port unchanged; child
    combinators, `:nth-child`, `:first-child` and sibling combinators written against unvirtualized
    markup do not, and `:nth-child` reflects the current window rather than the item index.
  - A delegated listener on the host now sees an un-retargeted `event.target`, so
    `closest('[part="row"]')` no longer resolves — use `closest('[data-lr-virtual-list-row]')`.
  - The document cascade now reaches row content, including resets that previously could not.
  - Per-row light-DOM state does not survive a disconnect/reconnect, because disconnect removes the
    projected rows completely.
  - On a hydrated page the first window is shadow-rendered for one task before it swaps into the
    light DOM, so server markup and the first client render agree.
  
  `rowProjection` is `'shadow'` by default. Left unset, every existing list renders byte-identically
  and the host's light DOM stays empty.

### Patch Changes

- ce460db: **Documentation: `aria-pressed` stopped reaching a link button (`href` set) in 16.0.0 — recorded
  here because 16.0.0 filed it where nobody would find it.**
  
  No behaviour changes in this release. This entry exists because the change itself shipped without a
  discoverable release note, and a consumer pinning the old contract met it as an unexplained test
  failure after a dependency bump.
  
  **What changed, and when.** Through 15.0.0, `<lr-button>`, `<lr-icon-button>` and `<lr-card>`
  forwarded a host `aria-pressed` onto the `<a>` they render when `href` is set. Since **16.0.0** they
  do not: `aria-pressed` reaches the `<button>` rendering only. `role="link"` has no pressed state, so
  forwarding it there asserted an ARIA state that does not exist on that role — an ARIA conformance
  failure, not a feature. The behaviour is correct and is not being reverted.
  
  This narrowed a documented forwarding contract, so it was a **breaking change** for anyone relying
  on the old behaviour, and it should have been filed as one. 16.0.0 did describe it, but as the third
  sub-heading of a long `<lr-card>` disabled-state entry whose opening sentence never mentions
  `<lr-button>` — so neither scanning the release notes for button changes nor reading a per-component
  digest surfaced it.
  
  **Migration.** If the control is a real toggle, drop `href` — the same host `aria-pressed` then
  reaches the `<button>` that replaces the anchor, and the pressed state is exposed again. If it is a
  navigation target, use the global `aria-current` (`page`, `step`, `location`, `date`, `time`,
  `true`, `false`) instead: that one is valid on `role="link"` and does still reach the anchor.
  
  The `lr-button`, `lr-icon-button` and `lr-card` reference sections now name 16.0.0 as the version
  the carve-out landed in, so the rule can be dated from the reference alone.
- 87d02cb: Fixed `<lr-reorder-list>`'s documented Ctrl/Cmd+ArrowUp/ArrowDown shortcut so it fires from real
  keyboard focus on a row's own move button, not only when a `keydown` happens to be dispatched
  synthetically on the button's host element. The move buttons are composed `<lr-icon-button>`s, so
  real DOM focus during ordinary keyboard use lands on that button's own native `<button>` — one
  shadow boundary deeper than `<lr-reorder-item>`'s own shadow root. The exclusion predicate that
  tells the row's own chrome apart from a consumer's nested control only recognized an element whose
  root node was exactly the item's shadow root, so the nested native button fell through to the
  generic `name === 'button'` exclusion and every Ctrl/Cmd+Arrow press starting from real focus on a
  move button was discarded as though it had come from a consumer's own control.
- 87d02cb: Fixed `<lr-reorder-list>` in `controlled` mode so it no longer steals focus back to the moved row
  when the consumer has deliberately moved focus elsewhere while an async `finalizePendingMove()` /
  host reconciliation is still in flight. Previously the post-move focus restore unconditionally
  focused the moved row's move button once the host's re-render settled, even when the consumer had
  already moved focus to an unrelated control on the page in the meantime — overriding that
  deliberate choice. The restore now only runs when the pre-restore focus was still somewhere inside
  this list, or there was nothing meaningful to preserve (nothing focused, or the previously-focused
  node was disconnected by the host's own reconciliation); a still-connected external focus target
  is left alone.
- cada7da: `<lr-table>`'s `editType: 'select'` cell editor now shows a themed disclosure chevron instead of
  looking like a plain text field. `appearance: none` on `select[part='cell-editor']` removed the
  native arrow with no replacement; the glyph is repainted on the enclosing `[part='cell']` (a bare
  `<select>` cannot host a decorative child, and a `mask` can't apply to the select itself without
  clipping its own text), using the same mask + `background: currentColor` technique already shipped
  for the map attribution-toggle glyph, positioned at the logical inline-end edge so it mirrors
  correctly under `dir="rtl"` and survives `forced-colors: active`. The `text`/`number` cell editors
  are unaffected.
- e4402ef: Fixed `<lr-thread-list>` data-mode rows so choosing a `renderActions`-rendered menu action (for
  example a consumer's `<lr-dropdown>` containing an `<lr-menu>`) no longer also fires the row's own
  `lr-select` and selects/opens the conversation. The row's `lr-select` listener previously reacted to
  any `lr-select`-named event that bubbled through the `<lr-conversation-item>`, including one
  coincidentally emitted by a `renderActions`-rendered descendant (`<lr-menu>` fires its own
  `lr-select` on item choice). It now only treats the event as row activation when it was dispatched
  directly on the `<lr-conversation-item>` itself (`e.target === e.currentTarget`), which is how a
  real row click/keyboard activation always dispatches it; a descendant's bubbled event is still
  absorbed at the row boundary (as before) but no longer re-emitted as the row's own selection.

## 17.0.0

### Major Changes

- a0507c3: Release the accumulated Lyra UI API, metadata, compatibility, and accessibility corrections as
  `17.0.0`. This release includes the common-issue cleanup pass across form controls, overlays, data
  and retrieval components, viewers, shared tokens, localization, RTL behavior, generated contracts,
  and package-size tooling.

### Minor Changes

- e7d8187: `<lr-app-rail>`'s `[part="panel"]` gains four direction-aware per-corner radius tokens
  (`--lr-app-rail-panel-radius-start-start`/`-start-end`/`-end-start`/`-end-end`), each defaulting to
  the existing uniform `--lr-app-rail-panel-radius` so an unset override still rounds all four
  corners exactly as before. Because the mobile drawer is always flush against its own logical
  inline-start edge, the two `-end` corner tokens are the ones a flush-against-one-edge drawer
  typically rounds, and both mirror to the opposite physical side under `dir="rtl"` with no second
  consumer rule. `[part="nav"]` gains `--lr-app-rail-nav-padding` and `--lr-app-rail-nav-gap`,
  defaulting to the values that rule previously hard-coded. `<lr-app-rail-item>`'s icon-only square
  gains `--lr-app-rail-item-icon-only-size`, which — when set — sizes `[part="base"]`'s icon-only
  square (and its `min-block-size` floor) directly, independent of
  `--lr-app-rail-item-min-block-size`, so a taller expanded row and an icon-only square pinned to
  `--lr-icon-button-size` can coexist. Unset, all three continue to reproduce their prior rendering
  exactly.
- e7d8187: `<lr-app-rail-item>`'s `[part="current-indicator"]` full-height edge bar now suppresses itself by
  default in the `icon-only` presentation, where it previously painted a broken-looking bar across a
  square icon tile. `--lr-app-rail-item-current-indicator-display` restores it per instance. A new
  `--lr-app-rail-item-current-ring` token adds an inset ring on the icon-only current tile by
  default — a non-color-only signal (WCAG 1.4.1) that replaces the suppressed bar there — while
  leaving the full presentation, which already conveys current state through the bar and
  `--lr-app-rail-item-current-font-weight`, ring-free by default. Setting the ring token explicitly
  applies the same value in both presentations. Full presentation is otherwise byte-identical to
  before.
- fa5c976: `<lr-app-rail-item>` gains its own expandable child list — the treeitem-with-link pattern used by
  repository trees, Notion-style page trees and IDE explorers, where the row itself navigates and a
  separate disclosure expands that item's own nested rows. Nested `<lr-app-rail-item>`s slotted into
  `children` grow a built-in disclosure (`[part="toggle"]`) as a SIBLING of the item's own
  link/button, never nested inside it, so the link keeps navigating on its own and the disclosure
  keeps toggling on its own. The new `expanded` property (reflected, `false` by default) is driven
  through a cancelable `lr-toggle-request`/settled `lr-toggle` pair, mirroring
  `<lr-app-rail-group>`'s collapsible contract exactly (same event names, same `{ open }` detail
  shape, same request/commit veto semantics). The disclosure carries `aria-expanded` and
  `aria-controls`, and a localized accessible name interpolating the item's own label. `icon-only`
  forwards from an item onto every `<lr-app-rail-item>` it owns through `children`, exactly how
  `<lr-app-rail-group>` forwards onto the items and groups it owns; a new
  `--lr-app-rail-item-indent` token (default `var(--lr-space-l)`) indents `[part="children"]` once
  per nesting level. An item with nothing slotted into `children` renders no disclosure and no
  `[part="children"]` at all — byte-identical to an item authored before this feature existed.
- 351a078: Added `lr-chart` (and every chart tag built on it -- `lr-bar-chart`, `lr-line-chart`,
  `lr-scatter-chart`, `lr-bubble-chart`, `lr-pie-chart`, `lr-doughnut-chart`, `lr-radar-chart`,
  `lr-polar-area-chart`, `lr-histogram`) three additive surfaces so a mixed dashboard chart no longer
  needs to drop to raw Chart.js: `LyraChartSeries.stack` (a per-series Chart.js dataset `stack` group
  id -- two groups on the same stacked axis are summed independently and drawn side by side);
  `stackedAxes` (a per-value-axis override of `stacked`, so a stacked bar series on the primary axis
  can sit next to an unstacked overlay series on `y2`); and `tooltipTitleFormatter`/
  `tooltipFooterFormatter` (tooltip title/footer hooks receiving every hovered item's context at
  once, in the same `LyraChartFormatterContext` shape `formatter`'s `'tooltip'` surface already
  produces). `computeStackTotals()` is now computed per stack group as well as per axis, and the
  canvas `stack-totals` data-label draws once per (axis, group) rather than once per axis. All three
  additions are opt-in and unset by default: a chart that only sets the legacy `stacked` boolean
  renders byte-identically to before. `lr-lite-chart` has no counterpart for any of the three --
  documented as a deliberate omission (its single-value-scale, single-stack SVG bar-geometry model
  and native per-mark `<title>` tooltip have no equivalent shape for a second value axis, a
  per-series stack group, or a multi-item tooltip title/footer) rather than implemented.
- 7ef4f5e: `<lr-combobox>` now takes part in the library's positioning-strategy contract. It exposes
  `positioningStrategy` (attribute `positioning-strategy`), spelled the same as on `<lr-select>`,
  `<lr-popover>`, `<lr-dropdown>`, `<lr-tooltip>` and `<lr-color-picker>`, and when the instance sets
  nothing it honours the cascading `--lr-positioning-strategy` custom property ahead of its own
  default.

  Previously the listbox was always `fixed` through an undocumented internal default: an app that set
  `--lr-positioning-strategy` once on `:root` to retune every floating surface silently left comboboxes
  behind, and no instance could opt out. The default stays `fixed` — what the listbox has always
  rendered, and the right choice for a typeahead list that usually sits inside a scrollable region — so
  existing layouts do not move.

  There is deliberately no `hoist` alias on this control. On `<lr-select>` it is Shoelace's established
  spelling; here it would be a boolean defaulting to `true`, whose attribute could only ever express
  the value the control already has. Use `positioning-strategy="absolute"` to opt out.
- 4fc2bc1: `<lr-context-meter>`'s interactive mode can now express a non-actionable band. A `segments` entry
  accepts `disabled`, which renders that band and its legend row as genuinely disabled controls — no
  tab stop, no hover or press affordance, and no `lr-segment-activate` on activation. Previously a
  band that filtered to nothing was still a fully enabled, tabbable button, so a keyboard user tabbed
  through dead controls with no indication, and nothing distinguished the row visually.

  Two derived state tokens join `segment-selected`/`legend-item-selected`, and all of them compose:
  `segment-empty`/`legend-item-empty` for a band whose `value` is 0, and
  `segment-disabled`/`legend-item-disabled` for a declared-disabled entry. `--lr-context-meter-disabled-opacity`
  (default `0.5`) themes the disabled treatment.

  Inertness is deliberately never inferred from `value === 0`: a zero band is legitimately clickable
  in a token-budget meter, so the empty pair is a presentational hook with no built-in treatment while
  `disabled` is declared by the consumer. A meter whose entries set neither renders exactly as before.

  Internally this adds a shared `statePart()` helper, so a part name carrying several states is built
  one way across the library instead of being hand-rolled per component.
- 6c826ec: `<lr-file-input>` and `<lr-drop-zone>` can now enforce a cumulative `maxFiles`/`maxTotalSize` cap
  across separate picker or drop sessions. Both limits previously counted only the files the control
  itself held, so with `nonRetaining` (or after a reload in retaining mode) a user already holding 99
  server-persisted files could add 50 more without the control ever rejecting, and the consumer had to
  re-implement the aggregate check and render its own error.

  Two optional numeric properties, `heldFileCount` (`held-file-count`) and `heldTotalSize`
  (`held-total-size`), are the numeric counterpart of `valuePresent`: they are added to the running
  totals before a batch is evaluated, in both retaining and non-retaining modes, so an over-cap batch is
  rejected through the control's own rejection UI with the existing `maxFiles`/`maxTotalSize` reasons.
  Both default to `0`, which reproduces today's behavior exactly, and a negative, `NaN` or infinite
  value is treated as `0`. The `lr-files` detail also gains `remainingFiles` and `remainingTotalSize`
  (`null` while that limit is unset), so a consumer can say how many more files may be added. The
  aggregate arithmetic is now shared by both components, so the contract cannot drift between them.
- 771d6fc: Five `<lr-filter-bar>` fixes.

  An unset `'select'` or single `'combobox'` filter bound `''`, which those controls treat as a
  committed unmatched value, so every page load showed their "not in catalog" treatment instead of the
  declared placeholder. Unset now binds absent. The date branch deliberately keeps `''`, having no
  catalog to mismatch against.

  `activeFiltersDisplay` (`all` | `changed` | `hidden`, default `all`) controls the active-filter chip
  row. It was unconditional, and a filter resting at its `defaultValue` counted as active — so a bar
  whose default narrows the view claimed the user had narrowed it. `changed` lists only what the user
  actually changed. The default reproduces today exactly.

  The `'checkbox-menu'` trigger gains the disclosure caret its neighbours have, which also start-aligns
  its content instead of centring a bold sentence in a stretched field, and its label and caret parts
  are forwarded under the `filter-control-*` scheme.

  A lean registration entry lets a bar pay only for the filter types it declares. The real cost was
  that `filter-bar.class.ts` bare-imported eleven composed control modules it never referenced; those
  are gone, so the class module is genuinely side-effect-free and the default entry still registers
  everything for existing consumers.

  The composed combobox tag's remove button is now forwarded, so a consumer re-skinning filter tags as
  pills can style the control inside them instead of leaving library defaults in a custom pill.
- 71e9296: `<lr-gauge>`'s value/label captions are now sized in `em` (`--lr-size-1em` for the value, and
  `calc(var(--lr-size-1em) * 0.625)` for the label — the same 0.625 multiplier `--lr-font-size-2xs`
  carries, kept as `em` math because the value-named token catalog's growth is frozen) instead of the
  previous `rem`-anchored tokens (`--lr-font-size-m`, `--lr-font-size-2xs`, and, for the `linear`
  shape specifically, a flat `--lr-size-0-5rem`). Every box in this component was already `em`-based
  (`--lr-size-8em` for `radial`/`ring`, `--lr-size-12em` by `--lr-size-1-5em` for `linear`), so a
  smaller `size` tier or a caller's own `font-size` on the host previously shrank the frame while the
  caption stayed pinned to the document root — at the smallest tiers, or below them, the fixed
  caption nearly filled or overflowed the box. The caption now scales with the same font-size that
  sets the frame, so `<lr-gauge shape="linear" size="xs">` (or smaller) is usable as a compact
  dashboard meter. At the default (unset `size`, unmodified ambient font) tier the resolved caption
  size is unchanged.

  New `showValue` property (default `true`, matching today's rendering) lets the decorative
  `part="value"` caption be omitted — `<lr-gauge show-value="false">` — the same way an empty
  `label` already omits the `part="label"` caption. Mirrors `<lr-progress-bar>`'s and
  `<lr-progress-ring>`'s own `showValue` name and meaning; the default differs because a gauge's
  purpose is showing the reading it announces, so hiding it is the opt-out. The accessible value
  (`aria-valuenow`/`aria-valuetext`, the host's computed accessible name) is unaffected either way,
  since the caption was always `aria-hidden`.
- 2435dad: A blocked native form submission — clicking a `type="submit"` control, calling
  `form.requestSubmit()`, or implicit Enter submission — now marks every invalid participant as
  user-interacted, so `:state(user-invalid)` (and `:state(user-valid)` once corrected) matches
  exactly like native `:user-invalid`/`:user-valid` do. Previously only an explicit
  `control.reportValidity()`/`form.reportValidity()` call counted: a submission attempt drives
  `ElementInternals` directly and never calls a control's own `reportValidity()` method, so
  `aria-invalid` correctly went `true` on a blocked submit while the `user-invalid` custom state
  silently stayed unset.

  A control's own silent `checkValidity()` query continues to never count as interaction, however
  invalid the control already is — this is unchanged and is the one path interactive validation is
  deliberately distinguished from.

  Affects every form control that manages `ElementInternals` directly rather than through the
  `FormAssociated` mixin (the mixin itself already got this fix): `<lr-radio>`, `<lr-radio-button>`,
  `<lr-radio-group>`, `<lr-checkbox>`, `<lr-checkbox-group>`, `<lr-switch>`, `<lr-slider>`,
  `<lr-select>`, `<lr-combobox>`, `<lr-token-input>`, `<lr-time-range>`, `<lr-file-input>`,
  `<lr-rating>`, `<lr-model-select>`, `<lr-voice-picker>`, `<lr-locale-picker>`,
  `<lr-graph-query-builder>`, `<lr-tool-param-form>`, and `<lr-rubric-form>`. Every affected
  component's `@cssstate user-valid`/`user-invalid` JSDoc is corrected to document the rule above
  instead of the looser "or a native validity check"/"or an explicit validity report" wording some
  of them previously carried.
- 96c30ac: `<lr-model-select>` and `<lr-voice-picker>` catalog entries accept `disabled`. A disabled row renders
  as a genuinely non-actionable control — no tab stop, no hover or press affordance — and keyboard
  navigation steps over it without stranding focus. Both components share one catalog entry type, so
  the contract cannot drift between them. An entry that does not set it renders exactly as before.
- a80606f: `<lr-color-picker>` palette swatches accept `disabled`, so a swatch can be shown as unavailable
  rather than silently doing nothing when chosen. The swatch renders genuinely disabled, selection
  emits nothing, and keyboard navigation steps past it. A swatch that does not set it renders exactly
  as before.
- d6eb9b1: `<lr-flow-canvas>` nodes accept `disabled`, so a locked, read-only or in-progress node can be shown
  without being activatable. The node's control renders genuinely disabled, activation emits nothing,
  and roving focus steps past it. A node that does not set it renders exactly as before.
- 62db946: `<lr-mention-popover>` items accept `disabled`, so an ineligible mention or command can stay visible
  without being selectable. The row carries `aria-disabled`, activation emits nothing, and
  active-descendant navigation steps over it without leaving focus stranded. An item that does not set
  it renders exactly as before.
- 5f4e877: `<lr-sequence-strip>` items accept `disabled`, so a step that cannot currently be opened stops being
  a reachable control that does nothing. Activation emits nothing and roving focus steps past it. An
  item that does not set it renders exactly as before.
- 694032e: `<lr-suggestion-chips>` suggestions accept `disabled`, so a suggestion that is visible but not
  currently offerable stops being a live, focusable control that does nothing. Activation emits
  nothing and keyboard navigation steps past it. A suggestion that does not set it renders exactly as
  before.
- 3e5b916: Every `registrations.json` row now carries `localeKeys`: the `LyraMessageKey`s reachable by every tag
  that importing it registers. It reuses the reachability walk that already derives the tree-shakeable
  default-string slices — including keys a component reaches only through an indirect lookup table,
  such as `lr-attachment-trigger`'s per-kind trigger and menu keys — rather than scanning for literal
  `localize()` calls. Paired with `getRegisteredLyraLocaleKeys()`, a consumer can now verify that its
  locale catalogue covers everything its rendered components can reach entirely from public, generated
  metadata, with no parsing of `dist/`. The field is additive, so `schemaVersion` stays `1`.
- 3e5b916: `registrations.json` now covers every published registration specifier, not just per-tag aliases.
  The three published integration-bridge specifiers — `components/media/flag/flag-peer.js`,
  `components/viewers/archive-viewer/archive-viewer-register.js` and
  `components/viewers/ebook-viewer/ebook-viewer-register.js` — had no row at all, so a consumer asking
  which tags importing one of them defines still had to walk `dist/`. They now appear in a new
  top-level `integrations` array, each with a `registers` list derived from the module's real
  transitive import closure (including the lazy `import()` a document-format registrar uses once a
  matching file appears), never inferred from the file name.

  Every row, in `entries` and `integrations`, also gains `distModule`: the registration module's own
  published deep specifier. `registrationModule` is a `src/` path that cannot be resolved against the
  tarball; `distModule` can.

  The bridges live in their own array rather than as tag-less rows inside `entries`, because every
  `entries` row has always carried `tag` and a reader keying by it would otherwise receive `undefined`.
  All additions are additive, so `schemaVersion` stays `1`.
- 83cde91: `<lr-streaming-text>`/`<lr-streaming-text-core>` forward the rest of the composed `<lr-markdown>`/
  `<lr-markdown-core>` configuration surface verbatim: `tabSize`, `htmlMode`, `gfm`, `linkTarget`,
  `internalLinkPrefix`, `headingOffset`, `highlightCode`, `headingAnchors`, `math`, and `maxHeight`,
  alongside the already-forwarded `content`, `streaming`, and `languages`. Previously every one of
  these was pinned at the composed element's default and unreachable through the wrapper, so a
  consumer who had deliberately set, for example, `link-target=""` for same-tab links silently got
  `target="_blank"` back after adopting `<lr-streaming-text>`. Each new property defaults to exactly
  the composed element's own default, so leaving all of them unset renders identically to before.
  The composed element still applies its own `rel="noopener noreferrer"` guard whenever a forwarded
  `linkTarget` emits a `target`, and never a bare `opener`.
- 3b1e717: `<lr-table>`'s `editTrigger: 'double-click'` inline cell editor is now reachable by keyboard, closing
  a WCAG 2.1.1 gap: a `'double-click'` column's resting cell is its own `tabindex="-1"` roving-focus
  stop, reachable with ArrowRight/ArrowLeft once the row itself has focus, and `F2` or `Enter` on that
  focused cell opens its editor exactly as a double-click would. `Enter` on the row itself still only
  activates the row. Closing the editor, by commit or by cancel, returns focus to the cell that opened
  it. A new public `editCell(rowKey, columnKey)` method opens the same editor programmatically, for a
  consumer's own key binding or menu action. A table with no `editTrigger: 'double-click'` column
  renders byte-identical markup. `TableColumn.cell` is now optional for an `editTrigger: 'always'`
  column, whose persistent editor renders unconditionally and never falls back to it; every other
  column still requires `cell`.
- f1612c9: `<lr-table>` columns gain `editLabel?: (row: T) => string`, mirroring the existing `editValue`/
  `cellTitle` row callbacks: when defined, it becomes the inline cell editor's accessible name for
  that row instead of the interpolated `tableEditCell` default (`Edit {column}`). The default is
  identical for every row in a column, which is adequate for `editTrigger: 'double-click'` (only one
  editor is ever open at a time) but not for `editTrigger: 'always'`, where every row's editor is a
  permanent, individually focusable Tab stop -- a 50-row column with no `editLabel` exposed 50
  identically named controls to keyboard and screen-reader users (WCAG 2.4.6, 1.3.1). A column that
  omits `editLabel` renders byte-identical output to before.
- 0baf79e: `<lr-table>` gains `rowExpandLabel?: (row, expanded) => string`, the accessible name for one row's
  expand/collapse chevron. Every chevron was named from the same localized `expand`/`collapse` string,
  so a long table exposed dozens of identically named Tab stops with no way to tell the rows apart —
  the same shape as the inline editor's naming gap, on a different surface.

  The default is unchanged, because this component has no row-header notion to derive row context from
  (`rowKey` is an opaque identity, not a label), so a table that does not set the callback renders
  byte-identically.
- acb4266: Added `waitForLyraElement()` and `waitForToast()` to `@aceshooting/lyra-ui/testing`. `toast()`
  registers `<lr-toast>`/`<lr-toast-item>` through a dynamic `import()` on first call, so a
  fire-and-forget `toast(...)` -- the normal application pattern -- leaves the document empty for at
  least one microtask, forcing a downstream test to hand-write a polling loop before it can assert on
  the toast's rendered text. `waitForLyraElement(selector, options?)` is a generic, bounded,
  event-driven awaitable (`MutationObserver` plus `customElements.whenDefined()`, no busy polling) for
  "a matching library element is connected and upgraded," reachable from an optional `root` and
  filterable by an optional `match` predicate; it rejects with a descriptive error after a bounded
  timeout (2000ms default). `waitForToast(match?, options?)` is the named convenience for `<lr-toast-item>`,
  matching by trimmed text or a predicate. Both run in a real browser and in the documented happy-dom
  test environment. `confirm()` was swept too: it registers `<lr-dialog>` synchronously and has no
  equivalent gap, so it gets no new helper.
- 857f2ef: `@aceshooting/lyra-ui/theme-bootstrap.js` -- the static, external no-flash theme script for a
  strict Content-Security-Policy -- now reads `data-lr-theme-storage-key` and
  `data-lr-theme-attributes` (space-separated) from its own `<script>` tag via
  `document.currentScript` at parse time, so an application with its own pre-existing `localStorage`
  key can use the shared static asset instead of inlining a per-app copy generated from
  `createLyraThemeBootstrap({ storageKey })`. Both attributes are optional and validated, failing
  closed to today's baked-in defaults (`'lyra-theme'` and `data-lr-theme`/`data-theme`) on an absent,
  empty, oversized, or malformed value -- including an attribute-name list entry that is not a safe
  `data-*`-shaped token (rejecting `on*` handler names, `style`/`class`/`id`, and anything containing
  whitespace, a quote, `=`, or a control character), a list longer than eight entries or with a
  duplicated entry, an oversized storage key, or a `null` `document.currentScript` (module/async
  misuse). A `<script>` tag carrying neither attribute -- every existing deployment, and every inline
  use of `lyraThemeBootstrap`/`createLyraThemeBootstrap()` -- behaves exactly as before.

### Patch Changes

- 2787a1d: `<lr-button>` now re-evaluates its icon-only geometry when the slotted label's visibility changes
  through CSS alone. Detection ran on `slotchange` and once on the first update, so the most natural
  responsive idiom — a container or media query hiding the label at a narrow width — changed computed
  style with no DOM mutation and nothing re-ran: the button kept full text geometry, padding and
  min-width around an invisible label. It happened to look right only when the element was created
  after the breakpoint was already crossed, which made it read as an intermittent bug.

  A `ResizeObserver` now watches the label wrapper's own box. It deliberately does not watch the host
  or the base part: once icon-only, those take a fixed size, so widening the container would not resize
  them and the button could never flip back — the same one-way trap the fix exists to remove. The
  recompute is deferred to an animation frame, matching `<lr-textarea>`'s auto-grow observer, because a
  microtask-deferred write lands inside the same delivery pass and trips a ResizeObserver loop warning.
- 1059b56: Fixed `custom-elements.json` silently omitting `value`, `name`, `required`, and `disabled` (as both
  members and attributes) for a `FormAssociated` component that inherits the mixin through a
  superclass in a different module rather than declaring it directly -- `lr-number-input` and
  `lr-native-time-input` (both extend `LyraInput`, which owns the mixin). The
  `lr-form-associated-mixin-members` custom-elements-manifest plugin's superclass-chain walk already
  found and back-filled these declarations correctly; the gap was that `scripts/manifest-compact.mjs`
  runs as a separate pass after `cem analyze` and prunes any member/attribute whose `inheritedFrom`
  resolves to an identical entry on the named superclass, which is exactly what the back-filled (and,
  for `value`, CEM's own natively-inherited) entries looked like. Both tags now carry the same
  member/attribute shapes, types, and defaults as every other `FormAssociated` consumer. Manifest
  accuracy only -- runtime behavior was already correct.
- 0b207cb: Docs: every component whose icon-only action became a composed `<lr-icon-button>` in 16.0.0 now
  carries the migration note that only `lr-copy-button` had. A `::part()` rule that used to paint that
  button now names the composed child's host, which paints nothing — and because `color` still
  inherits, such a rule looks half-alive rather than broken, which is how it escapes review. The note
  says what to do instead: set the `--lr-icon-button-*` paint tokens on the component or an ancestor,
  and use `--lr-theme-icon-button-size` for size, since `--lr-icon-button-size` is re-declared on every
  host and never reaches a composed child.

  Also pins the border half of that contract with a test: an ancestor `--lr-icon-button-border` does
  reach a composed control, so border is not the one paint property that silently dies.
- ef05323: Fixed `<lr-confirm-bar>`'s `returnFocusTo` thunk resolving before a reactive host could re-create
  the control it names. The documented motivating case -- a host that conditionally swaps a focused
  trigger out for this bar, then swaps a brand-new trigger back in once a decision lands -- could
  never work: every supported host framework re-renders asynchronously relative to the bar's own
  synchronous focus handoff, so the thunk's first (and, previously, only) call always found the
  replacement control missing and fell back past `[part="status"]` to `<body>` once the host's own
  re-render removed the bar.

  When the thunk's immediate resolution fails, the handoff now retries once more after the host has
  had a real chance to react, and moves focus there only if it has since appeared, is connected and
  focusable, and nothing else has claimed focus in the meantime. An immediately-resolving thunk or a
  plain element value is unaffected -- resolved once, synchronously, exactly as before.

  The retry is a new shared primitive, `deferComposedFocusRepair()` (plus its `nextHostUpdateOpportunity()`
  timing helper), in `src/internal/focus-navigation.ts`, built on the existing
  `captureComposedFocusRepair()`/`applyComposedFocusRepair()` pair so any other component with the same
  shape -- a return-focus thunk naming a control its host re-creates asynchronously -- can adopt it
  directly.
- 71e9296: Fixed: `<lr-context-meter shape="ring">`'s centered caption (`.ring-label`, inside `part="label"`)
  now sizes itself in `em` (`calc(var(--lr-size-1em) * 0.625)`) instead of the previous
  `rem`-anchored `--lr-font-size-2xs`. The ring itself is already `--lr-size-8em`, so a caller's own
  `font-size` on the host shrinks the ring but previously left the caption pinned to the document
  root — the same defect shape `<lr-gauge>`'s linear caption had. At the default (unmodified ambient
  font) size the resolved caption size is unchanged.
- 63ee11b: `<lr-data-grid>` no longer treats an unchanged re-bind of `filters` or `sort` as a change. Both
  setters rebuilt a fresh frozen array on every write, so the new reference was never `===` the old one
  and Lit's dirty check always reported a change. Because a `filters`/`sort` change schedules a server
  request — and a `filters` change schedules the *debounced* one — a host that re-binds these
  properties on every render (the ordinary controlled pattern, often driven by the grid's own events)
  could push the server request further away indefinitely, and re-rendered for nothing in the meantime.

  The setters now compare the normalized content and keep the held value, including its reference, when
  nothing actually changed. A genuinely different value still schedules exactly as before.
- 36f83ca: `<lr-input>`'s and `<lr-textarea>`'s `debounce` no longer silently stops working under the
  controlled-input pattern. A pending `lr-input-settled` was cancelled by any programmatic `value`
  write, including the one every framework binding makes on each render when it writes the just-typed
  value straight back, so the event simply never fired and nothing warned. Only a write that actually
  changes the value now cancels the pending settle; a write of the value already held leaves it
  pending. A genuinely different value still supersedes the in-flight edit, exactly as before.

  The fix lives in the shared `DebounceController` (a new `cancelIfChanged()`), so every debounced
  control that routes external writes through it gets the same contract.
- 622e12c: `formatNumber`, `formatDate`, `formatRelativeTime`, `formatBytes` and the exported `binValues()` now
  treat an omitted `locale` as the app's active locale instead of hardcoding English. They passed the
  argument straight to the Intl cache, whose resolver starts at `'en'`, so an app that called
  `setLyraLocale('fr')` saw every component render French while these standalone helpers silently stayed
  English — and it looked correct in any English-locale test run.

  An explicit `locale` argument still wins, and an app that never calls `setLyraLocale()` gets
  byte-identical output: the resolution consults only the pinned active locale and deliberately does
  not fall through to `<html lang>` or `navigator.language`, since a bare function call has no host
  element to resolve against.
- 6130112: Fix: the seven built-in controls that compose `<lr-icon-button>` for an icon-only action
  (`<lr-callout>`, `<lr-dialog>` — inherited by `<lr-drawer>` — `<lr-code-block>`, shared by
  `<lr-code-block-core>`, `<lr-message-actions>`, `<lr-reorder-item>`, `<lr-attachment-trigger>`, and
  `<lr-copy-button>`) no longer capture a public `--lr-icon-button-*` token on their own `:host` and
  re-declare that same public name on the composed part. `<lr-icon-button>` now carries a private
  `--_lr-icon-button-<token>-default` fallback tier for every paint token (background/color/border and
  their hover/active variants), generalizing the existing `--_lr-icon-button-radius-default` shape;
  each composing component sets its own default directly on that private tier instead of the public
  one. An ancestor `--lr-icon-button-*` override still wins exactly as before — `<lr-icon-button>`'s
  own stylesheet checks the public token first, ahead of any default a composing parent supplies — but
  no descendant declares the public name from a private token derived from that same public token
  anymore, so a scope-flattening custom-property resolver with no notion of which element declared
  what (happy-dom, at least through 20.14.5) no longer sees a cycle: rendering any of the seven
  controls under such an environment no longer throws `RangeError: Maximum call stack size exceeded`.
  Revises the `llms/shared.md` testing note added in 16.0.0 to match — current versions are
  unaffected. No public API changed.
- 2435dad: Verified and documented that `--lr-icon-button-border` reaches every 16.0.0 composed icon action
  (`<lr-dialog>`'s close control, `<lr-reorder-item>`'s move controls, `<lr-code-block>`'s copy
  control, `<lr-attachment-trigger>`'s trigger) the same way `--lr-icon-button-background`/`-color`/
  `-radius` do. A report read the absence of a relayed `--_lr-icon-button-border-default` on
  `<lr-dialog>`/`<lr-reorder-item>`/`<lr-code-block>` (none of which paint a resting border) as
  border theming being broken for those components. Rendered `getComputedStyle` assertions now cover
  all four components, including `<lr-attachment-trigger>`'s `outlined`/`filled-outlined`
  appearances, which DO relay a non-zero border default and must still let an ancestor's public
  token override it. No source change was needed — the public token was already the first, winning
  arm of the fallback chain in every case; only test coverage and the authored `llms/` reference
  pages were missing an explicit statement of the rule.
- af075a9: Docs: corrected the token-scope contract for `--lr-icon-button-size`, `--lr-otp-input-segment-size`,
  and `--lr-popover-viewport-clamp`. All three read like ordinary per-component
  `--lr-<component>-*` tokens, but the shared base token layer (`internal/tokens.styles.ts`) declares
  each of them on its own `:host` block, which every `lr-*` component includes -- so, like
  `--lr-focus-ring-width`/`-color`/`-offset`, a rule that sets one of them on an ancestor is reset at
  the first intervening `lr-*` component and never reaches a nested target. This was always the
  behavior (and is already asserted by `internal/tokens.test.ts`); the docs previously implied all
  `--lr-icon-button-*`/`--lr-otp-input-*` tokens inherit uniformly from an ancestor, which is true for
  every other one of them (e.g. `--lr-icon-button-radius`/`-background`) but not these. Updated
  `lr-icon-button`'s and `lr-otp-input`'s own `@cssprop` JSDoc, the icon-button and otp-input entries
  in `llms/forms.md`, and the design-token overview built by `scripts/build-llms.mjs`
  (`llms/tokens.md`) to state the general rule once and name the affected tokens; added test coverage
  proving the asymmetry against a nested shadow root. No runtime behavior changed and no token was
  renamed.
- 616e6d6: Twenty components derived state from their slotted children only through a `slotchange` listener, so
  they depended on the environment firing that event for a slot's INITIAL assignment. A DOM
  implementation that does not — happy-dom, through 20.14.5 — leaves that state empty forever when the
  children already exist at connect, which is what the ordinary conditional-render pattern produces.
  Most visibly, an `<lr-select>`/`<lr-combobox>` whose `<lr-option>` children are rendered in the same
  commit had zero options there, so the shipped `chooseOption()` testing driver threw, contradicting
  the testing entry's own cross-environment promise.

  Each affected component now also collects once on first update, from the slot's assigned elements,
  through a shared helper. The collection is idempotent, so a real browser firing the initial event as
  well changes nothing. Also fixed along the way: `lr-menu`'s collection was not idempotent (a second
  pass misread the settled active item as having moved and stole focus), and `lr-chart` could have an
  already-collected slotted config clobbered back to undefined by a phantom `slotchange` fired at the
  slot element discarded during its loading-to-loaded render swap.

  About eighty other components were checked and already seed their slot-derived state eagerly; they
  are unchanged.
- 9c19aed: The cascading `--lr-positioning-strategy` custom property (`src/internal/positioning-strategy.ts`)
  was documented as read by "every anchored/positioned overlay in this library", but until now only
  `<lr-select>`, `<lr-popover>` (and `<lr-dropdown>` through it), `<lr-tooltip>`, `<lr-color-picker>`
  and `<lr-combobox>` actually routed their placement through the shared resolver. An app that set the
  property once on `:root` to retune every floating surface silently left every other anchored surface
  behind.

  `<lr-menu>` (the private submenu surface), `<lr-mention-popover>`, `<lr-export-button>`,
  `<lr-usage-badge>`, `<lr-tool-call-chip>`, `<lr-tour>`, `<lr-locale-picker>`, `<lr-date-input>`,
  `<lr-time-input>`, `<lr-citation-badge>`, `<lr-entity-chip>`, and `<lr-app-rail-item>` now honour the
  same cascading property. None of them exposes a per-instance `positioning-strategy` property — that
  remains a separate, deliberate decision — so an ancestor override is the only way to change them, and
  their own default stays exactly what it always rendered (`fixed` in every case), so nothing moves for
  anyone who sets nothing.

  `<lr-popup>`, the low-level positioning primitive, is deliberately excluded: its own `strategy`
  property is a plain, always-defined value (default `'absolute'`, never `undefined`), so there is no
  way to distinguish "left unset" from "authored the default" without adding the same
  explicit-vs-default tracking machinery the higher-level components' `positioning-strategy` property
  uses — which this sweep does not add anywhere. It is also meant to be composed directly by a consumer
  who already controls `strategy` explicitly.
- 2435dad: Added `pnpm run regen`, a single command that runs every source-artifact generator (manifest,
  component inventory, framework types, events, the testing event registry, component metadata,
  registrations/tag-aliases, the autoloader manifest, the registration graph, default-string and
  translation slices, the three palette generators, design tokens, reservation styles, editor data,
  and llms) in real dependency order, so a batch of source changes no longer needs a human to
  remember and re-run each generator individually before `pnpm lint`. Added `scripts/check-regen-
  coverage.mjs` (wired into `contract-policy` as `check:regen-coverage`/`test:regen-coverage`), a
  gate that derives which generators `regen` must reach directly from the freshness gates themselves
  — a same-file `--check`/write argument pair, a gate's own `pnpm run <name>` remedy text, or a gate
  that imports/references a generator file directly — rather than a hand-kept list, so a newly added
  or renamed generator cannot silently fall out of `regen` again. No runtime behavior changed;
  `pnpm run regen` on an already-fresh tree is a no-op.
- 74a887b: `<lr-table>` no longer gets stuck with `priority`-hidden columns after the table widens back out.
  Narrowing correctly hid `low`/`medium` columns once their content actually overflowed, but widening
  never restored them: `recomputeHiddenPriorityColumns()` reconstructed "how wide would everything be"
  by adding a hidden tier's cached natural width to `[part='base']`'s CURRENT `scrollWidth` — and once a
  tier was hidden, `[part='table']`'s own `inline-size: 100%` stretched the remaining columns to fill
  any leftover room, so that `scrollWidth` tracked `clientWidth` instead of the remaining content's real
  width. The reconstructed total then chased whatever the container currently measured rather than the
  actual content, so `overflowAtFull` never dropped enough to re-admit a hidden tier — `reload` or
  `priorityColumnsVisible = true` were the only ways back, because both bypass the measurement entirely.

  The fully-visible width is now reconstructed from three independently cached natural widths (the
  always-visible columns, plus each hidden tier) instead of from the currently-rendered, potentially
  stretched `scrollWidth`. Every cache is refreshed only on a pass where the table is genuinely too wide
  for its container — the one condition under which nothing rendered has spare room to stretch into, and
  necessarily true on the pass that first decides to hide anything — so a widening container now
  correctly restores every tier that fits, without the restored tier immediately re-hiding on the very
  next measurement pass its own restoration triggers.
- 2435dad: Fixed `<lr-input>`'s required-field description, `<lr-task-list>`'s per-item status label,
  `<lr-transcript-feed>`'s interim "Transcribing…" marker, and `<lr-filter-bar>`'s touched-required
  inline error and hidden filter-control label: all four rendered `class="sr-only"` without
  composing the shared `srOnly` style block from `internal/a11y.ts` into their own
  `static override styles`, so the class name did nothing and the assistive-only text rendered as
  ordinary visible content instead of being clipped to a 1px box. Each component now composes the
  shared `srOnly` export, matching the reference shape in `time-input.class.ts`.

  Added `scripts/check-visually-hidden.mjs` (with its own `scripts/check-visually-hidden.test.mjs`
  unit tests), a new static gate wired into `contract-policy`/`pnpm lint` as
  `check:visually-hidden`/`test:visually-hidden`. It flags any `*.class.ts` file whose render
  template applies the `sr-only` class without either composing the shared `srOnly` export or
  declaring a matching scoped `.sr-only { ... }` rule in its own sibling `*.styles.ts`, so this
  class of defect fails CI instead of shipping silently. Added browser regression tests for all four
  components asserting the rendered `getComputedStyle()` result (clipped, absolute-positioned, with
  the accessible text still present in the DOM), plus explicit `<lr-input>` coverage of the pristine
  (no visible required text, error part hidden) and failed-submission (error becomes visible) states.

## 16.0.0

### Major Changes

- 9f0179a: Every built-in icon-only action now composes a real `<lr-icon-button>` instead of re-deriving its
  CSS.
  
  Seven controls each carried their own hand-rolled copy of the same thing: a bare `<button>` with its
  own background, radius, hover/press mix, focus ring, disabled dimming and `--lr-icon-button-size`
  floor. They drifted — different radii, different hover fills, two of them with no press state that
  matched their hover. They now render `<lr-icon-button>`, so all of that comes from one component
  and one token contract, and setting `--lr-icon-button-background` (or `-color`, `-radius`,
  `-border`, and their `-hover`/`-active` variants) on an ancestor reaches every one of them exactly
  as it reaches a standalone icon button. Each component keeps its own resting/hover opinion as the
  FALLBACK arm of those tokens, so an ancestor override wins rather than being shadowed.
  
  **Migration — internal part targets moved one shadow boundary deeper.** The old part name still
  resolves to a node, but that node is now the composed `<lr-icon-button>` host, which owns the
  accessible name, the activation API and the part names while the *painted* surface is its internal
  control. A rule that set `background`/`border`/`padding`/`outline` through the old part must move to
  the newly forwarded control part, or (better) to the token:
  
  | Component | Old target | Now |
  |---|---|---|
  | `<lr-copy-button>` | `::part(base)` / `::part(button)` | `::part(base__control)` |
  | `<lr-dialog>` (and `<lr-drawer>`, which inherits it) | `::part(close-button)` / `::part(close-button__base)` | `::part(close-button__control)` |
  | `<lr-reorder-item>` | `::part(move-up-button)` / `::part(move-down-button)` | `::part(move-up-button__control)` / `::part(move-down-button__control)` |
  | `<lr-message-actions>` | `::part(regenerate-button)` / `::part(edit-button)` | `::part(regenerate-button__control)` / `::part(edit-button__control)` |
  | `<lr-attachment-trigger>` | `::part(trigger)` / `::part(menu-trigger)` | `::part(trigger__control)` / `::part(menu-trigger__control)` |
  | `<lr-code-block>`, `<lr-code-block-core>` | `::part(copy-button)` | `::part(copy-button__control)` |
  | `<lr-callout>` | `::part(close-button)` | `::part(close-button__control)` |
  
  Concretely, a consumer rule that used to paint the dialog's close button:
  
  ```css
  /* BEFORE 16.0.0 -- silently paints nothing now: the node it selects is the
     <lr-icon-button> host, and the background belongs to the control inside it. */
  lr-dialog::part(close-button) {
    background: var(--brand-050);
    border-radius: 4px;
  }
  
  /* AFTER, option 1 -- the forwarded control part. Same specificity, same file. */
  lr-dialog::part(close-button__control) {
    background: var(--brand-050);
    border-radius: 4px;
  }
  
  /* AFTER, option 2 (preferred) -- the token contract, which also gives you the
     hover/press states the hand-rolled rule had to restate, and reaches every
     composed icon action in the subtree at once. */
  lr-dialog {
    --lr-icon-button-background: var(--brand-050);
    --lr-icon-button-radius: 4px;
  }
  ```
  
  Layout, placement and the accessible name did NOT move: the old part still selects the node that
  carries `aria-label`, `part=`, the click target and the grid/flex placement, so a rule that only
  sets `margin`, `grid-column`, `order` or `display` keeps working untouched. In JavaScript,
  `shadowRoot.querySelector('[part~="close-button"]')` still returns the control you want to
  `.click()` or `.focus()` — both are forwarded — but its `localName` is now `lr-icon-button`, and
  reading a painted value off it (`getComputedStyle(node).backgroundColor`) needs
  `node.shadowRoot.querySelector('[part~="button"]')`.
  
  **Size is the one token that does NOT follow that rule.** `--lr-icon-button-background`/`-color`/
  `-radius`/`-border` inherit down to a composed action, but `--lr-icon-button-size` set on the
  wrapping component does not reach it, and `min-inline-size`/`min-block-size` written on the old
  part cannot push the nested control past its own floor — so a pre-16 rule that sized one of these
  controls, including a coarse-pointer touch-target pair, is inert with no visible symptom. Use
  `--lr-theme-icon-button-size` instead; it is the hook that crosses into the composed child. This
  applies to every component in the table above, not only `<lr-copy-button>`, whose reference page
  was previously the only place it was written down.
  
  **Not affected by this change.** Other icon-sized built-in buttons deliberately stay native
  `<button>`s in 16.0.0, so every rule you already wrote against them still applies: `<lr-alert>`,
  `<lr-toast-item>`, `<lr-lightbox>` and `<lr-tool-result-dialog>` close buttons; `<lr-chip>`,
  `<lr-tag>`, `<lr-attachment-chip>` and `<lr-token-input>` remove buttons; `<lr-json-viewer>`,
  `<lr-diff-view>` and `<lr-terminal>` copy/download buttons; and every disclosure toggle, including
  `<lr-code-block>`'s own `::part(toggle)` and `<lr-chat-message>`'s `::part(collapse-button)`.
  `<lr-chat-message>`'s `::part(retry-button)` is a labelled, bordered control rather than an
  icon-only action and is likewise unchanged.
  
  `<lr-code-block>`'s copy control additionally carries `copy-button-text` or `copy-button-icon` for
  its active appearance, so `::part(copy-button)` written as an exact-value selector in your own
  tooling must become a token match.
  
  Alongside the composition:
  
  - **`<lr-attachment-trigger>` gains `appearance` and `size`,** which it had neither of.
    `appearance` is the library's shared `accent`/`filled`/`outlined`/`filled-outlined`/`plain`
    vocabulary, defaulting to `plain` — byte-identical to the treatment it shipped before. `size` is
    the shared six-step ladder (accepting the `small`/`medium`/`large` spellings too) and scales the
    glyph; the tappable box deliberately stays on the `--lr-icon-button-size` accessibility floor at
    every tier, because the ladder's tightest steps resolve below WCAG 2.5.8's minimum.
  - **`<lr-code-block>`/`<lr-code-block-core>` gain `copy-appearance` and a `header-actions` slot.**
    `copy-appearance="icon"` swaps the copy control's visible label for a compact glyph and promotes
    the same localized Copy/Copied/failure string to its accessible name; `'text'` (the default) is
    unchanged. `header-actions` takes extra controls at the trailing end of the header row, and its
    content alone is enough to render the header — appending or removing such a child at any time
    brings the header into existence or retires it, with no property write needed. Its
    `::part(header-actions)` wrapper carries the `hidden` attribute (and computes to
    `display: none`) whenever nothing is assigned, so an empty slot contributes no header gap; a
    consumer rule that sets `display` on that part must qualify itself with `:not([hidden])`.
  - **`<lr-icon-button>` gains two capabilities the composition needed,** both useful standalone:
    `aria-labelledby` on the host is resolved onto the internal control through
    `ariaLabelledByElements` (an idref cannot cross a shadow boundary; the reflected element
    reference can), and `getToolbarActions()` contributes the button as one logical action to
    `<lr-message-actions>` and any other `LyraToolbarAction` toolbar. Without the latter a
    roving-tabindex owner had no way to manage an icon button at all: writing `tabindex` on a
    custom-element host neither adds nor removes its shadow-internal button's tab stop, so a slotted
    icon button either stayed permanently tabbable or dropped out of the toolbar's stop list.
    A public `control` accessor returns that internal element for the same idref-projection reason
    `<lr-virtual-list>` exposes `scrollContainer`.
  - **`<lr-icon-button>`'s internal control now sets `font: inherit`.** Slotted content inherits
    through the flattened tree, so an `<svg width="1em">` slotted into an icon button was taking the
    native button's UA font-size (13.33px at a 16px root) rather than the surrounding text's — every
    `em`-sized slotted glyph silently rendered smaller than intended.
  - **`<lr-reorder-list>`'s Ctrl/Cmd+Arrow move.** Its "did this key press come from a consumer's own
    nested control?" test exempted only a native `<button>` in the row's own shadow root, so composing
    the move controls would have made every move key press look like it came from consumer content
    and be discarded. It now exempts anything the row rendered itself, which is the property it
    actually meant: consumer content arrives through a slot and is never in that root.
  - **`<lr-drawer>` now registers `<lr-icon-button>`.** It extends `<lr-dialog>` and therefore
    inherits its close control; a consumer importing only `drawer.js` would otherwise have rendered
    an inert, never-upgrading element.
- 9f0179a: `<lr-button>`'s label no longer grows to fill a stretched button.
  
  `[part="label"]` was `flex: 1 1 auto`. In a button wider than its content that made the label
  absorb every spare pixel, so a stretched button with a `start` icon and a short label rendered the
  icon hard against the leading edge and the text floating in the middle of a wide empty row — the
  native `<button>` UA stylesheet centres text, which the label wrapper inherited. The gap between
  icon and text read as arbitrary rather than as the `--lr-button-gap` token.
  
  The label is now `flex: 0 1 auto` with `min-inline-size: 0`, so it shrink-wraps its text and the
  whole icon+label pair centres as one unit; the icon-to-text distance is exactly `--lr-button-gap`.
  It also now sets `text-align: start`, which fixes two more things the inherited centring caused: a
  label narrower than its own text centred the overflow, so the ellipsis appeared at the end while
  the START of the word was silently clipped, and the `<a>` root (which never inherited the centring)
  disagreed with the `<button>` root across a mode switch.
  
  Growing is now conditional: a `with-caret` button, and one with an `end`/`suffix` adornment, keep
  the growing label so that trailing affordance stays pinned to the trailing content edge, which is
  what a dropdown trigger needs.
  
  **Migration.** Affected: a `<lr-button>` laid out WIDER than its own content — an explicit
  `width`/`--lr-button-width`, a `display: block`/`width: 100%` wrapper, a `flex: 1` or a grid cell —
  that has a `start`/`end` adornment or a slotted icon, and no `with-caret`. Such a button now centres
  icon+label as one unit instead of stretching the label across the row. Unaffected: any button
  sized by its own content (the overwhelmingly common inline case) renders byte-identical, as does
  any `with-caret` or `end`-adornment button, which keeps the grown label.
  
  Restore the old layout per-instance, or globally, with one declaration:
  
  ```css
  /* BEFORE 16.0.0: [part="label"] was flex: 1 1 auto, so this rendered
     [icon][........ label ........] with the text floating mid-row. */
  .toolbar lr-button {
    width: 100%;
  }
  
  /* AFTER, to keep exactly that: re-grow the label. */
  .toolbar lr-button {
    width: 100%;
    --lr-button-label-grow: 1;
  }
  
  /* Or keep the new shrink-wrapped pair and just place it. */
  .toolbar lr-button {
    width: 100%;
    --lr-button-justify: flex-start; /* space-between, end, … */
  }
  ```
  
  `--lr-button-label-grow` also overrides the automatic grow in the other direction: setting it to
  `0` opts a `with-caret` / `end`-adornment row OUT of pinning its trailing affordance to the content
  edge. `--lr-button-justify` replaces a hard-coded `justify-content: center`, so before 16.0.0 there
  was no supported way to reposition the row at all.
  
  Two additions land with it:
  
  - **`wrap`** — a `false`-defaulting boolean that wraps a long label onto multiple lines instead of
    ellipsis-truncating it to one, the same opt-in (and the same four declarations) `<lr-chip>`
    already ships. Unset, `[part="label"]` keeps its single-line, ellipsis-truncated rule exactly.
  - **Icon-only detection ignores a visually hidden label.** `<lr-button>` detects an icon-only
    default slot and applies the square, `--lr-icon-button-size`-floored treatment. It counted a
    visually hidden label as content, so the library's own recommended way to name an icon-only
    action — an icon plus an `.sr-only` span — rendered as a wide labelled button with a blank second
    column. `<lr-visually-hidden>`, `hidden`/`display: none`/`visibility: hidden`, and the standard
    absolutely-positioned `clip-path: inset(50%)` algorithm are all recognised from computed style,
    so a consumer's own utility class works whatever it is called.
- 9f0179a: `<lr-confirm-bar>` can now resolve a decision from a promise, and announces when a decision has
  finished rendering. Breaking: `lr-approve`/`lr-deny` carry a new detail field to do it.
  
  - `lr-approve` and `lr-deny` now carry `waitUntil(promise)` in their detail, ExtendableEvent-style.
    Calling it from the listener puts the bar into its `pending` presentation (`loading` on the
    activated control, `disabled` on the other) and the promise's settlement drives the rest: a
    resolution finalizes `decision`, a rejection restores the undecided state and returns focus to the
    control that can retry it. Several `waitUntil()` calls, from one listener or from several, are
    awaited together. This replaces — but does not remove — the imperative dance of calling
    `preventDefault()`, casting `event.currentTarget` to the component type, writing `pending`, and
    then writing `decision` or clearing `pending` by hand; that path still works exactly as before.
    `waitUntil()` called after its own dispatch has finished does nothing and warns in dev mode.
  - A listener that resolves the decision itself synchronously still wins outright over both paths,
    `waitUntil()` included. Writing `decision` or `pending` from inside the listener means the
    listener owns the outcome, and the bar applies no bookkeeping of its own.
  - New `lr-decision-settled` event, `detail: { decision }`, non-cancelable. It fires after the decided
    `[part="status"]` has rendered and its live-region announcement has been made, on every path that
    reaches a decision — the bar's own, a `waitUntil()` settlement, and a host writing `.decision`
    directly. A host that swaps the bar out for its own result UI can now do it on this event instead
    of having to know that awaiting one `updateComplete` is not enough. A `decision` present in the
    initial markup still announces and settles nothing: it never transitioned.
  - `lr-deny`'s detail changes from `null` to `{ waitUntil }`, and `lr-approve`'s from `{ args }` to
    `{ args, waitUntil }`. `args` is unchanged, and `lr-deny` still carries no denial data of its own.
    A listener that asserted on the whole detail object (`detail === null`, or a deep-equality check
    against `{ args }`) needs updating to read the fields it actually uses.
- 9f0179a: A `checkbox-menu` filter type for `<lr-filter-bar>`, the rest of its definition passthrough, and
  `<lr-data-grid>`'s all-columns visibility panel rebuilt on the same composition.
  
  - `<lr-filter-bar>` gains a `'checkbox-menu'` filter type: a toolbar button that opens
    `<lr-dropdown>` plus one `<lr-dropdown-item type="checkbox">` (`role="menuitemcheckbox"`) per
    option, staying open across toggles so several categories can be switched in one visit. Its
    value is a `string[]`, identical to a `'combobox'` with `multiple`, so it joins the same `value`
    record, active-filter chips, `reset()` path, `required` validation and single full-value
    `lr-input` event as every other filter — which is exactly what a hand-assembled dropdown inside
    a `type: 'custom'` filter could not do. Rows are controlled by `value` rather than self-toggling,
    so a refused toggle can never leave a checkmark the bar disagrees with. Pick it over a combobox
    when the set is small and fixed and typing to filter would only be in the way.
  - Filter definitions finish their passthrough to the control each one composes. `size`, `icon`
    (an inert, `aria-hidden` leading adornment) and the new `labelVisibility` now apply to every
    built-in type — `'select'` and `'date'`/`'date-range'` included, where the already-exported
    `filter-control-start` part had no way to be populated — and `clearable` applies to every type
    whose control ships a clear action, reaching `<lr-date-input>` under its own `with-clear`
    spelling. `'combobox'` adds `emptyText` for its no-matches row, and
    `LyraFilterBarOption.searchText` lets a `'combobox'` filter's short visible label still match a
    long canonical key (`'combobox'` only — `<lr-select>`'s type-ahead matches on the option label
    alone and never reads `search-text`).
    Everything stays optional and defaults to the composed control's own default, so existing filter
    definitions render byte-identically.
  - `labelVisibility: 'hidden'` routes a filter's `label` to the composed control's own `aria-label`
    and — when the definition declares no `placeholder` of its own — to its placeholder, instead of
    rendering a stacked label above it. The label is re-routed, never dropped, so a compact toolbar
    row still names every field for assistive technology; the previous workaround was visually
    hiding `::part(filter-control-label)` in CSS, which removed the name along with the text.
  - `<lr-data-grid>`'s `with-columns-menu` panel is now that same composition instead of a
    hand-rolled toggle button wrapping an inline `role="group"` of native checkboxes. It gains
    `role="menuitemcheckbox"` rows, roving focus and type-ahead, and its Escape and focus-return
    behaviour now comes from the shared overlay manager through `<lr-dropdown>` rather than from the
    grid's own overlay helper. **Potentially breaking for styling and tests:** `::part(columns-menu)`
    now resolves to the `<lr-dropdown>` itself, so a rule or query written against the former
    structure (`::part(columns-menu) > button`, a descendant `input[type="checkbox"]`) no longer
    matches. The `withColumnsMenu` property, the `lr-column-visibility-change` event and the
    `columns-menu` part name are unchanged. A grid detached and reattached while that menu was open
    now comes back closed, matching every other transient panel it owns.
- 34cdefb: Every floating surface in the library now paints from one shared overlay token family, so a
  consumer restyles every popup from one place.
  
  `--lr-overlay-surface`, `--lr-overlay-border` and `--lr-overlay-radius` are read by `<lr-popover>`'s
  and `<lr-dropdown>`'s popup (and its arrow, fill and edge only — never the radius, whose corners its
  clip path already cuts), `<lr-select>`'s and `<lr-locale-picker>`'s listbox, `<lr-combobox>`'s
  popup, `<lr-menu>`'s own surface and its submenu surface, and `<lr-dialog>`'s and `<lr-drawer>`'s
  panel, including the dialog's header and footer rules and its close button's corner. Every other
  anchored surface in the library moved with them, so the family really is one place: `<lr-mention-popover>`'s
  and `<lr-voice-picker>`'s and `<lr-model-select>`'s listbox, `<lr-color-picker>`'s panel,
  `<lr-export-button>`'s menu popup and `<lr-selection-toolbar>`'s toolbar. Elevation
  stays two names rather than one, because it is the one property of a floating surface that differs
  by kind rather than by theme: `--lr-overlay-shadow-anchored` (default `var(--lr-shadow-m)`) for
  positioner-placed popups, listboxes, menus and submenus, and `--lr-overlay-shadow-modal` (default
  `var(--lr-shadow-xl)`) for modal panels — so raising popups never raises dialogs.
  
  None of the five is declared on `:host`. That is what makes the family a real cascade point: an
  undeclared custom property inherits, so one declaration on `:root` retints every overlay in the
  application, and the same declaration on any other ancestor scopes the retint to that subtree.
  Before this, the surface tokens each popup read were declared per `:host` by the token layer, so
  "a popup surface is not a control surface" could not be expressed at all — retinting a popup meant
  retinting every card, panel and input behind it.
  
  A component that already publishes its own hook keeps it as the outer arm: `<lr-locale-picker>`'s,
  `<lr-voice-picker>`'s, `<lr-model-select>`'s and `<lr-color-picker>`'s surfaces each resolve
  `var(--lr-<component>-radius, var(--lr-overlay-radius, …))`, so a component-scoped override still
  wins over the shared name.
  
  **Breaking — anchored popups move onto the overlay surface, a visible dark-mode change.** An
  anchored popup, listbox or menu surface previously painted `--lr-color-surface`, the page surface;
  it now paints `--lr-color-surface-overlay`, the surface a panel floating over the page uses. In
  light mode the two resolve to the same value and nothing changes. In dark mode the overlay surface
  is a distinctly lighter near-black, so a popup that used to be the same colour as the page behind it
  now reads as a raised object — which is the point, but it *is* a visible change. To restore the
  previous colour, set `--lr-overlay-surface: var(--lr-color-surface)` on `:root`.
  
  `<lr-tooltip>` is a deliberate exclusion: a tooltip bubble is a high-contrast label, not a panel, so
  it keeps `--lr-tooltip-background`/`--lr-tooltip-color`, no border, and its tighter corner.
  `<lr-drawer>` reads the family's fill and edge but keeps squaring its own corners and stepping its
  own elevation down, because three of its edges are flush with the viewport.
  
  `<lr-menu-item>` also gains four row-chrome hooks, each an inline fallback so unset rendering is
  byte-identical: `--lr-menu-item-hover-bg` (default `var(--lr-color-brand-quiet)`, and the pressed
  state mixes from that same value by default), `--lr-menu-item-active-bg` (default the same
  hover-derived mix — `color-mix(in oklab, var(--lr-menu-item-hover-bg, var(--lr-color-brand-quiet)),
  var(--lr-color-mix-partner) var(--lr-color-mix-active))` — for setting the pressed fill directly,
  independently of a retuned hover fill, matching `--lr-option-active-bg`'s equivalent hook),
  `--lr-menu-item-icon-color` (default `inherit`) and `--lr-menu-item-min-height` (default
  `max(var(--lr-form-control-height), var(--lr-size-24px))`).
  
  `<lr-dialog>`'s reference entry previously listed `--lr-color-surface`, a bare `--lr-shadow` and
  `--lr-easing-standard` among the shared tokens its panel reads. None of the three has ever appeared
  in a declaration in the dialog's stylesheet; the entry now states the overlay-family tokens and the
  shared tokens the panel and its chrome really consume. `<lr-mention-popover>`'s entry carried the
  same invented `--lr-shadow`, and is corrected the same way.
- 9f0179a: `<lr-sequence-strip>` now summarises past its render cap instead of stretching a window across it,
  and `<lr-span-waterfall>`/`<lr-trace-tree>` stop rescaling their time axis to a truncated trace.
  
  - **`<lr-sequence-strip>` renders differently above 200 items.** It previously mounted a 200-item
    window around the roving stop and let those cells stretch to the strip's full width, so a
    600-item sequence drew as if it were 200 items — the visible span was a third of the real one and
    nothing on screen said so. Each cell is now a contiguous **range** of items, painted by that
    range's dominant category (a tie goes to the category appearing earliest in the range) and marked
    when any item inside it sets `marker`. The 200 cells always tile the whole sequence, so the strip
    covers the full span at full width and scrolling horizontally is never needed. At or below 200
    items nothing changes: every cell is still exactly one item.
  - **Keyboard, ARIA and selection address ranges, not items.** ArrowLeft/ArrowRight and Home/End
    step one range at a time (still direction-aware under RTL), `aria-posinset`/`aria-setsize` count
    ranges, and the cell whose range contains `selectedIndex` carries `aria-current="true"` and
    `data-selected`. Activating a range — by click or Enter/Space — emits `lr-item-activate` for that
    range's **first** item, so a playback consumer receives a real sequence index it can scrub from.
    The event's `detail` shape is unchanged.
  - **Migration.** `[part="window-range"]` is gone; it disclosed a projection window this component
    no longer has. `[part="bucket-summary"]` replaces it on the same spot below the strip, reading
    "600 items in 200 ranges" (localized). If you styled `::part(window-range)`, rename the selector.
    Each cell also gains `data-range-start` / `data-range-end` (zero-based, inclusive item indices),
    and code that read a cell's `data-index` as an item index must read those instead — above the cap
    `data-index` is the range's position, not an item's. Two localization keys are new:
    `sequenceStripBucketLabel` (`"{label}, items {start} to {end}"`) and `sequenceStripBucketSummary`
    (`"{items} items in {ranges} ranges"`); the generated list `aria-label` gains the second one as a
    trailing clause so assistive technology learns the strip is showing ranges.
  - **`<lr-span-waterfall>` and `<lr-trace-tree>`: bars no longer stretch when a trace is truncated.**
    Both cap rendering at 500 spans and keep the earliest ones, but both then measured the time axis
    from just those survivors — so on a 10-second trace whose tail was truncated, a 1-second span drew
    at 100% track width with nothing to indicate the distortion. The axis and every duration bar now
    scale to the whole trace, measured before the cap is applied. The row cap, its localized notice,
    and the row projection itself are unchanged. **Type change:** the exported `LyraSpanProjection`
    gained a required `extentEndMs: number` (trace-relative ms, the greatest span end measured before
    the 500-span cap), so TypeScript code that constructs that shape by hand — a test double, or a
    wrapper that re-projects spans — must now supply it.
- de8d9b6: `<lr-table>`'s `columns[].priority` column-hiding now reacts to the table's actual measured
  overflow instead of two fixed container-width breakpoints.
  
  In 15.0.0, a `'low'`-priority column hid under a fixed ~900px container width and `'medium'` under
  ~640px, regardless of whether the table's content needed the room — a table narrower than the
  threshold lost columns even when nothing overflowed, while a wide table with very long cell content
  kept every column and simply grew wider than its container. `scroll-mode="auto"` already measures
  real overflow (`scrollWidth` vs `clientWidth`) to decide whether the table becomes its own
  horizontal scrollport; `columns[].priority` now shares that same measurement instead of
  disagreeing with it. `'low'` still hides first, and `'medium'` hides too only if the table would
  still overflow with `'low'` gone — the same progressive order as before, now triggered by genuine
  overflow rather than a width threshold. `hasHiddenPriorityColumns`,
  `priorityColumnsToggleAvailable`, `priorityColumnsVisible`, and the
  `lr-priority-columns-visibility-change` event are unchanged.
  
  The two thresholds remain deliberately non-themeable: a `@container` query, which a token-driven
  threshold would need to read, can only ever see ancestor inline-size, never a measured overflow
  amount.
  
  **Migration.** A table whose content genuinely doesn't fit its container behaves the same as
  before. Affected: a table sized comfortably wider than its content that happened to sit under the
  old fixed threshold (e.g. a 700px-wide `<lr-table>` whose columns easily fit in 500px) no longer
  hides its priority columns — previously it did, regardless of fit. A table whose content is wide
  enough to need the room continues to hide the same tiers in the same order. There is no opt-out:
  `table.styles.ts`'s `[part='base'][data-hide-priority-low]`/`-medium` attributes replace the former
  `@container` rules and are internal wiring, not selectors a consumer should target directly.
- 75a4e6d: Theme runtime (`@aceshooting/lyra-ui/theme.js`): the accent ramp now accepts a per-role color
  record and an optional surface reference, and derives success/warning/danger/neutral ramps with
  the same contrast guarantees the brand ramp already had.
  
  Previously `setLyraTheme()`'s accent derivation accepted exactly one color and derived only the
  brand ramp, mixed against two hard-coded backgrounds (`#1a1a1a` dark / `#ffffff` light) rather than
  the application's own surface. There was no way to supply a different base color for `success`,
  `warning`, `danger`, or `neutral`, or to retarget the mix against a real surface color, so an
  application that needed the library's own guaranteed-contrast quiet/normal/loud/on-* math for those
  roles had to reimplement relative-luminance, contrast-ratio, and color-mix logic outside the
  library and keep it in step by hand.
  
  `accent` is now either an absolute CSS color (unchanged shorthand for the brand role) or a per-role
  record `{ brand?, success?, warning?, danger?, neutral? }`; only the roles you supply are
  (re)derived, each with the same >=4.5:1 paired-foreground and >=3:1 border/focus contrast
  guarantees the brand ramp already had, and a color that fails to resolve fails closed to `null` for
  just that role rather than taking the others down with it. Each role's value can in turn be a bare
  color (applied in both modes, as above) or a `{ light?, dark? }` map deriving that role's ramp from
  a genuinely different base color per resolved mode — e.g.
  `{ brand: { light: '#2563eb', dark: '#f59e0b' } }` — rather than only a different tint weight of
  the same hue; an omitted branch keeps that mode's inherited/palette default. `LyraTheme` gains a new `surface` field:
  an absolute CSS color used as every supplied role's mix base instead of the shipped light/dark
  defaults. The pre-paint no-flash bootstrap (`lyraThemeBootstrap`/`createLyraThemeBootstrap()`)
  derives from the same math, inlined self-contained as before, so the bootstrap and the runtime
  setter can never drift apart.
  
  **Migration.** `setLyraTheme({ accent: '<color>' })` and `getLyraTheme().accent` being a bare
  string both keep working exactly as before — that shape is now shorthand for
  `{ brand: '<color>' }`. `getLyraTheme()` and the `lr-theme-change` event detail gain a `surface`
  field (`null` by default), so code that structurally compares the whole returned/emitted record
  (for example `assert.deepEqual(getLyraTheme(), { mode, accent })`) needs `surface: null` added.
  Pass `{ accent: <old value> }` to keep prior behaviour unchanged in every other respect.
- 152ea02: Remove two long-deprecated compatibility aliases: `<lr-app-rail-item>`'s `active`
  property/attribute and `<lr-widget>`'s `activeView` property.
  
  Both were restored in 11.2.0 after shipping as undocumented dead expandos (a Lit property binding
  on a custom element is untyped, so a silent rename breaks no type check, no test, and no build).
  Each carried a `removalNotBefore: '13.0.0'` compatibility window, which has now passed by three
  major versions.
  
  **Migration.**
  
  `<lr-app-rail-item active>`/`.active` no longer exist — use `current`/`.current`, which has been
  available since 11.2.0. Setting `active` now has no effect: the attribute is unrecognized and the
  property is a plain, unobserved expando.
  
  `<lr-widget>.activeView` no longer exists — use `.activeViewId`, available since 11.2.0. Setting
  `activeView` now has no effect: it no longer seeds `activeViewId`.
  
  The other eight deprecated-alias records reviewed for this release remain — each is tied to Web
  Awesome or Shoelace still shipping its own equivalent deprecated name upstream (`lr-accordion-item`'s
  `base` part, `lr-file-input`'s `base` and `label` parts, `lr-icon`'s `autoWidth`/`auto-width`,
  `lr-known-date`'s `label` part, `lr-qr-code`'s `base` part, `lr-sparkline`'s `base` part, and
  `lr-video-playlist`'s `base` part), confirmed still present in the pinned Web Awesome 3.11.0 /
  Shoelace 2.20.1 manifests and (for the three Pro-tier components without a published manifest)
  the current Web Awesome documentation pages.

### Minor Changes

- 68451c6: `LyraDateRangePreset` (`<lr-date-picker>`, `<lr-date-input>`, `<lr-filter-bar>`'s `'date-range'`
  filter definitions) and `TimeRangePreset` (`<lr-time-range>`) gain an optional `readonly id?:
  string`. It's a caller-owned correlation key, never read or interpreted by any of these
  components, that a TypeScript consumer can now read back off `appliedPreset.id` (or a
  `'date-range'` filter's `lr-input` detail) with no unchecked cast and no side `WeakMap` to persist
  which preset is active. `<lr-date-picker>`/`<lr-date-input>`/`<lr-filter-bar>` already returned the
  caller's own preset object by reference, so `id` round-trips there with no runtime change.
  `<lr-time-range>` copies presets into a frozen defensive snapshot, so its snapshot construction now
  also carries `id` through; an untagged preset is unaffected, and the snapshot's own key shape is
  unchanged when no `id` is supplied.
- a653796: `<lr-tab-group>`'s active-panel mouse-hover preview, `<lr-scroller>`'s viewport preview,
  `<lr-carousel>`'s scroll-container preview, `<lr-tool-select-dialog>`'s tool-list body preview, and
  `<lr-thinking-panel>`'s transcript-body preview each gain the same four-longhand outline shape
  `<lr-virtual-list>` already exposes: a `-width`, `-style`, `-color`, and `-offset` custom property
  per component (for example `--lr-tab-group-panel-hover-outline-color`), each defaulting to that
  site's previous literal paint so nothing repaints unless a consumer sets one. Setting the `-color`
  hook to `transparent` opts a site out of the hover preview entirely.
- 152ea02: Every internal scroll container now honors an opt-in theme-level `--lr-theme-scrollbar-width` /
  `--lr-theme-scrollbar-gutter` hook pair: `<lr-table>`'s `base` part, `<lr-virtual-list>`'s `base`
  viewport (used by `<lr-thread-list>`), `<lr-code-block>`/`<lr-code-block-core>`'s `body` part,
  `<lr-code-editor>`'s `editor` part, and the components that already hardcoded scrollbar CSS
  (`<lr-carousel>`'s `scroll-container`, `<lr-scroller>`'s `viewport`, `<lr-time-input>`'s `column`,
  `<lr-emoji-picker>`'s `grid`). Set either custom property once on `:root` or any ancestor to retheme
  every one of these scrollports at once; unset, each keeps rendering with its own previous default
  (`auto`/`auto` for the newly-covered scrollports, and each component's own prior literal for the
  four that already declared `scrollbar-width`/`scrollbar-gutter`), so nothing repaints for a consumer
  who never sets the hook.
- 152ea02: Ship a complete Italian (`it`) translation catalog
  
  `@aceshooting/lyra-ui/translations/it.js` now registers a full, independently reviewed Italian
  catalog covering every one of the 1,288 keys in `LYRA_DEFAULT_STRINGS`, alongside the ten existing
  full catalogs (`ar`, `de`, `es`, `fa`, `fr`, `he`, `ja`, `pt-BR`, `ru`, `zh-CN`). It ships in the
  same shape as every other locale since 16.0.0: a thin `translations/it.js` aggregate plus twelve
  smaller `translations/it/<family>.js` side-effect slices, so an application can import the whole
  catalog or just the families it actually renders. Every pluralized message uses Italian's own CLDR
  `one`/`many`/`other` categories. Importing the catalog also makes `it` show up in
  `getRegisteredLyraLocales()`, and therefore in `<lr-locale-picker>`, for free.
- 68451c6: Fixed `<lr-progress-ring>`'s track/indicator stroke width silently changing from its documented `4px`
  default to `3px` for any consumer who imports `theme.css` and sets no override at all. The stroke
  width used to bridge the widely-shared `--lr-theme-border-width-thick` theme input directly, and
  `theme.css` declares that input at `3px` -- the correct default for the many surfaces that genuinely
  share it, but not for this ring, whose own default has always been `4px`. It now reads a dedicated
  `--lr-theme-progress-ring-track-width` theme input instead: unset by default whether or not
  `theme.css` is imported, so the ring always renders at `4px` until a consumer sets this property on
  `:root` or any ancestor to retune it.
- 770e1f4: Focus repair and density parity for `<lr-activity-feed>`, plus an opt-in focus entry point and
  Escape handling for `<lr-confirm-bar>`.
  
  - `<lr-activity-feed>`: focus is now repaired instead of silently dropping to `<body>` when the
    control holding it disappears. Collapsing (`expanded` becoming `false`) moves focus already
    inside the body to `[part="header"]` before the body is hidden; removing the specific `entries`
    row that held focus does the same once that render (and, while virtualized, the internal
    `<lr-virtual-list>`'s own follow-up render) has settled. Neither case touches focus that's
    elsewhere — appending a live entry never steals focus from an unrelated, still-present control.
    The component also gains the `compact`/`frame` density vocabulary its agent-surface siblings
    (`<lr-confirm-bar>`, `<lr-thinking-panel>`) already expose: `compact` tightens header and
    entry-row padding via `--lr-activity-feed-compact-header-padding`,
    `--lr-activity-feed-compact-header-gap`, and `--lr-activity-feed-compact-entry-padding`;
    `frame="plain"` removes the outer card border, background, and corner radius. Both default to
    the existing byte-identical presentation when unset.
  - `<lr-confirm-bar>`: gains an opt-in `autofocus` boolean that moves focus into the bar after its
    own first render — the Deny control when it's present and enabled, else `[part="status"]` —
    useful for a host that swaps a focused control out for this bar (the case the class doc already
    described but left entirely to the host, as `<lr-memory-panel>`'s own
    `focusPendingConfirmation()` still does). Also gains an opt-in `escape-denies` boolean that maps
    Escape on `[part="base"]` to the same outcome as clicking Deny, scoped to this element's own
    base rather than `document` since this bar is inline and non-modal, not a member of the shared
    `activateOverlay()` Escape/stacking contract real overlays use. Both default to `false`; neither
    changes any existing behavior unless a host opts in.
- 96f604c: Give every framed agent-surface component the card-chrome theming hooks its density knobs already
  implied, so retuning a nested card no longer needs a `::part(base)` override. Each hook is an
  inline `var()` fallback at its point of use, so an unset component paints exactly as before, and
  any of them can be set on the element itself or on an ancestor transcript:
  
  - `<lr-activity-feed>`: `--lr-activity-feed-background`, `--lr-activity-feed-border-color`,
    `--lr-activity-feed-radius`. The border-colour hook also covers the header/body divider that
    `frame="plain"` keeps, so a retuned card no longer strands a mismatched rule inside itself.
  - `<lr-agent-run>`, `<lr-result-card>`, `<lr-stack-trace>`, `<lr-task-list>`,
    `<lr-thinking-panel>`: `--lr-<tag>-background`, `--lr-<tag>-border-color` and `--lr-<tag>-radius`
    on `[part="base"]`, with the border colour reaching each component's own interior divider
    (`lr-result-card`'s header rule, `lr-task-list`'s and `lr-thinking-panel`'s header/body rule).
  - `<lr-commit-card>`: `--lr-commit-card-border-color` and `--lr-commit-card-radius`, plus
    `--lr-commit-card-background`, which defaults to `transparent` — this card has never painted a
    fill of its own, so it still takes the surface it sits on unless a consumer opts in.
  - `<lr-terminal>`: `--lr-terminal-border-color` and `--lr-terminal-radius` join the pre-existing
    `--lr-terminal-surface-color` fill, the border colour also covering the toolbar/log divider.
    `--lr-terminal-surface-color` keeps its name; nothing is renamed or deprecated.
  - `<lr-subagent-panel>`: `--lr-subagent-panel-background`, `--lr-subagent-panel-hover-background`,
    `--lr-subagent-panel-border-color` and `--lr-subagent-panel-radius` for each run row's border,
    radius, resting and hovered fill, and action divider. Hover is a separate hook because a panel
    retuned to a dark resting fill would otherwise flash the stock raised surface under the pointer;
    the pressed fill mixes from the hover hook, so retuning hover carries the press with it. A
    selected row still takes its border from `--lr-subagent-panel-selected-border`.
  - `<lr-chat-composer>`: `--lr-chat-composer-background`, `--lr-chat-composer-border-color` and
    `--lr-chat-composer-radius`, so a composer docked into a themed panel can match it. The
    `:focus-within` border keeps its brand colour — that is state paint, not card chrome.
  
  `frame="plain"` still removes the outer border, radius and fill on every one of these components,
  so the hooks tune the card presentation rather than reinstating chrome a consumer asked to drop.
  `<lr-commit-card>`'s plain rule now clears the fill as well: with no fill of its own it never had
  one to clear, and without that line the new background hook would have been the one way to get a
  filled "plain" card. Unset, every component paints exactly as before.
- 34cdefb: `<lr-alert>` gains an opt-in `size` on the library's one six-step size ladder
  (`2xs`/`xs`/`s`/`m`/`l`/`xl`, plus the `small`/`medium`/`large` spellings, which are accepted as
  authored rather than rewritten to the short form). A tier scales the panel's padding and text
  together and takes `<lr-callout>`'s values for both, so a tiered alert and a tiered callout of the
  same size line up in one column.
  
  The close action keeps the shared tappable-target floor at every tier — it is a WCAG 2.5.8
  minimum, not a density knob — and its optical pull-out toward the panel edge is now clamped to the
  tier's own gutter, so the two smallest tiers cannot push it through the panel's clipped border.
  
  Two things deliberately do not vary by tier, again matching `<lr-callout>`: the gap between the
  icon, the message and the close action — it separates three adjacent boxes rather than setting the
  panel's density, and tightening it at the small tiers only crowds the close control — and the
  leading icon glyph, a status affordance whose size is bounded by the shared tappable-target token.
  The tier also uses the ladder's inline gutter on all four sides, because the ladder's block gutter
  belongs to a single-row control and collapses to zero at the two smallest tiers, which would leave
  an alert's text touching its own border.
  
  This is the one Lyra addition on top of the pinned Shoelace alert surface, and it is opt-in for
  exactly that reason: with no `size`, the panel keeps the padding it always had and the text size it
  inherits, so migrated markup renders unchanged. The untiered states of the two panels are therefore
  not interchangeable even though every tier is: an untiered `<lr-callout>` reads the ambient
  form-control slots and falls back to the shared `m` padding and text size, while an untiered
  `<lr-alert>` keeps its fixed gutter and inherits. Pinning a default tier here would have resized
  every alert that shipped before this property existed. An unsupported value normalizes to the
  omitted state and removes the attribute, matching how `countdown` already behaves on this
  component.
- 9f0179a: `<lr-app-rail>` gains an opt-in desktop collapse control and a `toggleCollapse()` method.
  
  Setting `collapsible` renders a `[part="collapse-toggle"]` button inside `[part="header"]` that
  flips the rail between its `'full'` and `'icon-only'` presentations. It carries a localized
  accessible name (`appRailCollapse` / `appRailExpand`, both new `DEFAULT_STRINGS` keys translated in
  all ten shipped catalogs) and renders `aria-expanded` in both states, so a screen reader announces
  which presentation the rail is in rather than only the button's label. Its chevron is
  direction-aware through the new `[part="collapse-icon"]` wrapper's own `transform` — one glyph,
  mirrored by the wrapper, under both `dir` values. Four hooks retune it:
  `--lr-app-rail-collapse-toggle-hover-bg`, `--lr-app-rail-collapse-toggle-hover-color`,
  `--lr-app-rail-collapse-toggle-active-bg` and `--lr-app-rail-collapse-toggle-active-color`.
  
  `toggleCollapse()` performs the same flip for a consumer that renders its own control — in app
  chrome, a command palette, a keyboard shortcut — instead of, or alongside, opting into
  `collapsible`. Both write `preferredMode`, never `forceMode`, so the `mobile-breakpoint` keeps
  being tracked automatically (a genuinely too-narrow viewport still wins), the change announces
  itself through the existing `lr-mode-change` event, and it survives a reload whenever `storage-key`
  is set and `persist` includes `preferred-mode`.
  
  Both are inert in `'mobile'` mode: the control is not rendered at all (a second, meaningless
  control inside the focus-trapped overlay next to the dismiss button), and `toggleCollapse()`
  returns without recording a preference the user never chose. `collapsible` defaults to `false`,
  where the rail renders exactly as before — no extra element, and `[part="header"]`'s original block
  layout unchanged.
- 9f0179a: `<lr-app-rail>` now projects its mobile overlay's disclosure state onto the external element
  `trigger`/`for` resolves.
  
  Wiring a hamburger button in application chrome to the rail already returned focus to it on every
  close path, but the button itself announced nothing: it carried no `aria-expanded` and no
  `aria-controls`, so a screen-reader user operating it could not tell whether the navigation was
  open or what it controlled. The resolved trigger now receives both, through the same shared ARIA
  ownership `<lr-popover>` uses for its own trigger — including the element-reference form that
  crosses the shadow boundary an `aria-controls` idref cannot, and including restoring whatever the
  consumer had written itself once the association is released. `aria-expanded` renders in both
  states, never only when open.
  
  The association applies while the rail is in `'mobile'` mode and is released when it leaves that
  mode or disconnects: outside mobile there is no overlay to expand, and a permanent
  `aria-expanded="false"` would announce a disclosure that does not exist. Unlike the focus-return
  association (resolved once, when the overlay opens), this tracks live — reassigning `trigger` moves
  the state to the new element and clears it from the old one.
- 9f0179a: `<lr-app-rail-item>` gains `meta` and `end` slots, and a new `<lr-app-rail-group>` component titles
  and optionally collapses a section of rail items.
  
  Both new item slots render as SIBLINGS of the item's own link/button — the shape `<lr-details>`
  already uses for its `header-actions` — never inside it. A control slotted into `end` (an overflow
  menu trigger, a dismiss button) therefore keeps its own click, keyboard activation and focus order
  instead of being swallowed by the item's activation target, and neither slot's text joins the
  item's accessible name. `meta` carries secondary text such as an unread count and is visually
  clipped in `icon-only` mode exactly as `[part="label"]` is, staying available to assistive
  technology; `end` stays visible there. Both wrappers (`[part="meta"]`, `[part="end"]`) are hidden
  while empty, so an item using neither renders as it did before. `--lr-app-rail-item-meta-color` and
  `--lr-app-rail-item-meta-font-size` retune the metadata text, and
  `--lr-app-rail-item-gap` now also spaces the item's control from its adornments.
  
  `<lr-app-rail-group>` groups items by composition — it holds whatever it is given, with no items
  array and no renderer callback, so it can never disagree with what is rendered inside it. It names
  itself with a real heading landmark (`role="heading"` plus a settable, range-clamped
  `heading-level`, rather than a hard-wired `<h3>` whose level would be wrong in half the pages that
  embed a rail) and labels its own `role="group"` container from that heading. `collapsible` opts in
  the standard disclosure shape: the heading's own text becomes the button carrying `aria-expanded`
  and `aria-controls`, and collapsing runs through a `lr-toggle-request` / `lr-toggle` request/commit
  pair, so a consumer can veto it or resolve it by assigning `open` from the request listener.
  `open` defaults to `true` and accepts `open="false"` from markup. A `header-actions` slot places
  controls beside the heading without toggling the group.
  
  The rail marks a slotted group `icon-only` the same way it marks a slotted item, and the group
  forwards that to the items it owns — including items appended later — so grouping survives the
  rail's icon-only presentation.
- 96f2b67: `<lr-app-rail>`/`<lr-app-rail-item>`: mobile overlay focus-trap fix, external trigger association, and new geometry/theming hooks.
  
  - **Fix:** in mobile mode, `[part="toggle"]` is now reparented to be the first child of
    `[part="panel"]` for exactly as long as the overlay is open, mirroring how `<lr-dialog>` keeps
    its close button inside its own panel. Previously it rendered as a DOM sibling ahead of the
    panel, so while the overlay was open it stayed visible and clickable but was outside both the
    `aria-modal` subtree and the shared focus trap's Tab cycle — reachable by mouse, unreachable by
    keyboard. The same button (never destroyed/recreated) moves back to its resting position, a
    sibling ahead of the panel, once closed, and is rendered as its own reserved row ahead of the
    `header` slot rather than absolutely overlaid on top of it, so a wide/slotted header is never
    obscured.
  - **New:** `trigger`/`for` properties associate this rail with an external open control (e.g. an
    application-chrome hamburger button used together with `hide-toggle`). Closing the overlay by
    any path — Escape, backdrop click, a nav-item click, or the built-in toggle — now returns focus
    to that external trigger, instead of relying on whichever element happened to still hold focus.
    `hide-toggle` now only suppresses the toggle in its outside/closed ("open") position; it stays
    visible once reparented inside the open panel, since it is then the only in-panel dismiss
    control — previously `hide-toggle` left an open mobile panel with no in-panel way to close it at
    all besides Escape/backdrop.
  - **New tokens**, all byte-identical when unset: `--lr-app-rail-panel-inset-block-start` (applied
    to both `[part="panel"]` and `[part="backdrop"]`, for a fixed app bar/status area above the
    drawer), `--lr-app-rail-panel-radius`, `--lr-app-rail-panel-overflow-inline`/
    `--lr-app-rail-panel-overflow-block` (the panel's `overflow-inline: clip`/`overflow-block: auto`
    also clip a `position: fixed` popup opened by a slotted/nav-item control whose rendered box
    extends past the panel; setting both tokens to `visible` together opts out — per the CSS
    overflow spec a lone `visible` axis paired with a non-`visible` one computes as `auto` instead,
    which still clips, so only one of the two tokens is not enough), `--lr-app-rail-background`,
    `--lr-app-rail-panel-background`, `--lr-app-rail-header-padding`, and
    `--lr-app-rail-footer-padding`.
  - **New on `<lr-app-rail-item>`:** `--lr-app-rail-item-min-block-size` (floor-clamped to
    `--lr-icon-button-size`, preserving the WCAG 2.5.8 hit-area minimum regardless of the override),
    `--lr-app-rail-item-padding`, `--lr-app-rail-item-gap`, and `--lr-app-rail-item-icon-size`, plus
    a `[part="current-indicator"]` rendered only while the item is `current`/`aria-current="page"`,
    mirroring `<lr-conversation-item>`'s shipped `active-indicator` part and its
    `--lr-app-rail-item-current-indicator-color`/`-width`/`-inset-inline` tokens.
- 8049921: Add themeable CSS custom properties and a layout opt-in across four components, every one
  byte-identical when unset:
  
  - `<lr-attachment-chip>`: `--lr-attachment-chip-padding` and `--lr-attachment-chip-compact-padding`
    make `[part="base"]`'s previously-hardcoded padding themeable in both the resting and `compact`
    states; `--lr-attachment-chip-compact-thumbnail-only-padding` gives the lone thumbnail rendered
    by `compact` + `thumbnail-only` its own reduced, symmetric padding instead of keeping the
    padding sized for a text row that no longer renders.
  - `<lr-chip>`: a new `wrap` boolean property lets a long `[part="label"]` wrap onto multiple lines
    instead of ellipsis-truncating to one, matching `<lr-suggestion-chips>`'s identical opt-in.
  - `<lr-chip-group>`: `--lr-chip-group-gap` makes `[part="base"]`'s wrap gap themeable.
  - `<lr-switch>`: `--lr-switch-track-border`, `--lr-switch-checked-thumb-fill`, and
    `--lr-switch-label-color`/`--lr-switch-checked-label-color` add a track border hook and
    independent checked-state thumb/label colors.
  - `<lr-conversation-item>`: `--lr-conversation-item-align` lets a consumer opt a reliably
    single-line row into `center` cross-axis alignment (default stays `flex-start`, which remains
    correct for the common multi-line title+excerpt row).
- a89c79a: Add missing theming hooks to `lr-button`, `lr-card`, and `lr-segmented`, all undeclared by default
  so existing rendering is unchanged unless a consumer opts in:
  
  - `lr-button`: `appearance="link"` now resets `box-shadow: none`, so a globally-set
    `--lr-button-shadow` no longer paints a shadow behind a zero-chrome inline link. The shared hover
    rule also exposes `--lr-button-hover-color` and `--lr-button-hover-border`, letting appearances
    such as `quiet` (which already expose resting `--lr-button-quiet-text`/`-border`) theme their
    hover text/border independently; unset, each falls back to whatever colour/border the active
    `appearance` already paints at rest.
  - `lr-card`: `::part(base)` gains `--lr-card-shadow` (default `none`, mirroring
    `--lr-button-shadow`'s pattern) for a raised card, plus `--lr-card-interactive-hover-shadow` on
    the `actionable`/linked hover state, which falls back to `--lr-card-shadow` itself.
  - `lr-segmented`: the hover rule gains `--lr-segmented-hover-bg` and `--lr-segmented-hover-shadow`,
    alongside the existing `--lr-segmented-hover-color`. The track (`[part="base"]`) gains
    `--lr-segmented-track-bg` and `--lr-segmented-track-border-color`, replacing a literal
    `var(--lr-color-border)` read with an overridable one.
- 96f604c: `<lr-callout>`, `<lr-empty>`, `<lr-rag-answer>`, `<lr-retrieval-search>`, `<lr-retrieval-results>`,
  `<lr-table>` and `<lr-ingestion-queue>` gain an opt-in `announce` boolean that sends the message
  they are already presenting when they first mount to the shared light-DOM announcement sink.
  
  Every one of them has always announced *later* content changes and deliberately stayed silent about
  the content present at mount — correct for a callout or empty state that is simply part of the page
  a user is arriving on, because that text is read in document order anyway. It is wrong for the
  other common case: a callout or empty state created in response to something the user just did
  ("Save failed", "No results"). Nothing else announces that first message, so a screen-reader user
  whose focus stayed on the control they activated never learned the outcome. `announce` opts one
  component into announcing it, and the default keeps every existing consumer silent at mount.
  
  The initial announcement goes through each component's existing announcement path, so it resolves
  exactly like a later update: `<lr-callout>` picks assertive urgency for `variant="danger"`
  (including a `danger` inherited from a composed ancestor) and polite otherwise, `<lr-empty>` is
  always polite, both skip content hidden by `hidden`/`inert`/`aria-hidden`/CSS or a hidden composed
  ancestor, both read flattened text through nested forwarding slots, and a callout's nonempty
  `aria-label`/`accessible-label` prefixes the message through the full localized
  `calloutAnnouncementWithContext` template rather than being prejoined. A closed callout
  (`open="false"`) announces nothing.
  
  The three retrieval components announce the state they are already presenting rather than their
  whole content, because that is the only part of them that is a message: the verbatim
  caller-supplied `errorText` assertively, or — with no error and nothing loading —
  `<lr-retrieval-search>`'s localized zero-result message and `<lr-retrieval-results>`' localized
  empty-result message, politely. A component still `loading`, or one already showing an answer or
  chunks, has settled on no message and announces nothing.
  
  `<lr-table>` and `<lr-ingestion-queue>` close the same gap on their failure surfaces. A table
  created to report a reload that already rejected mounts with `error` set, so there is no `error`
  transition for its assertive sink to catch; `announce` speaks the `error-heading` text once,
  through the same sink and the same text the later transition uses, and deliberately does not
  forward `announce` to the composed `[part="error"]` `<lr-empty>` so the failure is spoken once
  rather than twice. A queue mounted with `stage: "failed"` rows announces their caller-supplied
  `item.error` strings once, list-formatted in the effective locale exactly as a later failure is.
  
  `announce` is read once, when the component first mounts. A later reconnection or adoption stages
  the existing content again instead of replaying the announcement — the same reconnect contract
  these components already documented — and later content changes continue to announce whether or
  not `announce` is set. In every case the shared region is acquired on connect, strictly before the
  first text lands in it — for `<lr-callout>` and `<lr-empty>` the mount announcement is additionally
  deferred one animation frame past the first update so slot distribution has settled first. Assistive
  technology has to have been observing the region for an addition to be read at all.
- 9f0179a: Chart formatters now know which axis, series and datum they are formatting, and `<lr-lite-chart>`
  can label its category axis for display only.
  
  - `LyraChartFormatterContext` gains `axis?: 'x' | 'y' | 'y2' | 'r'`, and every tick call names the
    scale it is building. Two value axes usually exist precisely because they carry different units,
    which one formatter previously could not tell apart: `x`, `y`, `y2` and the radial `r` scale all
    received an identical context, so a dual-axis or scatter chart had to escape into
    `config.options.scales.<id>.ticks.callback` — and the accessible table, the CSV export and the
    spoken announcement then lost the unit text the typed formatter exists to keep consistent.
  - `<lr-chart>`'s tooltip, DOM legend and data-label (`visual`) calls now spread `datasetIndex`,
    `index`, `label` and `seriesLabel` the way the table, export and spoken calls already did. Each
    call site already resolved that metadata and dropped it, so a tooltip formatter could not treat a
    percentage series differently from a value series in the same chart. Indexes are reported in
    source space, matching the data table, the CSV export and `lr-point-click`. The `axis` field
    reaches the table, export and spoken surfaces too, so one formatter renders one unit everywhere.
    A `stackTotals`/`tableTotals` stack total is deliberately the one context with no `datasetIndex`
    and no `seriesLabel`: it sums across the stack, so naming its topmost series would make a
    unit-switching formatter render that series' unit for a cross-series number. It keeps the
    category `index`/`label`, the stack's `axis`, and `statistic: 'total'`, matching
    `<lr-lite-chart>`'s total cells.
  - Same gap, same fix, in the two siblings: `<lr-lite-chart>` names its value axis on every surface
    and now tells the `visual` formatter which category a mark is, not only which series;
    `<lr-box-plot>` names its value axis on ticks and passes the hovered datum's
    `datasetIndex`/`index`/`label`/`seriesLabel`/`statistic` to tooltip formatting instead of
    discarding what the callback was handed.
  - `<lr-lite-chart>` gains `axisLabelText?: (label, index) => string | null`, a display-only
    override for one category tick's text. Returning `null` renders no tick there at all, which makes
    boundary-aligned ticks (a month, a release, a shift change) expressible without the even stride
    `maxLabels` imposes. `labels` stays authoritative for the accessible table's row headers, the
    per-mark title and accessible name, the announcement and the CSV export, so blanking a tick never
    blanks the same category where a reader or a spreadsheet needs it. The returned text is
    ellipsized to its own slot exactly like a source label, keeps its full text as the tick's
    accessible name, and a return value that is neither a string nor `null` falls back to the source
    label.
  
  Every field is additive: it was `undefined` before, so no existing formatter changes behaviour.
- 9f0179a: `<lr-combobox>` reports async source failures properly and gains a public `refresh()`
  
  A rejected `source` call now renders the library's shared failed-load state inside the listbox —
  the same `<lr-empty>` shape `<lr-table>` uses, with the same `retry-button` — instead of a
  one-line message with no way forward. The precedence is the library-wide one: loading beats error
  beats empty, so a retry in flight never flashes the stale failure and a failure is never hidden
  behind "no matches" copy.
  
  New surfaces:
  
  - `lr-source-error` — non-cancelable, `detail: { error }` carrying the raw rejection, so a host can
    log or report it. The rendered copy stays localized and never shows it.
  - `lr-retry` — cancelable; the built-in action calls `refresh()`, and `preventDefault()` leaves the
    failure on screen for a host that owns its own retry timing.
  - `refresh()` — re-runs the current query without changing the source's identity, its debounce
    controller, or its delay. This is the missing way to invalidate a *stable* `source`: reassigning
    the property was the only other route, and that clears the fetched rows and the pending-selection
    cache because it means "a different provider". Called while the listbox is closed, it queues for
    the next open.
  - `source-error` slot and the `source-error` / `source-error-row` / `retry-button` parts, for
    replacing or restyling the state. It is named apart from the form control's own `error` slot
    deliberately: they are different failures and a field has to be able to show both.
- 9f0179a: `<lr-confirm-bar>` can now be told where focus belongs after a decision, and `<lr-checkpoint>` no
  longer drops focus when a host starts restoring.
  
  - `<lr-confirm-bar>` gains `returnFocusTo`, an optional property (an `HTMLElement`, `null`, or a
    thunk resolving to one) naming where focus should go once a decision lands. Until now the bar's
    only focus destination was its own `[part="status"]` — correct as a landing spot that cannot
    disappear mid-handoff, but a dead end for the case the component exists for: a host swaps a
    focused control out for the bar, and once the decision is made focus belongs back on that control,
    not on a status line the host is about to unmount. It applies to every path that reaches a
    decision, including a `pending` decision finalized externally. A named target that is missing,
    detached, `inert`, or otherwise refuses focus falls back to `[part="status"]` rather than to
    `<body>` — an `inert` element refuses `focus()` silently. Left unset (the default), the handoff is
    byte-identical to today's.
  - `<lr-checkpoint>`: confirming a restore now refocuses `[part="restore-button"]` even when the host
    sets `restoring` from its own `lr-restore` listener. Confirming destroys the confirm group and with
    it the Confirm button holding focus, and the previous guard declined the refocus whenever a restore
    had already started — dropping keyboard users onto `<body>` at the moment the operation they
    authorized began. The restore button is still rendered in that state (`aria-disabled`, not
    `disabled`), so it was focusable the whole time.
- 9f0179a: `<lr-context-meter>` becomes a filter control when you ask it to, and its legend can show the
  numbers it stands for.
  
  - `interactive` (reflected, `false` by default) turns every band and every legend row into a real
    button that emits the cancelable `lr-segment-activate` (`detail: { index, label, value }`). The
    ring's arcs carry `role="button"` with their own tab stop and Enter/Space handling, since an SVG
    shape cannot be a native button. This is what a part-to-whole bar above a grid has always been
    asked to do — click the band, narrow the grid — and it previously required hand-rolling the strip,
    its colours and its accessible semantics.
  - `selectedIndices` renders `aria-pressed="true"`/`"false"` on the band and its legend row, plus a
    `segment-selected`/`legend-item-selected` part token and the
    `--lr-context-meter-selected-ring-color`/`-width` hooks. A filter toggle with no pressed state
    cannot be reported as on or off by assistive technology, which is why activation alone was not
    enough. Uncontrolled by default — an activation nobody vetoes toggles the index — and
    `preventDefault()` on `lr-segment-activate` hands the selection entirely to the consumer. A
    `bar`-shape band and a legend row mark the selection with an inset ring rather than an outline,
    so it composes with the hover, press and focus-visible outlines instead of being replaced by them
    the moment the user operates the filter; a `ring`-shape arc has no box of its own, so it thickens
    in place (`--lr-context-meter-selected-arc-stroke`) rather than painting an outline that would
    trace the whole ring identically for every selected arc.
  - In that mode, and only in that mode, the legend leaves `aria-hidden` so a keyboard or
    screen-reader user can reach the rows, and the visually-hidden `[part="segment-list"]` steps
    aside because the buttons already expose the same label/count pairs with their pressed state
    attached.
  - `legendDisplay` (`legend-display`) accepts `label` (the default, byte-identical to before),
    `label-value`, `label-percent` and `label-value-percent`, rendering `[part="legend-value"]` and
    `[part="legend-percent"]` spans. The share is the same clamped ratio the bar or ring paints, so
    the key can never disagree with the band it stands for, and both numbers are formatted through
    `effectiveLocale`. Folding a count into `segment.label` instead — the only workaround before —
    also pushed it into the hover title and the visually-hidden breakdown, where a screen reader
    heard the number twice.
  
  Unset, every one of these leaves today's pure-visualization behaviour unchanged: no buttons, no
  events, and an `aria-hidden` legend of labels.
- f10aaa2: `<lr-dialog>` and `<lr-progress-bar>` gain an opt-in `size` property on the library's shared
  six-step size ladder (`2xs`/`xs`/`s`/`m`/`l`/`xl`, plus the `small`/`medium`/`large` aliases). Both
  default to `m`, rendering byte-identically to before this property existed.
  
  - `<lr-dialog>`: `size` steps the panel's width cap, from a compact `20rem` at `2xs` up to a roomy
    `48rem` at `xl` (`32rem` unchanged at the `m` default). An explicit `--lr-dialog-width` or
    `--lr-dialog-max-width` override still wins over every tier.
  - `<lr-progress-bar>`: `size` steps the track/indicator thickness, from a slender `0.25rem` at
    `2xs` up to a bold `1.5rem` at `xl` (`1rem` unchanged at the `m` default). An explicit
    `--lr-progress-track-height` (or the upstream `--track-height`/`--height` aliases) still wins
    over every tier.
- 96f604c: Nine components that embed a search or filter field of their own can now be told what size that
  field should be. Until now each one shipped a single fixed geometry — one padding, one corner
  radius, and whatever text size it happened to inherit — with no property and no custom property
  that reached it, so a built-in filter box could not be lined up with the themed search field
  sitting next to it. `<lr-table>`, `<lr-data-grid>` and `<lr-knowledge-graph-explorer>` still
  render their built-in search field at a fixed size; they are not covered by this release.
  
  `<lr-thread-list>` gains an opt-in `size` on the library's one six-step ladder
  (`2xs`/`xs`/`s`/`m`/`l`/`xl`, plus the `small`/`medium`/`large` spellings, accepted as authored
  rather than rewritten). A tier gives the built-in search field the row height, text size, gutters
  and corner radius an `<lr-input>` of that tier has. Unsupported values normalize to the omitted
  state and remove the attribute. It also gains `--lr-thread-list-search-padding`,
  `-search-gap`, `-search-min-height`, `-search-font-size`, `-search-padding-inline`,
  `-search-padding-block`, `-search-radius` and `-search-clear-size`, each of which wins over the
  tier, so a consumer can take the tier and then move one value.
  
  Two things deliberately do not follow the tier here: the gutter around the field, which is
  sidebar chrome rather than field density, and the clear button, whose box is a tap target bounded
  by the shared minimum target size rather than by the text scale. Both have their own custom
  properties instead.
  
  The same capability now exists on the five siblings whose field is a native `<input>`, each with
  today's values as the defaults so unset markup renders byte for byte what it rendered before:
  
  - `<lr-command-palette>`: `--lr-command-palette-search-padding`, `-search-gap`,
    `-search-min-height`, `-search-font-size`.
  - `<lr-emoji-picker>`: `--lr-emoji-picker-search-min-height`, `-search-font-size`,
    `-search-padding-inline`, `-search-padding-block`. Its existing `size` still scales only the
    emoji glyph and item box, as documented — it has never tracked the form-control ladder, and
    pulling the filter field along would have resized a surface nobody asked to move.
  - `<lr-eval-dataset>` and `<lr-tool-select-dialog>`:
    `--lr-<tag>-search-min-height`, `-search-font-size`, `-search-padding-inline`,
    `-search-padding-block`, `-search-radius`. The trailing gutter stays reserved for the overlaid
    clear button and is not a knob.
  - `<lr-node-palette>`: the same five, with the height knob able only to raise the field — the
    shared tappable-target minimum stays underneath it, so no tier can shrink the field past the
    WCAG floor.
  
  `<lr-document-library>`, `<lr-source-picker>` and `<lr-retrieval-search>` could not be fixed with
  custom properties at all: their fields are composed `<lr-input>`/`<lr-combobox>`/`<lr-segmented>`
  elements that resolve their tier inside their own shadow roots. All three therefore gain an opt-in
  `size` that is forwarded to every control they compose — for the retrieval row that means the query field, the mode selector
  and the submit button together, because sizing one of three controls on a shared baseline is
  exactly what makes a row ragged. The retrieval submit button also gains
  `--lr-retrieval-search-submit-min-height` and keeps the tappable-target floor at every tier, so the
  smallest tiers cannot shrink it below the WCAG minimum. With no `size`, every composed control
  keeps its own `m` default.
- 9ba23e9: `<lr-export-button>`'s built-in CSV download gains an opt-in `bom` property so it opens correctly
  in Excel on Windows.
  
  - `bom: boolean = false` (reflected). Excel on Windows ignores a downloaded file's MIME charset
    and instead decodes a byte-order-mark-less CSV using the system ANSI code page, so accented,
    Arabic, CJK, and typographic characters in exported rows render as mojibake once the file is
    opened. Setting `bom` prepends a UTF-8 byte-order mark (U+FEFF) ahead of the CSV header row,
    which Excel uses to detect UTF-8 and decode the file correctly. Google Sheets, LibreOffice, and
    Numbers already sniff UTF-8 correctly with or without a byte-order mark, so this only matters
    for Excel.
  - The default remains `false`: today's downloaded CSV bytes are unchanged unless a consumer opts
    in.
  - The built-in JSON download never gets a byte-order mark, under any setting -- RFC 8259 forbids
    one in JSON.
  - The package-level `buildCsv()` helper (`@aceshooting/lyra-ui/components/utility/export-button/
    csv.js`) gained the same opt-in through a new third `options: LyraBuildCsvOptions` parameter
    (`{ bom?: boolean }`), for standalone callers that build their own CSV without going through
    `<lr-export-button>`.
- 9f0179a: `<lr-export-button>` gains an explicit lazy row source, and its late-read contract is now
  documented rather than merely true.
  
  - New **`getRows?: () => readonly Record<string, unknown>[]`** is consulted only when a built-in
    CSV/JSON download is actually about to be built: after the cancelable `lr-export` event was not
    prevented, and never for a custom format this component does not serialize itself. When set it
    replaces `rows` for that download, so a consumer can export a collection it already holds — an
    `<lr-table>`'s `viewRows`, for instance — without copying it into the element and keeping a second
    live copy there. The column fallback (used when `columns` is unset) derives its header row from
    the lazily supplied rows rather than a stale eager property. A non-array return is treated as no
    rows, matching how `rows` normalizes one; a callback that throws is reported through
    `lr-export-error` and the shared failure announcement, since an export whose data could not be
    collected has failed and a silent empty file would hide that.
  - The **eager `rows` property is read after the `lr-export` dispatch**, not at assignment time, so
    a listener that lets the built-in download proceed may assign `.rows` from inside its own handler
    and that data is what gets downloaded. That was already the behavior; it is now a stated contract
    with a test, so it cannot regress silently.
  
  Nothing changes for an export that sets neither: with `getRows` unset the download serializes
  `rows` exactly as before.
- 62c973a: `<lr-textarea>`, `<lr-code-block>`/`<lr-code-block-core>`, `<lr-dialog>`, and `<lr-details>` now
  fill a definite-height container instead of collapsing to their own content-sized default,
  following the library's established unconditional `block-size: 100%` chain (see
  `<lr-file-input>`/`<lr-code-editor>`). Every chain link is a plain percentage against its own
  parent, which resolves to `auto` — a no-op — for the ordinary content-sized case, so an unsized
  instance of any of these renders exactly as before.
  
  - `<lr-textarea>`: the chain now runs `:host` → `form-control` (a flex column, so `label`/`hint`/
    `error`/`footer` keep their natural size) → `base`/`form-control-input`/`textarea-adjuster`/
    `textarea-wrapper` → the native `textarea`, without disturbing `resize="auto"`'s own JS-driven
    growth or its `--lr-textarea-max-block-size` cap.
  - `<lr-code-block>`/`<lr-code-block-core>`: `base` is now a flex column and `body` grows to fill
    it, still capped by `--lr-code-block-max-height` and still scrollable.
  - `<lr-dialog>`: `body` now grows to fill whatever block space `panel` has left once `header`/
    `footer` take their own natural size, giving slotted content a definite, scrollable size. New
    `--lr-dialog-height` custom property (default `auto`, always capped at the viewport like
    `--lr-dialog-width`) opts into an assertive panel height instead of only the existing
    content-sized default.
  - `<lr-details>`: the previously-unstyled internal content gate between `base` and `content` now
    continues the fill chain, so open panel content can fill and scroll inside a bounded host without
    disturbing the closed/`until-found` findable state or the disclosure's open/close lifecycle.
- c1b03d1: Two `<lr-filter-bar>` gaps closed, both byte-identical to today's rendering when unset.
  
  - **The composed combobox's chip parts are now forwarded.** `<lr-filter-bar>` re-exports each
    built-in control's shadow surface under `filter-control-*` aliases, but the combobox alias list
    omitted `tag`/`tag-label`/`tags` even though `<lr-combobox>` exposes all three publicly
    (`tag-label` is documented with a `--tag-max-size` cap). Because `::part()` pierces exactly one
    shadow boundary, a consumer had no selector reaching a `multiple` combobox filter's
    selected-value chips through the bar — they stayed capped at that control's own default and
    could not be widened or restyled. `filter-control-tags`/`filter-control-tag`/
    `filter-control-tag-label` now forward `tags`/`tag`/`tag-label`, following the existing
    `filter-control-*` naming exactly.
  - **Every filter field now carries its own `field-<filterId>` part token, alongside the shared
    `field` token.** `[part="field"]` previously gave every field the same
    `--lr-filter-bar-field-basis`, so a `multiple` combobox holding several chips got no more room
    than a narrow single-select, and `::part(field)` could not be qualified to pick out one field —
    `::part(field)[data-id]`-style compounds are invalid and silently match nothing (only
    pseudo-classes may follow `::part()`). Each field wrapper's `part` attribute is now
    `"field field-<filterId>"` (for example `part="field field-status"`), so
    `lr-filter-bar::part(field-status) { flex: 2 1 20rem; }` targets exactly that field and can set
    any layout property, not just a width; `::part(field)` rules continue to match every field
    unchanged. `filterId` is consumer-supplied and `part` is a space-separated token list like
    `class`, so an id containing whitespace (or any other character that would need escaping) could
    otherwise fabricate an unrelated second token — in the worst case, one colliding with a real
    part name such as `active-filters`. The `field-<filterId>` token is therefore included only when
    `filterId` reads as a plain CSS ident (ASCII letters/digits/`-`/`_`, starting with a letter); an
    id that doesn't renders `field` alone, exactly as before this part existed.
- 8ed4b78: Three additions to `<lr-filter-bar>`, every new token byte-identical to today's rendering when
  unset.
  
  - **The per-filter field wrapper is now a themeable public part.**
    The row-stacking wrapper around each filter's composed control was a bare, untokenized
    `.filter-field { flex: 1 1 var(--lr-size-12rem); }`, with zero `@cssprop` entries anywhere on
    the component. It is now `part="field"`, with its flex-basis themeable via
    `--lr-filter-bar-field-basis` (still `var(--lr-size-12rem)` by default). The `controls` row's
    own previously-hardcoded gap is likewise now `--lr-filter-bar-gap` (still `var(--lr-space-s)` by
    default).
  - **A new `end` slot holds host-supplied trailing actions next to the
    reset button.** `<lr-filter-bar>` had zero host-level slots, so a consumer wanting a "Save
    search" or "Export" action beside the built-in reset had nowhere to put it short of wrapping the
    whole component. `end` renders inside the new `end` part (itself inside `[part="controls"]`,
    next to `reset-button`) and stays `hidden` -- claiming no layout space -- while nothing is
    slotted, matching the adornment-slot vocabulary (`start`/`end`) and the `header-actions`-style
    precedent set by `<lr-details>`/`<lr-card>`/`<lr-dialog>`.
  - **`'text'`/`'combobox'` filter definitions now forward
    `clearable`/`size`/`icon` (plus, `'text'`-only, `inputType`) to their composed
    `<lr-input>`/`<lr-combobox>`.** `renderControl()`'s `'text'`/`'combobox'` branches hardcoded
    `type="text"` and never set `clearable`/`size`/a `start`-slot adornment, even though
    `INPUT_EXPORT_PARTS`/`COMBOBOX_EXPORT_PARTS` already forwarded `start`/`clear-button` -- those
    forwarded parts were provably unreachable. All four new fields are optional and default to the
    composed control's own default (`clearable: false`, `size: 'm'`, `inputType: 'text'`, no icon),
    so an existing filter definition renders unchanged. `'combobox'` also gains the same optional
    `debounce` (ms) `'text'` already had, coalescing a burst of rapid selection changes (picks, a
    multi-select toggle, an `allowCustomValue`/`allowCreate` commit, or the clear action) into one
    delayed commit. Unlike `'text'`'s uncontrolled-with-sync field, the composed `<lr-combobox>`'s
    `.value=` binding stays fully controlled throughout a pending debounce: it renders the pending
    selection rather than the last-committed value, so the control's own display never reverts
    mid-delay. A pending debounce is flushed by the control's own blur and cancelled by `reset()`, a
    chip removal, and disconnection -- identical to `'text'`.
- b98c6be: Three bug fixes across `<lr-tab>`, `<lr-confirm-bar>`, and `<lr-app-rail>`.
  
  - `<lr-tab>`: the projected tab descriptor's own host declared no `color`/`font`, so it inherited
    the library's base default text color instead of the real `[part="tab"]` button's computed
    color. In practice, `--lr-tab-group-selected-color` and `--lr-tab-group-hover-color` never
    visibly reached a tab's label text, even though they correctly recolored `[part="tab"]` itself.
    `<lr-tab>`'s host is now `color: inherit; font: inherit;` alongside its existing
    `display: contents`, so the projected label picks up the button's real computed color and font.
  - `<lr-confirm-bar>`: `decide()` dispatched the cancelable `lr-approve`/`lr-deny` event
    synchronously, then unconditionally overwrote `.pending` with its own built-in value — silently
    clobbering a synchronous listener that had called `preventDefault()` and then resolved the
    decision itself (by setting `.decision` or `.pending` directly) instead of waiting for the
    built-in pending/loading presentation. That listener's own state now wins: the built-in
    `pending` fallback only applies when the listener left both `.decision` and `.pending`
    untouched. Also adds a new reflected `disabled` boolean property that disables both Deny and
    Approve and makes `decide()` a no-op, independent of and composable with `pending`.
  - `<lr-app-rail>`: a `preferred-mode` restored from `localStorage` on mount (`storage-key` +
    `persist="preferred-mode"`) used to write the resolved mode directly, bypassing
    `setEffectiveMode()` — so a consumer syncing app chrome to the rail's mode never learned a
    persisted preference had been restored. Restoration now fires `lr-mode-change` too, deferred to
    the first `updated()` (after that mount's render and attribute reflection have already landed)
    so the event never precedes the DOM state it describes. No event fires for an ordinary mount
    with nothing persisted, or when the restored mode happens to equal the default.
- 34cdefb: `<lr-gauge>` gains an opt-in `size` on the library's one six-step size ladder
  (`2xs`/`xs`/`s`/`m`/`l`/`xl`, plus the `small`/`medium`/`large` spellings, which are accepted as
  authored rather than rewritten to the short form).
  
  The gauge's whole box has always been expressed in `em`, so a tier simply pins the host font size
  and the frame, the stroke geometry and both SVG captions step together — `8em` square for
  `radial`/`ring`, `12em` by `1.5em` for `linear`, each against that tier's font size.
  
  `size` is genuinely opt-in rather than defaulting to `m`, because a gauge with no tier inherits the
  ambient text size: pinning it would silently resize every gauge sitting in a smaller or larger
  typographic context. Unset, the component renders exactly what it rendered before this property
  existed. An unsupported value normalizes to the omitted state and removes the attribute, so a typo
  falls back to that same pre-ladder rendering instead of snapping to a tier nobody asked for.
- 6cae1c7: `<lr-gauge>` can now recolor its fill from the current `value` instead of a single fixed color.
  
  - New `thresholds` property: an array of `{ at, variant }` entries. The fill uses the last entry
    (sorted by `at`, regardless of authored order) whose `at` is at or below the current `value`,
    falling back to the new `variant` property when `thresholds` is empty or none match. The same
    rule supports both a higher-is-worse mapping (e.g. CPU load: `[{at: 0, variant: 'success'},
    {at: 70, variant: 'warning'}, {at: 90, variant: 'danger'}]`) and a higher-is-better one (e.g.
    battery charge: `[{at: 0, variant: 'danger'}, {at: 20, variant: 'warning'}, {at: 50, variant:
    'success'}]`) — only the authored pairs change.
  - New `variant` property, matching `<lr-progress-bar>`'s shared semantic-tone vocabulary
    (`'neutral'|'brand'|'success'|'warning'|'danger'`, defaulting to `'brand'`).
  
  Leaving both unset renders exactly as before. The existing `--lr-gauge-fill` custom property still
  overrides everything, for a color outside the shared semantic-tone vocabulary.
- 9f0179a: Add `fitTo="container"` to `<lr-graph>` and `<lr-knowledge-graph-explorer>`, so a force graph can
  follow the box it is rendered into instead of a number it has to be told.
  
  `width`/`height` are numbers describing a drawing space, and the SVG is `inline-size: 100%;
  block-size: 100%` with `viewBox="0 0 width height"` and no `preserveAspectRatio` override. Any gap
  between those numbers and the real pane therefore scaled and letterboxed the whole drawing, and the
  layout centred on the wrong midpoint (`forceCenter(width / 2, height / 2)`) — library defaults of
  `800`/`600` draw at 512x384 inside a 1039x384 pane, using barely half of it. Filling a responsive
  card is the ordinary case for a force graph, so the only way out was to hand-roll a host-side
  `ResizeObserver`, a hysteresis threshold so sub-pixel resizes did not thrash, a hard-coded fallback
  width for the frames before the first measurement, and a re-render per resize. Through the explorer
  it was worse: the graph pane's real height is the reservation minus whatever the toolbar, search
  results, pinned row and path strip take, which is not derivable from the public API at all.
  
  The new `fitTo` property (attribute `fit-to`, values `'none' | 'container'`, default `'none'`)
  measures the host's own content box and drives the `viewBox`, the layout's centring force,
  `focusNode()`/`fit()`'s camera math and the loading skeleton from that measurement. Measuring is
  synchronous and happens before the first paint (the host's own height is applied first, so the
  measurement is never one frame of a stale box), so the first painted frame is already the right
  size rather than an 800x600 frame corrected a moment later; every later measurement arrives on the
  existing host-resize watcher (already frame-coalesced) and is rounded to whole CSS pixels, so
  sub-pixel jitter changes nothing. A resize re-centres the running layout in place — `forceCenter`
  plus a low-alpha restart — and never rebuilds the simulation, so settled node positions, pins and
  an in-flight drag all survive. It works in both renderers and across a renderer switch.
  
  `fitTo` does not change how the host itself is sized: an outer `block-size`,
  `--lr-canvas-reserved-height` and `height` still do that, and `'container'` simply follows whichever
  of them won. `fitTo` defaults to `'none'`, where the numeric `width`/`height` remain the explicit
  drawing space exactly as before, so nothing changes for existing usage. `<lr-knowledge-graph-
  explorer>` gains the same property and forwards it, which is what lets its composed graph draw at
  the pane its own layout actually handed over.
  
  `<lr-mind-map>` already derived its own `viewBox` from a self-measured box; this brings `<lr-graph>`
  in line with that sibling.
- c3cad7d: Fix `<lr-graph>`'s `height` regression, add two additive detail slots to
  `<lr-knowledge-graph-explorer>`, and let both respect a node's `accessibleLabel` with a new
  `nodeLabels` visibility control.
  
  `height` stopped sizing the rendered viewport somewhere between the `:host` reservation rule
  (13.0.0) and the pre-upgrade reservation stylesheet (10.0.1) — it kept resizing the SVG
  `viewBox`/canvas backing store, but `:host { block-size: var(--lr-canvas-reserved-height,
  var(--lr-size-24rem)) }` never referenced it, so the rendered box silently stayed at
  `--lr-canvas-reserved-height`'s default (or override) regardless of `height`. `<lr-graph>` now
  writes its normalized `height` to a private `--_lr-graph-requested-height` custom property, and
  `:host`'s `block-size` falls back to it beneath the author-facing `--lr-canvas-reserved-height`
  (`block-size: var(--lr-canvas-reserved-height, var(--_lr-graph-requested-height,
  var(--lr-size-24rem)))`) — an explicit `--lr-canvas-reserved-height` or outer `block-size` still
  wins, exactly like `--lr-chart-height`/`--_lr-chart-height` on `<lr-chart>`. `<lr-knowledge-graph-
  explorer>`'s own `[part="graph"]` rule mirrors the same fallback chain so its composed graph
  responds to `height` the same way once the explorer's own layout gives it room.
  
  `<lr-knowledge-graph-explorer>`'s `details` slot was all-or-nothing: overriding it meant
  reimplementing the whole default card, list, and pin button from scratch. Two new slots are
  additive instead — `detail-body` renders inside the default `lr-entity-card`'s body alongside its
  `lr-neighbor-list`, and `detail-actions` renders into the card's `actions` slot beside the existing
  pin button — so a consumer can append content without giving up the built-in behavior. Neither has
  any effect while `details` itself is overridden.
  
  The explorer's own `nodeLabel()`/`entityFor()` helpers ignored a node's `accessibleLabel`, falling
  straight from `label` to the bare `id` — so a node with only a spoken label (no visible one) showed
  its raw id in search results, pinned chips, and the details popover's name instead of that label.
  Both now resolve `label || accessibleLabel || id`, matching the precedence `<lr-graph>` already uses
  for its own spoken text.
  
  `<lr-graph>` also gains a `nodeLabels: 'always' | 'zoom' | 'none'` property (attribute
  `node-labels`), mirroring `showEdgeLabels`/`edgeLabelMinZoom`'s zoom-gate mechanism for node labels
  too: `'always'` draws every label unconditionally, `'zoom'` hides them below the existing canvas
  declutter threshold in both renderers (a `data-node-labels-hidden` attribute toggled on the zoomed
  `<g>` for `renderer="svg"`, no Lit re-render), and `'none'` never renders them. Left unset, each
  renderer keeps its exact pre-existing default — `'always'` for `renderer="svg"`, `'zoom'` for
  `renderer="canvas"` — so this is a purely additive opt-in. `<lr-knowledge-graph-explorer>` forwards
  its own new `nodeLabels` property straight through to the composed graph.
- 9f0179a: `<lr-heatmap>` can turn its legend off.
  
  - `withoutLegend` (`without-legend`, reflected, `false` by default) hides the colour legend, using
    the same name and the same polarity `<lr-chart>` has always used rather than inventing a third
    spelling for the same idea. Turning it on removes the whole row from the DOM — the gradient bar
    or `legendStops` swatches, the endpoint labels, the `valueLabel` caption, the labelled
    `annotations` entries, and the `legend` slot — so the row contributes no layout box and assigns
    no slotted content, instead of being painted and then hidden.
  - The legend's own preparation stops with it: `--lr-heatmap-color-steps-gradient`, the custom
    property this component writes onto the host for the legend bar and for nothing else, is not
    written while the legend is hidden, and is removed again if it had been.
  - Cells, tooltips, keyboard interaction, selection, and the generated accessible summary are
    unaffected — the summary already names the value label independently of the legend. Leaving
    `withoutLegend` unset reproduces the previous markup exactly, whitespace included.
- 75a4e6d: The shared icon-button tappable-target floor (`--lr-icon-button-size`, and the form-control size
  ladder's own per-tier heights) now grows to the platform touch-target convention (2.75rem/44px)
  under a coarse pointer (`@media (hover: none), (pointer: coarse)`), regardless of what an ancestor
  set the ordinary floor to. Both rules live in one place each — `internal/tokens.styles.ts`'s
  `baseTokens` for `--lr-icon-button-size`, `internal/sizes.styles.ts` for the ladder's
  `--lr-form-control-height` — so every one of the library's icon-only controls and every ladder
  consumer (`lr-button`, `lr-input`, `lr-select`, `lr-combobox`, ...) is covered without touching each
  component's own stylesheet, and no default rendering changes on an ordinary (fine) pointer.
  
  This is the safety net behind a pattern that already worked but was undiscoverable: lowering
  `--lr-theme-icon-button-size` (never `--lr-icon-button-size` itself, which every `LyraElement`
  re-declares on its own `:host` and so never reaches a composed child — see
  `internal/tokens.test.ts`) on an ancestor shrinks every icon-only control beneath it below the
  ordinary 2.5rem/40px floor, for a dense action row that a mouse-only layout can't otherwise afford.
  `<lr-copy-button>` and `<lr-message-actions>` — named in the request this closes — now document that
  override path, plus the equivalent `::part(base__control)` /
  `::part(regenerate-button__control)`/`::part(edit-button__control)` direct overrides, explicitly in
  their own class doc and in `llms/utility.md`/`llms/conversation.md`. With the coarse-pointer floor
  in place, that shrink is now safe to ship: whatever a dense-row layout lowered the floor to on a
  fine pointer, the rendered hit area still floors at 2.75rem/44px the moment the pointer reaching it
  is a finger rather than a mouse.
  
  An ancestor density switch and a per-component `size`/`compact` property were both considered and
  declined for this request: the token- and part-level overrides above already reach the same result
  without inventing a second sizing system on top of the library's one six-step ladder.
- 9f0179a: `<lr-icon-button>` gains a lean registration entry for slot-only consumers.
  
  `components/forms/icon-button/icon-button.js` eagerly imports `<lr-icon>`, because an `icon`/`src`
  attribute renders a nested `<lr-icon>` and that has to be registered synchronously — the common
  case must not paint its glyph a frame late. A consumer who only ever slots their own SVG paid for
  that anyway: `<lr-icon>`'s implementation landed in the entry chunk, and its guarded remote-SVG
  loader dragged in a sanitizer chunk that such a consumer could never reach.
  
  `components/forms/icon-button/icon-button-register.js` registers `<lr-icon-button>` and nothing
  else. Import it instead of `icon-button.js` when every icon button in your graph slots its content:
  
  ```js
  import '@aceshooting/lyra-ui/components/forms/icon-button/icon-button-register.js';
  ```
  
  Setting `icon`/`src` on a button registered this way renders no glyph until `<lr-icon>` is
  registered by something else — that is the whole trade, and it is why this is an explicit opt-out
  rather than a mount-time dynamic import: a lazy import would have made the common path (an `icon`
  attribute) asynchronous for everyone, risking flicker and layout shift on the most frequent usage
  in order to help the rarer slot-only one. One caveat recorded honestly: the sanitizer chunk sits
  behind a dynamic import, so it is deploy weight rather than first-paint weight — the real
  first-paint saving is `<lr-icon>`'s own code leaving the entry chunk.
  
  Every first-party component that composes an icon button internally (`<lr-copy-button>`,
  `<lr-dialog>`, `<lr-drawer>`, `<lr-reorder-item>`, `<lr-message-actions>`,
  `<lr-attachment-trigger>`, `<lr-code-block>`, `<lr-code-block-core>`, `<lr-callout>`) now uses this
  entry, since each slots its own glyph — so their consumers get the saving without changing
  anything.
- d121ce5: `<lr-input>` gains a declarative `match` constraint for confirm-value pairs (password confirm,
  change-email, and similar), plus documentation for the correct `autocomplete="new-password"` recipe
  on a set/change/reset flow.
  
  Setting `match` to a sibling field's id (resolved in this element's own root, the same
  never-crosses-a-shadow-boundary rule every other idref in the library follows) or to a direct
  element reference makes this field additionally fail validity — a localized `customError`
  (new `matchMismatch` `DEFAULT_STRINGS` key, translated in all ten shipped catalogs) — whenever its
  own value disagrees with the referenced field's, once every other constraint (`required`,
  `pattern`, length, type-specific format) already reports valid. It revalidates automatically on
  either field's own edits, including the referenced field's, through a listener on its own
  `input`/`change` events, so retyping the first half of a pair revalidates the second immediately. A
  `match` that does not resolve to a live element (most commonly a dangling id) is inert rather than
  a permanent block on submission.
  
  There is no dedicated password-purpose preset or component: `llms/forms.md` documents composing
  `type="password"`, `password-toggle`, `autocomplete="new-password"`, and `match` directly for a
  confirm pair, and why `new-password` (never a bare `password`, and never `current-password`) is the
  correct token on a set/change/reset flow.
- 9f0179a: Make `<lr-knowledge-graph-explorer>`'s search find a node by the name it displays for it.
  
  The explorer already resolved a node's human name as `label`, then `accessibleLabel`, then the raw
  `id` — in the search-result rows, the pinned chips, the neighbour rows and the details popover's
  accessible name. Its search *filter* was the one place that never learned the third fallback: it
  matched `id` and `label` only. A consumer whose node ids are machine keys, naming nodes through
  `accessibleLabel` so a dense force layout does not have to draw a visible label on every node, could
  therefore see "Marie Curie" in the results list, chips and popover, and get nothing at all by typing
  `Marie` — the name the component itself had just shown them.
  
  The filter now matches `id`, `label` and `accessibleLabel`, each folded with the active locale, so
  every name a node can be known by is searchable. A node carrying both a `label` and an
  `accessibleLabel` matches either: `accessibleLabel` is documented as the richer spoken form of the
  same node, so matching it can only make that node findable, never surface an unrelated one.
  Matching on `id` and `label` is unchanged, and an empty query still shows no result list and applies
  no search dimming.
- de8d9b6: Added `getRegisteredLyraLocaleKeys(locale)` to `@aceshooting/lyra-ui/localization.js`: a frozen
  snapshot of exactly the keys a registered locale's own catalog carries, with no BCP-47
  fallback-chain widening and no merge with the built-in English defaults. Diff its length against
  `Object.keys(LYRA_DEFAULT_STRINGS).length` to measure a locale's own translation coverage without a
  silent English-fallback merge making a partial catalog look complete.
  
  In development builds (silent in production), resolving a message for a non-English resolved
  locale that has no override, no fallback, and no registered catalog entry for that key now warns
  once per (locale, key) to `console.warn` before silently falling back to the English default.
  
  Added `resolveLyraScopedString(host, key, defaults, overrides?, fallback?, values?)` to
  `@aceshooting/lyra-ui/utilities/localization.js`: a scoped variant of `resolveLyraString()` that
  resolves against a caller-supplied `defaults` record instead of the complete built-in English
  catalog, so an application resolving a handful of its own messages never has to pull in the whole
  compatibility catalog to do it.
- 96f604c: `<lr-map>` legend rows can now show the glyph their point layer draws, instead of describing it in
  color alone.
  
  - A `legend` entry accepts an optional `icon`, deliberately the same record `point.icons` already
    carries: pass the very icon object the layer renders and the key reproduces the symbol on the
    map. Because a legend row has no category to match, the point icon's `value` is accepted (so a
    pass-through needs no reshaping) and left out of the canonical readback; every other field —
    `path`, `viewBox`, `mode`, `strokeWidth`, `lineCap`, `lineJoin` — keeps its point-icon meaning
    and its point-icon default.
  - The glyph renders inside the existing `[part="legend-swatch"]`, which carries `data-icon="true"`
    and paints the shape in the entry's own `color`; the solid color block and the `pattern` overlay
    are dropped for that row, since both would sit on top of the shape they are meant to identify.
    The `pattern` border is not: it frames the swatch rather than covering it, so a glyph row keeps
    the same solid/dashed/dotted/double edge a color-only row carries. That matters under
    `forced-colors: active`, where every authored color collapses to one system color and the border
    is the only non-color differentiator left — and in normal mode for two rows that share one glyph
    and differ only by category color. The glyph is decorative: the row's own visible label carries
    its meaning, and the swatch stays `aria-hidden` and `inert`.
  - Validation is the point icon's own, now literally shared with it: path data only, at most 8192
    characters, positive `viewBox` dimensions, everything else bounded and defaulted. An unusable
    record is dropped and that row keeps rendering exactly the color swatch it renders today — as
    does every entry that supplies no `icon` at all, down to the byte.
  - Fixes an accessibility defect the new coverage surfaced: the legend advertised
    `aria-controls="map-container"` unconditionally, including while the optional `maplibre-gl` peer
    was still loading and after any failure — states in which no map container is in the tree at all.
    A dangling idref is a critical ARIA violation, so the attribute is now withheld until the
    container it names actually exists.
- 13e7ace: Add missing theming hooks to `lr-markdown`/`lr-markdown-core` and `lr-poll-status`, a scroll-height
  cap to `lr-diff-view`, and fix a shiki `languages` map key silently never highlighting — every new
  token byte-identical when unset.
  
  - `lr-markdown`/`lr-markdown-core`: `--lr-markdown-code-bg` makes the background shared by inline
    `code` spans and the fenced `code-block` surface themeable (previously a hardcoded
    `var(--lr-color-brand-quiet)` in both places, with no override hook); `--lr-markdown-code-padding`/
    `--lr-markdown-code-radius` and `--lr-markdown-code-block-padding`/`--lr-markdown-code-block-radius`
    make the previously-hardcoded inline-code and code-block padding/radius themeable too.
  - `lr-poll-status`: the built-in `pause-button`'s hover/pressed paint, previously hardcoded straight
    against the shared `--lr-color-brand-quiet`/`--lr-color-brand` tokens, now reads
    `--lr-poll-status-pause-hover-bg`/`--lr-poll-status-pause-hover-color` and
    `--lr-poll-status-pause-active-bg`/`--lr-poll-status-pause-active-color`, so a consumer can retheme
    just this control without repainting every other brand-quiet surface.
  - `lr-diff-view`: a new `maxHeight` property (attribute `max-height`) and `--lr-diff-view-max-height`
    token (default `none`) let the view cap its own scroll height and scroll internally instead of
    growing the page, mirroring `lr-json-viewer`'s identical `maxHeight` property/token pair.
  - Fixed a shiki `languages` map entry keyed by anything other than the grammar's own registered
    `name`/`aliases` (e.g. reusing a TypeScript grammar under the key `tsx`) silently never
    highlighting in `lr-markdown`/`lr-markdown-core`, `lr-code-block`/`lr-code-block-core`, and
    `lr-diff-view` — shiki's registry resolved the map key strictly against each grammar's own
    name/aliases and threw `Language \`...\` not found`, which every caller here caught and downgraded
    to the plain-text fallback with no visible error. A `langAlias` map is now derived for exactly the
    keys that need it and passed to shiki's fine-grained `createHighlighterCore()`, so the author's
    chosen key highlights.
- 88e9556: Add missing CSS custom-property hooks and paint transitions across six components. Every new
  hook's fallback reproduces the exact rendering that shipped before, so nothing changes visually
  until a consumer sets one.
  
  - layout/menu-item: `<lr-menu-item type="checkbox" checked>` gained checked-row chrome hooks
    (`--lr-menu-item-checked-bg`/`--lr-menu-item-checked-color`/`--lr-menu-item-checked-font-weight`),
    matching the checked/selected-state hooks `<lr-option>`, `<lr-select>`, `<lr-combobox>`, and
    `<lr-tree-item>` already expose. Defaults to transparent/inherit/inherit, so an existing checked
    row is unchanged.
  - data/context-meter: `[part="track"]`'s block size, corner radius and background, and the
    hairline seam painted between adjacent bar-shape segments, were fixed to shared global tokens
    with no component-level override. They're now retunable via `--lr-context-meter-track-size`,
    `--lr-context-meter-track-radius`, `--lr-context-meter-track-bg`, and
    `--lr-context-meter-segment-seam-color`.
  - data/stat: introduced `--lr-stat-padding` and `--lr-stat-gap`, read by `::part(base)` in every
    rendering path — the default card, `compact`, `frame="plain"`, and (previously unreachable) the
    linked-card's internal `.linked-content` wrapper. A `href`-linked stat's padding/gap no longer
    goes inert against a `::part(base)` override: both the anchor and its content wrapper now read
    the same tokens.
  - layout/details: `--lr-details-spacing` alone used to drive the summary's and the panel content's
    padding on every axis. It's now joined by four independently-settable hooks —
    `--lr-details-summary-padding-block`, `--lr-details-summary-padding-inline`,
    `--lr-details-content-padding-block-end`, and `--lr-details-content-padding-inline` — mirroring
    how `--lr-details-gap` and `--lr-details-radius` are already independent of each other. Each
    falls through to `--lr-details-spacing` (and, above that, the upstream `--spacing` compatibility
    hook) when unset.
  - layout/control-group: `[part="base"]`'s `inline-size: 100%` previously only applied inside a
    `responsive`-gated `@container` narrow-allocation breakpoint, so a control group given a
    definite width by its host could still render shrink-wrapped. The fill is now unconditional,
    matching the established fill-chain pattern used elsewhere in the library (e.g.
    `<lr-file-input>`): a percentage inline-size against an indefinite/shrink-to-fit containing
    block resolves as `auto` per the flex sizing algorithm, so a toolbar with no explicit host width
    renders exactly as before.
  - forms/icon-button, layout/app-rail-item: hover/press background (and, for the icon button,
    border and foreground) now transition over `--lr-transition-fast`, matching `<lr-button>` and
    `<lr-copy-button>`'s existing paint transitions. `--lr-transition-fast` already collapses to a
    near-zero duration under `prefers-reduced-motion` at the shared token layer, so no additional
    media query was needed.
- 9f0179a: `<lr-multi-split>`: semantic collapse methods, a releasable pin, focus relocation, and a
  floating-drawer inset hook.
  
  - New `expandPane()`, `collapsePane()` and `togglePane()` methods. Each picks the mechanism the
    collapsing pane's **current band** actually provides, so a consumer-built trigger no longer has
    to branch on `collapseState` itself: inside the `floatBreakpoint` band the mechanism is the
    overlay drawer (`open`), above it the mechanism is the `collapseState` pin (`'wide'` for
    expanded, `'rail'` for collapsed). `togglePane()` reads the pane's current presentation —
    `'wide'` counts as expanded, `'rail'` as collapsed, `'floating'` as expanded exactly while
    `open` — so toggling a pane pinned to `'rail'` while the container has narrowed into the
    floating band opens the drawer rather than doing nothing visible. All three are no-ops while
    `collapse="none"` or fewer than two panels exist, they never create a pin the band already
    produces, and a pin they cancel is released rather than replaced, so an expand/collapse cycle
    leaves automatic breakpoint tracking exactly as it found it. They emit no `lr-toggle`, matching
    the existing rule that a direct `open` write does not.
    They are named `…Pane()` rather than `expand()`/`collapse()`/`toggle()` because `collapse` is
    already this component's pane-selection property (`'start'`/`'end'`/`'none'`) and a method
    cannot share that name; the suffixed trio follows `<lr-page>`'s `showNavigation()` precedent.
    **No trigger UI is rendered** — the component still renders no collapsed UI of its own.
  - New opt-in `releasePinOnBreakpoint` (attribute `release-pin-on-breakpoint`, default `false`,
    unchanged behaviour when unset). With it set, a pinned `collapseState` releases itself — exactly
    as if `'auto'` had been assigned — when the measured collapse band changes to a different one
    than the pin was made in, or when `effectiveOrientation` crosses `orientationBreakpoint`.
    Re-measuring the same band never releases a pin, so ordinary resizing inside one band leaves it
    alone. Previously a pin survived every band and orientation change until a consumer wrote
    `'auto'` by hand from an `lr-multi-split-collapse-change`/`lr-multi-split-orientation-change`
    listener, which is what made a pin meant for one layout leak into the next.
  - Focus is now moved out of a pane a collapse transition stops presenting: `'rail'` (clamped and
    clipped) and `'floating'` while closed (hidden outright). Focus lands on the first surviving
    pane that can take it, otherwise on the split's own divider, and focus anywhere other than the
    collapsing pane is left strictly alone. Previously focus stayed on a control that had just been
    clipped away, or was dropped to the document body when the drawer closed. The open floating
    drawer is unchanged — the overlay manager already owns focus there.
  - New `--lr-multi-split-floating-panel-inset` (default `0`): the `'floating'` drawer's distance
    from `[part="base"]`'s edges, applied to both block insets and to whichever logical inline edge
    `collapse` anchors the drawer to, so one declaration insets all three anchored edges. Unset, the
    drawer stays flush with its container exactly as before.
- ff72668: `<lr-multi-split>`: divider theming hooks, and two floating-panel geometry defects fixed.
  
  - Divider hairline color is now themeable: `--lr-multi-split-divider-color` (resting, default
    `var(--lr-color-border)`), `--lr-multi-split-divider-hover-color` (default
    `var(--lr-color-brand)`), and `--lr-multi-split-divider-active-color` (default the existing
    pressed `color-mix()`). A new `--lr-multi-split-divider-thickness` (default `var(--lr-size-3px)`)
    separates the painted hairline's width from `--lr-multi-split-divider-target-size`, so either can
    be tuned without affecting the other — retuning the paint thickness alone can never shrink the
    WCAG 2.5.8 pointer target, which stays governed solely by `--lr-multi-split-divider-target-size`.
    All four are byte-identical when unset.
  - The `'floating'` collapse state's closed drawer (`panel.hidden = true`) had no `[hidden]` rule in
    the component's own stylesheet, so an ordinary (non-`!important`) author `display` rule targeting
    the panel directly silently re-showed it despite `hidden` being set. `::slotted([hidden])` now
    restates `display: none` with the `!important` this specific defect requires: per CSS Cascade 5's
    shadow-tree encapsulation-context ordering, a normal-weight rule in the slotted element's own
    (light-DOM) tree can already outrank a same-specificity `::slotted()` rule in this shadow tree, so
    only `!important` reliably wins back the panel's actually-hidden state for every author
    specificity, not just some.
  - `::slotted(*)` gains `box-sizing: border-box`, since it does not inherit across the slot boundary;
    a slotted panel with its own padding/border no longer overflows the percent/flex-basis allocation
    computed for it.
  - The `'floating'` overlay card's `inline-size` (mirroring its own live `sizes[i]` percent) is now
    written as `var(--lr-multi-split-floating-panel-inline-size, ${percent}%)` instead of a bare
    literal, so a consumer can override the geometry through that custom property at ordinary
    specificity instead of needing `!important` against an inline style rewritten on every render.
    Unset, the rendered geometry is identical to before.
  
  The floating panel's `position`/`inset-block`/`inset-inline-*` were already moved to ordinary,
  `!important`-free stylesheet rules in a prior change (see `multi-split.class.ts`'s comment above
  `OWNED_PANEL_STYLE_PROPERTIES` and the corresponding `::slotted([data-collapse-state='floating'])`
  rule in `multi-split.styles.ts`) and needed no further change here.
- 96f604c: Picker family: a label-only option mode for `<lr-locale-picker>`, and published width/height
  geometry on `<lr-model-select>` and `<lr-voice-picker>`.
  
  - **`<lr-locale-picker>` gains `optionDisplay: 'label' | 'label-tag'`** (attribute
    `option-display`, default `'label-tag'` — today's two-line row). Under `'label'` the row renders
    the locale's label alone and the `option-tag` element is **omitted from the DOM**, not hidden with
    CSS: a visually hidden tag would still join the row's accessible name and would still be matched
    by a consumer's `::part(option-tag)` rule, so hiding is not omitting. Selection, flags, the
    trigger and every other option part are unchanged in both modes, and leaving the property unset
    paints exactly as before.
  - **`<lr-model-select>`'s 24rem host width ceiling is now `--lr-model-select-max-inline-size`**,
    defaulting to that same `var(--lr-size-24rem)`. The cap was kept rather than dropped to match
    `<lr-select>`'s uncapped host, because a model row usually sits in a settings card or composer
    toolbar where an uncapped control stretches across the whole container; publishing it means a
    full-width row is now one declaration (`--lr-model-select-max-inline-size: none`) instead of a
    `::part`/descendant override. The token is read as a `var()` fallback and never declared on
    `:host`, so a value set on an ancestor theme wrapper still reaches the control.
  - **`--lr-model-select-trigger-height` pins an exact trigger height.** The existing
    `--lr-model-select-trigger-min-height` remains a floor; the new name floors *and* caps the
    trigger (and the free-text combobox), so the picker can pixel-match a sibling field in the same
    toolbar row. It takes precedence over the floor, mirroring `<lr-select>`'s identical pair.
  - **`<lr-voice-picker>` had the same two gaps and gets the same treatment.**
    `--lr-voice-picker-max-inline-size` publishes its identical 24rem
    ceiling, and `--lr-voice-picker-trigger-min-height` / `--lr-voice-picker-trigger-height` replace a
    hard-wired `min-block-size: var(--lr-form-control-height)`. To be precise about what changed: the
    old values were always *overridable* — `trigger` is a documented part, and a normal
    `lr-voice-picker::part(trigger) { min-block-size: … }` rule in the outer tree outranks a shadow
    rule, just as `lr-model-select { max-inline-size: none }` has always outranked a `:host` rule.
    What they were not is *themeable*: every override had to name a part or a tag and be written at
    the element, so it could not be set once on a theme wrapper or `:root` and inherited, and it
    could not be expressed in the same `--lr-*` vocabulary as the rest of the component. That is the
    win here — one inherited custom property in place of a per-element `::part`/descendant rule.
    Defaults are byte-identical to what shipped.
  - **`<lr-voice-picker>`'s preview action now follows `--lr-voice-picker-trigger-height` too.** The
    action sits in the same flex row as the trigger under `align-items: stretch`, and stretch does
    not apply to an item with a definite cross size — so pinning the field left a short, top-aligned
    square beside a taller field. It now reads the same name, with its WCAG hit-area floor kept
    below the hook so a short pin cannot shrink the target.
  - **Three more components carried the identical un-themeable shape and are fixed in the same
    change**: `<lr-menu>`'s host and submenu surfaces publish
    `--lr-menu-max-inline-size` / `--lr-menu-min-inline-size` (the floor moves with the ceiling, so a
    cap below 10rem is no longer silently ignored; the viewport clamp and container allocation stay
    outside the hook, so no value can make a menu overflow), `<lr-model-settings-panel>`'s card cap
    publishes `--lr-model-settings-panel-max-inline-size`, and `<lr-time-input>` gains the
    `--lr-time-input-control-min-height` / `--lr-time-input-control-height` pair that `<lr-input>`
    and `<lr-date-input>` already had. `<lr-model-settings-panel>` also stops reaching past its
    nested select with a descendant `max-inline-size: none` and sets
    `--lr-model-select-max-inline-size: none` on its `model-row` part instead, which leaves that
    documented part as a re-cap point the old rule made unreachable.
  
  Also recorded, with no behaviour change: the picker family deliberately does **not** adopt
  `<lr-select>`/`<lr-combobox>`'s corrected "an empty string is a candidate value" contract, and the
  reasoning now lives next to the code, with a characterization test pinning it. A catalog row's `id` must be a nonblank string, so `''` can never name a row in
  `<lr-model-select>`/`<lr-voice-picker>`; a BCP-47 tag always has a primary language subtag, so `''`
  can never name a locale row either. In all three, `''` stays the single documented "nothing
  committed" sentinel that the placeholder, the preview and the `valueMissing` constraint are built
  on. `<lr-locale-picker>` likewise keeps falling back to a derived endonym for a value with no
  matching row rather than adopting the "not in catalog" badge: with `locales` unset its catalog
  tracks the live locale registry, so a value restored from a profile is routinely
  legitimate-but-not-yet-listed, and badging it would be the same false alarm `<lr-combobox>`
  suppresses while an async `source` fetch is still in flight.
- 9f0179a: `<lr-select>` and `<lr-combobox>` can show an out-of-list value as a re-selectable listbox row
  
  A committed value that matches no option has always rendered on the trigger with a dashed "not in
  catalog" badge — but it appeared nowhere in the listbox, so a user who opened the listbox had no
  way back to the value they arrived with.
  
  `show-unknown-option` appends that value to the end of the listbox as a synthetic, badged row: the
  policy `<lr-model-select>` already ships. It is a real row, so it is keyboard-reachable, type-ahead
  reachable and re-selectable, it toggles like any other row in `multiple` mode, and it disappears
  the moment a real option claims the value. Off by default, because it adds a row to a listbox that
  has only ever rendered authored options.
  
  `getUnknownLabel(value)` renders the label for such a value wherever it appears — the trigger, a
  `multiple` tag, and the synthetic row. The existing `getTag` hook cannot serve this case: it is
  handed a matched option, and by definition there is none. A blank return falls back to the raw
  value, and the hook is never consulted for a value a real option does claim.
- 9f0179a: `<lr-select>` and `<lr-combobox>` are now generic on `multiple`, so TypeScript narrows their value
  
  `LyraSelect<false>`/`LyraCombobox<false>` type `value` and `defaultValue` as `string`;
  `LyraSelect<true>`/`LyraCombobox<true>` type them as `string[]`. The `lr-change`/`lr-input`/
  `input`/`change` detail `value` narrows with the same parameter, and the two shapes are also
  exported as `LyraPickerValue<Multiple>` and `LyraPickerDetailValue<Multiple>` for a host that wants
  to name them directly.
  
  This is types only: the runtime, the reflected attributes, and the mirrored Web Awesome / Shoelace
  surface are all unchanged. The type parameter defaults to `boolean`, so an untyped `<lr-select>` —
  including everything reached through `document.querySelector('lr-select')` — keeps exactly today's
  `string | string[]` union and compiles unchanged.
  
  Migration tip (not required): a project that already carried its own `as string` narrowing around a
  single-select `value` can delete it, and its own linter may now flag it as unnecessary.
- 9f0179a: `<lr-popover>` gains hover, focus and manual trigger modes with delays and a hover bridge
  
  `trigger="click" | "hover" | "focus" | "manual"` selects which interaction opens the surface, and
  defaults to `click` — today's behaviour, unchanged for every existing popover.
  
  The two transient modes are built for the cases a click-only surface could not serve:
  
  - `show-delay` and `hide-delay` (both `0` by default) put a grace period around opening and
    closing, so a pointer grazing the trigger does not flash the surface open.
  - `hover-bridge` has the positioner clip an invisible quad across the `distance` gap between the
    trigger and the popup, so travelling between them never leaves both at once.
  - A hover- or focus-opened surface **never moves focus into itself**, so it cannot take the caret
    from whatever the user is actually typing into.
  - Focus resting anywhere inside the surface keeps it open, so a keyboard user can tab from the
    trigger straight into the content.
  - Clicking the trigger **pins** a transient surface open; clicking again releases the pin and
    closes it.
  
  `trigger="manual"` refuses every interaction and leaves the surface entirely to
  `show()`/`hide()`/`open`. `<lr-dropdown>` inherits the whole contract, menu focus included.
- 9f0179a: One `positioning-strategy` property across every anchored surface, with `hoist` retained as its alias
  
  `<lr-popover>`, `<lr-dropdown>`, `<lr-select>`, `<lr-tooltip>` and `<lr-color-picker>` now all
  accept `positioning-strategy="absolute" | "fixed"` — the same name, the same values, the same
  meaning. Each component keeps its own default, so nothing renders differently until you set it:
  `<lr-popover>` stays `fixed` (previously hard-coded and unreachable, now a real property you can
  turn off), and the other four stay `absolute`.
  
  Where `hoist` already existed it keeps working indefinitely, as the exact boolean alias of
  `positioning-strategy="fixed"`. Writing either spelling updates the other, so the two attributes
  can never disagree in the DOM, and an unsupported value resolves back to the component's own
  default. Prefer `positioning-strategy` in new code; `hoist` remains supported for the Shoelace
  spelling it mirrors.
- e81ad22: Three fixes to `<lr-select>` and `<lr-combobox>`'s controlled-value contract and popup rendering.
  
  - **An empty-valued `<lr-option>` is now a stable controlled
    selection.** Both pickers' `value`/`defaultValue` setters used to test the incoming value with a
    bare truthiness check (`next ? [next] : []`), so assigning `''` was indistinguishable from
    clearing the selection -- even when an option declared `value=""`. The contract is now explicit:
    assigning `undefined` or `null` clears the selection (the documented "unset" behavior, unchanged);
    every string, INCLUDING `''`, is instead a candidate value resolved against the current options,
    exactly like any other string. `value`/`defaultValue`'s setter types widen to accept
    `string | string[] | null | undefined` to make the `undefined`/`null` "clear" contract explicit at
    the type level; their getters are unchanged. This closes the asymmetry where clicking an
    empty-valued option already selected it (`lr-select`'s pointer path builds its committed array
    directly, with no truthiness filtering) while assigning the same value programmatically silently
    cleared instead -- both components now behave identically across the programmatic and pointer
    paths. A handful of internal call sites that previously used `''` as an implicit "nothing to
    restore/default to" sentinel (`formStateRestoreCallback`, `formResetCallback`,
    `refreshOptionDefaults`, `applySelectedRowValues`, `onOptionChange`'s deselect branch) now pass an
    explicit `[]`/`undefined` instead, so they keep meaning "clear" under the corrected contract.
  - **A committed value matching no option no longer leaks its raw
    string with no explanation.** Both components' `labelFor()`-style resolution already fell back to
    the raw value for a stale or programmatically mistyped value; that raw value is still fully
    reachable through `value`/`selectedOptions`/`selectedRows` and JSDoc's fallback description is
    unchanged. What's new is presentation: mirroring `<lr-model-select>`'s synthetic "not in catalog"
    stale-value row, the closed trigger label (`<lr-select>`) or filter input (`<lr-combobox>`), and
    any `multiple`-mode tag/chip for such a value, now render a dashed/italic
    `[part='unknown-value']` badge (reusing the existing localized `notInCatalog` string) instead of
    an unexplained bare label. New `--lr-select-unknown-value-border-*` /
    `--lr-combobox-unknown-value-border-*` custom properties retheme the dashed border.
    `<lr-combobox>` additionally suppresses the badge while an async `source` fetch is still in
    flight (not yet resolved is not the same as genuinely unknown) and never flags an
    `allowCustomValue` commit, which is a sanctioned unmatched value, not a stale one.
  - **`<lr-select>` now renders `<lr-option>`'s `start`/`end`
    adornments in its listbox rows**, mirroring `<lr-combobox>`'s existing `option-start`/`option-end`
    parts and clone-based rendering (`cloneNode(true)`, never `createElementNS`, so a custom-element
    adornment still upgrades). Previously `<lr-select>`'s rows read only `dotColor`/`label`/`sub` and
    silently dropped a slotted `start`/`end`/`prefix`/`suffix` adornment. A plain option with no
    adornment renders no extra wrapper, and the new parts mirror correctly under `dir="rtl"` like
    every other flex-row adornment pair in the library.
  
  Deferred: a `data` payload channel on `<lr-option>` needs an API design
  decision and is not part of this change.
- 34cdefb: Every field-shaped form control now publishes the same surface quartet — resting fill, resting
  border, hover border and a shared focus halo — so retinting a field no longer means a `::part()`
  rule per control per state. Purely additive: with nothing set, every control renders exactly as it
  did before.
  
  `<lr-select>` gains `--lr-select-trigger-fill`, `--lr-select-trigger-border-color` and
  `--lr-select-trigger-hover-border-color`. The first two are read by **every** appearance, each
  falling back to that appearance's own default (`--lr-color-surface-raised` for
  `filled`/`filled-outlined`, `transparent` for `plain`, `--lr-color-brand` for `accent`), so one
  value retints the trigger whichever treatment it is wearing — a hook wired only into the resting
  rule would have been dead for five of the six. The hover hook falls back to the resting border, so
  an unset control's hovered edge does not move, exactly as before.
  
  `<lr-locale-picker>` gains the matching `--lr-locale-picker-trigger-fill`,
  `-trigger-border-color` and `-trigger-hover-border-color`, and its trigger now eases its border
  colour alongside its background instead of easing one and snapping the other.
  
  `<lr-combobox>`'s trigger fill and border were private properties with no public arm; they are now
  `--lr-combobox-fill` and `--lr-combobox-border-color`. Its focused edge — the state the listbox
  opens in — was a hardcoded brand border and is now `--lr-combobox-open-border-color` (default
  `var(--lr-color-brand)`).
  
  The same gap existed on four siblings and is closed with them. `<lr-date-input>`'s resting row gains
  `--lr-date-input-fill` and `--lr-date-input-border-color` (its radius has always been public).
  `<lr-file-input>`'s resting dropzone gains `--lr-file-input-dropzone-fill`,
  `--lr-file-input-dropzone-border-color` and `--lr-file-input-dropzone-hover-border-color` — until
  now only its drag accept/reject tints were themeable, and the state the dropzone spends most of its
  life in was not. `<lr-phone-input>` and `<lr-token-input>` gain `--lr-phone-input-fill`/
  `--lr-phone-input-border-color` and `--lr-token-input-fill`/`--lr-token-input-border-color`; both
  already published a focused-border hook, and both still win over the resting pair, as does each
  control's invalid state.
  
  `--lr-form-control-focus-shadow` (default `none`) is one halo for all of them:
  `<lr-input>`, `<lr-textarea>`, `<lr-select>`, `<lr-combobox>`, `<lr-locale-picker>`,
  `<lr-date-input>`, `<lr-file-input>`, `<lr-phone-input>`, `<lr-token-input>` and `<lr-time-input>`
  each paint it as a `box-shadow` while focused (or open). `<lr-otp-input>` is the one field-shaped
  control left out: its focused segment already draws its own focus ring with `box-shadow`, and a
  shared `box-shadow` declaration would replace that ring rather than sit outside it.
  It is strictly additive — the focus outline and the focused/open border are the accessibility
  answer to focus and are never replaced by it — and, like the rest of the set, it is read through an
  inline fallback and never declared on `:host`, so one declaration on `:root`, or on any ancestor to
  scope it to a subtree, reaches every field inside it.
- 96f604c: Nine single-select controls gain one shared `lr-activate` event, a notification that fires on every
  user activation of an option — including re-picking the one that is already selected.
  
  Every change event in the library is change-only: it fires when, and only when, the value moves.
  That left a whole class of intent unobservable. Re-picking the current option is a real user action
  — "run that report again", "reload that panel", "re-fetch this page" — and these components
  reported it nowhere. A host could half-cover the pointer case by listening for `click` (the shadow
  button's click is composed, so it does escape), but the click retargets to the host and names no
  option, and keyboard activation produces no click at all: Home on an already-first selection, End
  on an already-last one, or an arrow key at a bound all activate an option and emitted nothing.
  
  | Component | `detail.value` | Silent change event it complements |
  | --- | --- | --- |
  | `<lr-segmented>` | the segment's `value` | `lr-change` |
  | `<lr-swatch-picker>` | the swatch's `value` | `lr-change` |
  | `<lr-select>` | the activated option's `value` | `change` / `lr-change` |
  | `<lr-combobox>` | the activated row's `value` | `change` / `lr-change` |
  | `<lr-rating>` | the committed rating (number) | `change` / `lr-change` |
  | `<lr-pagination>` | the requested page (number) | `lr-page-change` |
  | `<lr-tab-group>` | the activated tab's panel name | `lr-tab-show` |
  | `<lr-widget>` | the activated view's `viewId` | `lr-view-change` |
  | `<lr-knowledge-base-admin>` | the activated tab | `lr-tab-change` |
  
  One name rather than nine, so a host learns the contract once. Each component keeps its own precise
  `detail` type, so `LyraSelectEventMap['lr-activate']` stays exact while a document-level listener
  sees a single `lr-activate`.
  
  - Bubbling and composed, so a host outside the shadow tree receives it without piercing the shadow
    boundary.
  - Not cancelable. It reports that the user picked an option; it does not gate anything, and nothing
    in any of these components branches on it. Where the component already has a cancelable proposal
    (`lr-before-page-change`, `lr-view-request`), that proposal remains the veto point and a vetoed
    activation emits no `lr-activate` at all.
  - A disabled/unavailable option still activates nothing — no event fires for it.
  - When an activation does move the value, the component's own change events are emitted first, so a
    listener reading the value from any of them sees the settled state.
  - Programmatic assignment is never an activation. Writing `value`, `page`, `active`, `activeViewId`
    or `activeTab` from the host, and `<lr-tab-group>`'s `show()` method, fire nothing.
  
  Composites that already contain their children's raw events contain this one too, so no component
  starts emitting an event it never documented. All eleven: `lr-condition-builder`,
  `lr-graph-query-builder`, `lr-rubric-form`, `lr-retrieval-search`, `lr-entity-dossier`,
  `lr-spreadsheet-viewer`, `lr-table`, `lr-drilldown-panel`, `lr-filter-bar`, `lr-document-library` and
  `lr-tool-param-form` swallow the `lr-activate` of the
  `lr-select`/`lr-combobox`/`lr-segmented`/`lr-tab-group`/`lr-pagination` they compose, exactly as they
  already swallow its `lr-change`/`lr-tab-show`/`lr-page-change`.
  
  Purely additive: every existing event's timing, detail and silence-on-re-pick are unchanged, so an
  application that listens only for the change events behaves exactly as before.
- 96f604c: `<lr-stepper>` gains a `readonly` boolean that renders the same `steps` data as a passive progress
  display instead of a navigable control.
  
  While `readonly` is set, every step renders as a non-interactive item inside its existing
  `role="listitem"` wrapper rather than as a `<button>`: no `tabindex` on any step, no
  `aria-disabled`, no click or Enter/Space activation, and therefore no
  `lr-step-select` — including from a synthetic click dispatched at `::part(step)`. Arrow/Home/End
  keys are a no-op and no longer call `preventDefault()`, so Space keeps scrolling the page the way
  it does anywhere else in static content.
  
  Everything that describes *progress* is untouched: the numbered index chip, the completed
  checkmark, the optional topic icon, the per-step `title`, `aria-current="step"` on the current
  step, and every `--lr-stepper-*` custom property still apply exactly as before.
  
  This is deliberately not a disabled treatment. `disabled` says "you may not do this"; read-only
  says "there is nothing to do here" — so a read-only step keeps normal opacity and simply loses its
  pointer cursor, matching what `<lr-slider>` and `<lr-rating>` already do for their own `readonly`.
  Dimming a progress display reads as broken rather than as informational. A per-step `disabled`
  flag is inert while read-only for the same reason: there is no activation left for it to gate, so
  it contributes no dimming either, and the step's progress state still renders.
  
  Because no step is tabbable while `readonly`, a horizontal strip that genuinely overflows moves
  the single tab stop onto its own scroll container: `[part="base"]` takes `tabindex="0"` (and a
  `::part(base):focus-visible` ring) while, and only while, both conditions hold. Without it an
  overflowing read-only strip would be a scrollable region with no keyboard access at all — the
  off-screen steps unreachable, and an axe `scrollable-region-focusable` violation. A read-only
  strip that fits, a vertical one, and every interactive stepper still leave that container out of
  the tab order, so a stepper never costs more than one tab.
  
  `readonly` defaults to `false`, and unset behavior — button semantics, roving tabindex, hover and
  pressed treatments, and `lr-step-select` — is byte-for-byte unchanged. The internal hover and
  pressed rules now name the `button` element inside their existing `:where()` wrapper, which costs
  no specificity and leaves a consumer's `::part(step):hover` override winning exactly as it did
  before.
- 32a2ee3: `<lr-swatch-picker>` gains a wrapping hook, and the selected-gemstone presentation becomes
  importable so a trigger outside the picker can match it.
  
  - `--lr-swatch-picker-wrap` (default `wrap`) sets `flex-wrap` on the swatch row. The row's wrapping
    was hardcoded while `--lr-swatch-picker-gap` and `--lr-swatch-picker-hit-size` were already
    themeable, so a picker inside a fixed-width popover — where a second row changes the panel height
    and moves the popover under its trigger — had to reach past the API with a
    `::part(base) { flex-wrap: nowrap; }` rule. A custom property rather than an attribute, so it
    inherits through wrappers like the gap and hit-size hooks beside it. The default reproduces
    today's rendering byte-identically.
  - New `gemstoneSelectedGlyphStyles` export (a `CSSResult`, alongside `gemstoneGlyph()`) carries the
    treatment that makes a gemstone read as *selected*: the looping brightness shine, the coloured
    drop-shadow halo, and the reduced-motion rule. Until now only the glyph markup was exported and
    the presentation lived inside the picker's own stylesheet, so an application header showing the
    active accent on its own trigger button — two views of one piece of state, a few pixels apart —
    had to re-implement the keyframes, the halo and the reduced-motion behaviour and could then drift
    from the picker at the next release. Opt in by importing the stylesheet and setting the documented
    `data-lr-gemstone-selected` attribute on the element wrapping the glyph; it is themeable through
    `--lr-gemstone-selected-color`, `--lr-gemstone-selected-blur` and
    `--lr-gemstone-selected-shine-duration`.
  
  The reduced-motion branch hand-writes `animation: none` rather than relying on the ambient duration
  token collapsing: that token shortens the loop to an imperceptible 0.001ms but leaves it *infinite*,
  which is not a stopped animation. Anything forking this treatment had to know that; now nothing has
  to.
- 9f0179a: Card parity pass: a default-tier background token, a real `disabled` state, and toggle/current state on the control that actually carries the action — across `<lr-card>`, `<lr-media-card>` and `<lr-source-card>`, with the background token extended to every remaining card-like surface that had per-state hooks but no lever for the tier it actually renders at.
  
  **A background hook for the tier you actually render.** Each card documented per-state background tokens for everything except its own resting default, which was wired straight to `--lr-color-surface`. Retinting one themed card therefore meant a raw `::part(base)` rule or an app-wide surface change, while the identically shaped need one tier over was a one-line custom property. Seven new tokens close that: `--lr-card-outlined-bg` (the default `outlined` appearance, and `accent`, which adds a stripe without restating a surface — mirroring `<lr-details>`'s `--lr-details-outlined-bg`), `--lr-media-card-bg` (the resting `frame="card"` chrome, companion to the existing `--lr-media-card-active-bg`), `--lr-source-card-bg`, `--lr-entity-card-bg` and `--lr-confirm-bar-bg` (each the companion to that component's existing `compact` padding/gap levers), `--lr-stat-bg` (companion to the existing `--lr-stat-padding`/`--lr-stat-gap` and to a linked tile's pressed `--lr-stat-link-active-bg`), and `--lr-community-card-bg` (so a GraphRAG panel retints every card it renders rather than stopping at the siblings that happened to own a token). Every card component in the library now has one. Leave them unset and every surface paints exactly as before; `frame="plain"` still opts out of chrome entirely. Two pressed-state defaults now mix from the new resting token rather than from `--lr-color-surface` directly (`--lr-media-card-active-bg`, `--lr-stat-link-active-bg`), so one override retints resting and pressed together.
  
  **`disabled`.** All three cards now take a reflected `disabled` boolean that turns the card's own activation off rather than leaving you to restyle a disableable button into a card-shaped tile:
  
  - `<lr-card>` renders its `activation-button` `disabled`; a linked card's stretched `<a>` loses its `href`, so it genuinely cannot navigate rather than merely claiming to be disabled while still clickable. `lr-card-activate` stops firing from every path, `click()` included.
  - `<lr-media-card>` disables the image action and the video `open-button`, and a file chip's anchor loses its `href`/`download`, so `lr-media-open`/`lr-before-media-download` stop firing. The `kind="video"` player keeps its own native transport — that is media, not the card's action.
  - `<lr-source-card>` gates every self-rendered control, the "Show more" toggle included, so neither one can emit `lr-open` or `lr-expand`. The one `lr-expand` a disabled card still emits is the automatic collapse when the `full` slot empties while expanded — that reports a state change the card genuinely made, exactly as it does when enabled, and swallowing it would desync a host tracking expansion off the event stream.
  
  In each case the card leaves the tab order and paints at `--lr-opacity-disabled` with a `not-allowed` cursor. `disabled` describes the card's own action, so a passive card with nothing to turn off is left untouched, and slotted content stays yours to disable. None of the three is form-associated (a card is a container, not a form control), so an ancestor `<fieldset disabled>` does not cascade into one — disable each card explicitly.
  
  One DOM note if you assert on rendered markup: a linked `<lr-card>` and a safe `<lr-media-card>` file chip now always carry `aria-disabled`, rendering `"false"` when enabled rather than omitting the attribute. A stateful ARIA attribute has to express both states to be readable at all.
  
  **`aria-pressed` / `aria-current` forwarding.** A host `aria-pressed` or `aria-current` now reaches the native control inside the card — `<lr-card>`'s `activation-button`, `<lr-media-card>`'s image button or video `open-button`, `<lr-source-card>`'s `title` button — the same mechanism `<lr-button>` and `<lr-icon-button>` already use, with the same validated vocabularies (`true`/`false`/`mixed`; `page`/`step`/`location`/`date`/`time`/`true`/`false`) and the same "an unrecognized token is dropped, not passed through" rule. That is what lets a single-select list of card-shaped tiles announce which one is the active selection without hand-rolling a `<button>` that re-implements the card's bordered, hoverable, keyboard-activatable chrome.
  
  Two deliberate refinements over a literal copy of that mechanism, because an anchor is not a button: `aria-pressed` reaches buttons only (`link` has no pressed state, and asserting one there is an ARIA conformance failure), while the global `aria-current` reaches links too — its commonest use. And a disabled link, whose dropped `href` also drops its implicit role, now carries an explicit `role="link"` so its accessible name and `aria-current` remain valid rather than becoming prohibited attributes on a generic element.
  
  **Those same two refinements now apply to `<lr-button>` and `<lr-icon-button>`.** Both forwarded `aria-pressed` onto their `<a>` rendering, which `role="link"` does not support, and both left a disabled link button as a role-less generic element still carrying `aria-label`/`aria-haspopup`/`aria-expanded`/`aria-current`. A link button therefore no longer receives `aria-pressed` (remove `href` and the same host attribute reaches the `<button>` that replaces the anchor), and a disabled link button renders `role="link"`.
  
  **Disabled card chrome keys off native state.** Where a card's disabled paint sits on an element that already carries the state — `<lr-media-card>`'s image button and video `open-button`, `<lr-source-card>`'s `title` and `toggle` — the stylesheet now reads that element's own `:disabled` (and the file anchor's `aria-disabled`) instead of a parallel `data-disabled` attribute, and the attribute is gone from those elements. Because only a pseudo-class may follow `::part()`, that also makes `::part(base):disabled`, `::part(open-button):disabled`, `::part(title):disabled` and `::part(toggle):disabled` selectors a consumer can write. `<lr-card>`'s `[part="base"]` div and `<lr-source-card>`'s keep `data-disabled`: neither carries any native state, and `disabled` is reflected on the host, so `lr-card[disabled]::part(base)` styles them from outside.
- 9f0179a: `<lr-switch>` gains `--lr-switch-checked-track-border`, so a bordered track can differ when
  checked.
  
  `--lr-switch-track-border` already let a consumer add a rim to `[part="track"]`, but that rim was
  the same in both states — varying it meant reaching past the token vocabulary for
  `lr-switch:state(checked)::part(track)`. The new hook completes the pair, mirroring the shape
  `--lr-switch-checked-thumb-fill` and `--lr-switch-checked-label-color` already use: it falls back
  to `--lr-switch-track-border`, which itself falls back to no border at all, so an unset consumer
  renders byte-identically to before and a consumer who only sets the resting border keeps getting
  that border in both states. It takes a whole `border` shorthand value, like its resting sibling.
  Give both states the same border *width* unless a size change between them is what you want — the
  track is `box-sizing: content-box`, so a border grows its outer footprint.
- b925025: `<lr-table>`: a new built-in `error` state fills the gap between the existing `loading` and empty
  states — until now, a failed data load had no built-in representation, so a consumer had to swap
  the whole element out on error and lose the header, pagination, and filter context.
  
  - `error` (boolean, reflected) replaces `<tbody>`'s row content with a built-in failed-load
    `<lr-empty>` while keeping the surrounding `<thead>`, filter field, and pagination mounted —
    unlike either existing data-empty branch, which this state overrides and which replace that
    chrome too. `errorHeading`/`errorDescription` override the built-in copy, mirroring
    `emptyHeading`/`emptyDescription`'s own contract; an `error` slot replaces the built-in content
    wholesale, mirroring the existing `empty` slot. The same `error`-prefixed `exportparts` scheme as
    `empty-*` (`error-base`/`error-icon`/`error-heading`/`error-description`/`error-actions`) makes
    it addressable without replacing it.
  - A built-in `[part='retry-button']` emits a cancelable `lr-retry`: the default action clears
    `error`, and `preventDefault()` leaves it set for a consumer that owns its own retry timing.
  - Precedence when more than one state could apply at once: `loading` beats `error` beats every
    empty branch, so a `loading` table never flashes a stale `error`, and an `error` table never
    falls through to "no rows"/"no columns" copy underneath it. That last case is the common one
    rather than an edge case: when the load that would have supplied `columns` is what failed,
    `columns` is still empty, and reporting that as a configuration problem would both blame the
    consumer for a network error and hide the retry control. With no schema to keep mounted, the
    failed-load content renders on its own there — the same way `loading` degrades to a bare spinner
    when `columns` is empty — and still honours the `error` slot, the `error-*` parts, and the retry
    button.
  - A post-mount `error` transition is announced on the shared assertive light-DOM sink (distinct
    from the existing polite sink `loading`/empty-state copy already uses), guarded the same way the
    existing loading announcement guards its own first update.
- 9f0179a: `<lr-table>`'s inert-configuration warning now covers the whole priority-column family, and the
  reveal control's availability is publicly readable.
  
  - The development-mode diagnostic previously fired only for `revealColumnsLabel`/`hideColumnsLabel`.
    It now also names `priorityColumnsVisible` and `storageKey` when either is configured on a table
    where no column declares `priority` — both are equally inert there, since there are no hide rules
    to override and nothing else is persisted. One warning lists every inert member, so a table
    configured with all three does not teach the author to fix them one reload at a time. It stays
    production-silent and page-bounded, as before.
  - The diagnostic no longer fires while `columns` is still empty. A table configured in markup
    routinely receives its columns a tick later, and warning at that point also spent the
    page-bounded diagnostic on a table that was about to be configured correctly.
  - New read-only **`priorityColumnsToggleAvailable`** reports whether the reveal/hide control is
    currently offered at all — the public counterpart of the measurement
    `[part='reveal-columns-button']` itself renders from. It is true while at least one `priority`
    column is actually hidden at the current allocation and stays true once `priorityColumnsVisible`
    has revealed them, so a host can mirror the control (or explain its absence) instead of
    re-measuring the grid itself. Remeasured after every render and container resize; read it after
    `await table.updateComplete`.
- 9f0179a: `<lr-table>` gains public row, cell and expansion-panel element lookups, and its row/column data
  attributes become documented API.
  
  `rowElement(rowKey)` returns the rendered `<tr>` for one row key, `cellElement(rowKey, columnKey)`
  returns the `<td>` at a row/column pair, and `expandedContentElement(rowKey)` returns the
  `[part='expanded-cell']` holding that row's `expandedContent(row)` output — each `null` when that
  row, column or panel is not in the current render output (filtered out, paged away, column removed,
  row collapsed). They exist for code that has to reach content a consumer's own `cell(row)` or
  `expandedContent(row)` callback rendered into the table's shadow root: measuring it, scrolling it
  into view, or applying something `::part()` cannot express, since only pseudo-classes may follow a
  part selector.
  
  There is one method per callback because the expansion panel is a **sibling** `<tr>` of the data row
  rather than a descendant of it: `rowElement`/`cellElement` reach `cell(row)` output only, and
  `expandedContentElement` is the route to `expandedContent(row)` output.
  
  All three read the DOM as it stands, so `await table.updateComplete` before calling them after
  changing any input.
  
  The `data-row-key`, `data-col-key` and `data-expanded-row-key` attributes those elements carry are
  now documented stable API rather than an implementation detail. `data-col-key` is the column's own
  `key`; `data-row-key` (on the data row) and `data-expanded-row-key` (on the panel row) are a
  type-tagged encoding of the row key (`string:a` vs `number:1`) that keeps a numeric key distinct from
  the string that stringifies the same way. The panel deliberately does not repeat `data-row-key`, so
  every `[data-row-key]` query still resolves exactly one element per row. Prefer the methods over
  building a selector from any of them: a consumer-supplied key is not safe to interpolate into CSS
  unescaped, which is exactly what the methods avoid.
- 8675202: `<lr-table>`: `columns[].editType` gains `'select'`, so a closed-set column (status, priority,
  owner, ...) can use the built-in inline-editing path instead of hand-rendering a full `lr-select`
  plus its options inside `cell()` for every row -- a workaround that mounted one live component per
  rendered row and re-mounted on every filter change.
  
  - `editType: 'select'` renders a native `<select>` in place of the existing text/number `<input>`,
    populated from the new `columns[].editOptions: { value: string; label: string }[]`, one
    `<option>` per entry in order. A column with no `editOptions` renders an empty, valueless
    `<select>` instead of throwing.
  - The select editor reuses the existing `lr-cell-edit` event, `editTrigger: 'double-click'` /
    `'always'` lifecycle, and the documented Enter-commits / Escape-cancels key contract unchanged —
    no new event, no new trigger, and no general `renderEditor(row)` seam. Exactly one editor is
    still mounted at a time under `'double-click'`; a resting cell renders plain text with no control
    mounted at all, which is the whole point of routing a closed-set column through this path instead
    of `cell()`.
  - Each select editor keeps the same interpolated `tableEditCell` accessible name (`Edit {column}`)
    as the text/number editors, and the same automatic focus-on-open behavior.
  - Unlike the `'text'`/`'number'` editors, a persistent (`editTrigger: 'always'`) select editor does
    not protect an in-progress, uncommitted selection from an out-of-band `rows` update to that cell
    — `<select>`/`<option>` carry no native dirty-value flag the way `<input>` does.
- 9f0179a: `<lr-table>` can now own its row expansion, and its row-key type is parameterizable.
  
  - **`expansionMode` (`expansion-mode`, reflected, `'none' | 'single' | 'multiple'`, default
    `'none'`)** mirrors `selectionMode` member for member. The default keeps today's behavior
    exactly: `expandedRowKeys` stays consumer-controlled and a chevron activation only reports
    `lr-row-expand-toggle`. Set `'single'` or `'multiple'` and the table maintains the set itself,
    so an expandable table works with no host-side handler at all. `'single'` keeps at most one row
    open, and assigning `expansionMode = 'single'` coerces an already-larger `expandedRowKeys` down
    to its first key the same way `selectionMode` does for `selectedRowKeys`. An unrecognized
    attribute value falls back to the controlled behavior rather than self-managing.
  - **New cancelable `lr-row-expand-request`** (frozen readonly `detail: { row, rowKey, expanded }`)
    precedes each self-managed change. `preventDefault()` skips the built-in `expandedRowKeys` write
    and suppresses the following `lr-row-expand-toggle`, handing that one change back to the host —
    the same request/commit pair `<lr-thread-list>`, `<lr-chat-message>`, and `<lr-code-block>` use.
    It is never emitted under the default `'none'` mode.
  - **`lr-row-expand-toggle`'s detail gains `expanded`**, the state the activation resolves to, so a
    listener no longer has to derive it from `expandedRowKeys`. The existing `row`/`rowKey` fields
    are unchanged. Under a self-managed mode this event follows an accepted request and the write has
    already landed, so reading `expandedRowKeys` from the listener sees the new state.
  - **`'single'` reports the row it displaces.** Closing a row to make room for another is an
    expansion change like any other, and `lr-row-expand-toggle` is the only per-row expansion event
    there is, so `'single'` fires it once with `expanded: false` for the displaced row immediately
    before the accepted one. A host mirroring open rows from that event alone therefore stays correct
    instead of believing the displaced row is still open. The one boundary: a displaced row that is
    filtered or paged out of view has no `row` object for the detail to carry, so that case is
    reported through `expandedRowKeys` alone — as is the `expansionMode = 'single'` coercion above,
    for the same reason.
  - **No mode clears keys when the visible rows change.** Filtering, sorting, and pagination leave
    `expandedRowKeys` alone, so a row filtered or paged out of view comes back expanded and a key
    matching no current row simply renders nothing until one exists again — the convention
    `selectedRowKeys` already follows for server pagination. This is now stated explicitly rather
    than left to be inferred.
  - **`LyraTable<T, K = string | number>`** takes a second type parameter for the row-key type, used
    by `rowKey`'s return type and every event detail's `rowKey`/`rowKeys`, plus `selectedRowKeys` and
    `expandedRowKeys`. `LyraTable<Row, number>` now reads `event.detail.rowKey` as `number` with no
    cast. The parameter defaults to the previous `string | number` union, so every existing
    `LyraTable<Row>` annotation and untyped usage compiles unchanged.
- 8110cf7: Fix `<lr-table>` sticky columns hiding row state, and add cell density hooks.
  
  A `sticky` column's header/body cells hardcoded an opaque `background: var(--lr-color-surface)`,
  while striped, selected, hovered, and pressed row fills were declared only on `[part='row']` — so a
  striped, selected, hovered, or actively-pressed row visually broke at the sticky column, showing a
  flat surface instead of the rest of the row's fill. Each row-state rule now also writes its
  background to a private `--_lr-table-row-bg` custom property, which the sticky cell/header rule
  reads back (`background: var(--_lr-table-row-bg, var(--lr-color-surface))`); custom properties
  inherit from `[part='row']` down into its own `<td>` descendants, so a sticky body cell now shows
  the same fill as the rest of its row. A sticky header cell never sits inside `[part='row']`, so it
  is unaffected and keeps its existing plain-surface default — including a sorted-and-sticky header,
  which still renders exactly as before. `--lr-table-row-selected-bg` and `--lr-table-row-stripe-bg`
  now reach a row's `sticky` cell too; the heat-tint (`--lr-table-heat-t`) and forced-colors rules are
  unchanged.
  
  Also adds two new themeable hooks for cell density: `--lr-table-cell-padding` (default
  `var(--lr-space-s)`), which now backs the header cell, body cell, and row-total cell's padding, and
  `--lr-table-cell-padding-compact` (default `var(--lr-space-xs) var(--lr-space-s)`), the same hook
  for the group-header cell and the footer cell — kept as a second, independent token rather than
  flattened into the first, so their existing tighter block/inline shorthand is preserved rather than
  forced onto every other cell. `--lr-table-font-size` (default `inherit`) now backs the `<table>`
  element's font size, leaving the rest of the font shorthand (family, weight, etc.) inheriting from
  the host as before. All three default to today's exact hardcoded values, so an unstyled table is
  unchanged.
- 91e81ec: `<lr-table>`: three additive changes, all no-ops when unused.
  
  - New public readonly `viewRows`/`pageRows` getters expose the table's own filtered/sorted rows
    (`viewRows`, ignoring pagination) and the currently rendered page of them (`pageRows`), so a
    consumer that needs "what the grid currently shows" (e.g. to export it) can read them instead of
    re-implementing filtering, sorting, and pagination. Both return a fresh, frozen array on every
    read; mutating the result cannot reach the table's own internal state.
  - `columns[].defaultSortDir` lets one column declare its own initial sort direction the first time
    header activation makes it the active `sortKey`, taking precedence over the element-level
    `defaultSortDir` (which remains the fallback when a column omits it) — useful for a mixed
    text/numeric table where, say, a "last updated" column should start descending while the rest
    start ascending. Re-activating a column that is already `sortKey` still only toggles between
    `'asc'` and `'desc'`.
  - Setting `revealColumnsLabel`/`hideColumnsLabel` while no column declares `priority` is always
    inert, since `[part='reveal-columns-button']` never renders without at least one `priority`
    column. This now logs a one-time, development-only, production-silent `console.warn` for it,
    matching the existing missing-accessible-name diagnostic's shape.
- 68451c6: `<lr-data-grid>` gains the same `error` failed-load state `<lr-table>` already has.
  
  - `error` (boolean, reflected), `errorHeading`, and `errorDescription` — while `error` is set, the
    body's single row becomes the built-in failed-load `<lr-empty>` (the same `error`-prefixed
    exported parts and `[part='retry-button']` as `<lr-table>`), keeping the header, toolbar, and
    pager mounted around it. `loading` beats `error` beats every empty/no-columns/no-results branch.
  - An `error` slot replaces the built-in content wholesale.
  - A built-in `[part='retry-button']` emits a cancelable `lr-retry`: the default action clears
    `error`, and `preventDefault()` leaves it set.
  - `error` is host-controlled, like every other property here: the internal `dataSource` request
    cycle's own `lr-data-error` event does not set it, since that event's existing contract keeps
    prior rows rendered on a rejection. A consumer that wants a specific failure to replace the row
    content with the new built-in state sets `error = true` from its own `lr-data-error` listener.
- 68451c6: `<lr-document-library>` gains the same `error` failed-load state `<lr-table>` already has.
  
  - `error` (boolean, reflected), `errorHeading`, and `errorDescription` — forwarded to the nested
    `<lr-table>`, whose own built-in failed-load state (with its retry button) replaces the document
    rows while it's set, keeping the toolbar and selection bar mounted around it. `error` beats the
    nested table's own empty state.
  - An `error` slot replaces the built-in content wholesale.
  - A cancelable `lr-retry` mirrors `<lr-table>`'s own contract: the default action clears `error`,
    and `preventDefault()` leaves it set. This component intercepts the nested table's own `lr-retry`
    and re-proposes its own, so the outer `error` property never drifts out of sync with the nested
    table's internal state.
- 68451c6: `<lr-knowledge-base>` gains the same `error` failed-load state `<lr-table>` already has.
  
  - `error` (boolean, reflected), `errorHeading`, and `errorDescription` — forwarded to the nested
    `<lr-table>`, whose own built-in failed-load state (with its retry button) replaces the source
    rows while it's set, keeping the toolbar and summary mounted around it. `error` beats the nested
    table's own empty state.
  - An `error` slot replaces the built-in content wholesale.
  - A cancelable `lr-retry` mirrors `<lr-table>`'s own contract: the default action clears `error`,
    and `preventDefault()` leaves it set. This component intercepts the nested table's own `lr-retry`
    and re-proposes its own, so the outer `error` property never drifts out of sync with the nested
    table's internal state.
- 68451c6: `<lr-thread-list>` gains the same `error` failed-load state `<lr-table>` already has.
  
  - `error` (boolean, reflected), `errorHeading`, and `errorDescription` — data mode only: while
    `error` is set, the built-in failed-load `<lr-empty>` (the same `error`-prefixed exported parts
    and `[part='retry-button']` as `<lr-table>`) replaces the virtual list/built-in empty state.
    `error` beats the built-in empty state.
  - An `error` slot replaces the built-in content wholesale, alongside the existing `empty` slot.
  - A built-in `[part='retry-button']` emits a cancelable `lr-retry`: the default action clears
    `error`, and `preventDefault()` leaves it set for a consumer that owns its own retry timing.
- 75a4e6d: `<lr-thinking-panel>`'s live auto-follow can now see property-driven, shadow-DOM-rendered content.
  
  `<lr-streaming-text>`, `<lr-markdown>`, and `<lr-markdown-core>` now emit a shared, composed,
  bubbling `lr-content-settled` event (`detail: null`) at their own settle points — each time newly
  coalesced/parsed content actually reaches their rendered DOM. `<lr-thinking-panel>`'s live-mode
  auto-follow now listens for that event in addition to its existing light-DOM `MutationObserver`,
  so composing a `<lr-streaming-text>` or `<lr-markdown>`/`<lr-markdown-core>` in its default slot —
  the documented usage — correctly auto-scrolls even though those components take their content as a
  property and render entirely inside their own shadow root, which the `MutationObserver` alone can
  never see. A mutation and a settle event landing in the same animation frame still coalesce to a
  single scroll; manual scroll-up stickiness suppression and `post-hoc` mode's no-auto-scroll
  contract are both unchanged. `llms/agent-tools.md`'s `<lr-thinking-panel>` docs and
  `llms/conversation.md`'s `<lr-markdown>`/`<lr-markdown-core>`/`<lr-streaming-text>` docs are updated
  to describe the new mechanism and no longer contradict the documented live-mode composition
  example.
- d121ce5: `<lr-thread-list>` gains `itemElement(conversationId)`, the same kind of public row lookup
  `<lr-table>`'s `rowElement`/`cellElement`/`expandedContentElement` already document: the rendered
  `lr-conversation-item` for one thread's `conversationId` (data mode) or one slotted item's own
  `conversation-id` (slotted mode), or `null` when it is not currently rendered — filtered out,
  windowed out of the virtualized viewport, removed, or never present. `<lr-table>`'s three lookup
  methods and the `data-row-key`/`data-col-key`/`data-expanded-row-key` attributes they resolve were
  already shipped as documented stable API; this release adds test coverage confirming they behave
  correctly when a row is paginated away, filtered out, or addressed by a key unsafe to interpolate
  into a CSS selector.
  
  `itemElement()` exists for the same reason `<lr-table>`'s trio does: reaching rendered row content
  to measure it, scroll it into view, or apply a style `::part()` cannot express, without piercing
  this component's shadow root (and, in data mode, the nested internal `lr-virtual-list`'s own shadow
  root) and walking rendered rows by hand.
- 10dace2: `<lr-activity-feed>` entries and light-DOM `<lr-option>` gain an opaque `data` payload
  
  An activity-feed entry and a light-DOM `<lr-option>` can now carry an opaque `data` field — the
  original source record behind a rendered row — reached by reference (never deep-cloned) wherever
  the library already surfaces that row:
  
  - `ActivityEntry.data` survives the owned `entries` snapshot and is handed back to `renderText` on
    every render, so a host needing richer per-entry context (grouping metadata, a nested list of
    related sub-items, the original record) no longer has to re-derive it by re-scanning its own
    source array by id.
  - `<lr-option data>` is the light-DOM counterpart of an async combobox source row's own `data`
    field. `<lr-combobox>`'s `selectedRows` and the new `<lr-select>` `selectedData` getter both
    surface it, and both controls' `lr-input`/`lr-change`/`input`/`change` event details now carry a
    `data: readonly unknown[]` array alongside `value` — index-aligned with it, so `data[i]` is
    always the payload behind `value[i]` (`undefined` in that slot if that particular value
    currently matches no live row/option, rather than shifting every later entry). A consumer keying
    a picker on backend records can pass those records straight into `data` instead of stringifying
    an identifier into `value` and reversing the round trip in every change handler.
- 152ea02: `<lr-popover>`, `<lr-dropdown>`, `<lr-select>`, `<lr-tooltip>` and `<lr-color-picker>` now honor a
  new cascading `--lr-positioning-strategy` custom property (`absolute` | `fixed`) as a theme-level
  alternative to authoring `positioning-strategy`/`hoist` on every instance.
  
  A consumer whose dropdowns or selects live inside an `overflow: hidden` card, a scroller, or any
  other clipping ancestor previously had to set `hoist`/`positioning-strategy="fixed"` on each one;
  forgetting a single instance silently clipped it. Setting `--lr-positioning-strategy: fixed` once
  on that ancestor (or on `:root`, or on a theme) now changes every unset overlay beneath it instead.
  Precedence is unchanged for anyone who sets nothing: an explicit `positioning-strategy`/`hoist` on
  the instance always wins; otherwise the inherited custom property; otherwise the component's own
  mirrored default (`fixed` for `<lr-popover>`, `absolute` for the other four). The property is read
  from computed style only when the popup is actually (re)positioned (open, or a placement/anchor
  change while already open), never per animation frame, and never during server rendering.
  
  Aligning `<lr-dropdown>`/`<lr-select>`'s default strategy with `<lr-popover>` was declined for
  upstream parity (each mirrored component
  keeps its own shipped default), but the requester's stated alternative — a single global/theme-level
  way to set the strategy — is what this change delivers.
  
  Implementation note: fixed a latent ambiguity in each of the four duplicated
  `positioningStrategy` setters (`popover.class.ts`, `select.class.ts`, `tooltip.class.ts`,
  `color-picker.class.ts`) that this feature exposed. Writing an explicit value equal to the
  component's own mirrored default (e.g. `positioning-strategy="absolute"` on `<lr-select>`) never
  recorded that the author had set anything, because the setter's own no-op guard ran before the
  private field was assigned — making that case indistinguishable from "unset" once a cascading
  override needed to check it. The private field is now always recorded; the no-op guard still skips
  the reactive update and the `hoist` alias sync exactly as before, so no other observable behavior
  changes.
  
  Sibling sweep: every other component calling the shared positioner's `place()` was checked.
  `<lr-popup>` has its own `strategy` property but is deliberately excluded — it is the library's
  documented low-level "raw knobs" primitive, and an implicit ambient override would work against
  its whole purpose. `<lr-combobox>`'s listbox has no `positioningStrategy` property at all (it
  always places `fixed`, which already escapes the clipping scenario this change targets) and gaining
  one is a separate, larger feature gap outside this task. The remaining eleven `place()` callers
  (`lr-menu` submenus, `lr-tour`, `lr-mention-popover`, `lr-app-rail-item`, `lr-export-button`,
  `lr-tool-call-chip`, `lr-locale-picker`, `lr-date-input`, `lr-time-input`, `lr-citation-badge`,
  `lr-entity-chip`) expose no `positioning-strategy`/`hoist` property either and were never in this
  property's scope.
- de8d9b6: `<lr-input>` and `<lr-textarea>` gain an opt-in `debounce` (ms) property and a new
  `lr-input-settled` event, so a search-as-you-type field no longer needs its own hand-rolled timer.
  
  - `debounce?: number` — how long to wait after the last keystroke before emitting one
    `lr-input-settled` (`detail: { value }`, non-cancelable), while `input`/`lr-input` keep firing on
    every keystroke exactly as before. Omitted, `0`, or a non-finite value means no debounce at all
    — `lr-input-settled` never fires, and existing behavior is byte-identical.
  - A pending debounce is flushed immediately by `change`, Enter, or blur, so a blur or submit never
    drops the last keystroke.
  - A pending debounce is cancelled with no stray settle by disconnection, `<lr-input>`'s built-in
    clear button, and a programmatic `value` write (including a form reset).
  - Both components share the same `DebounceController` primitive `<lr-filter-bar>`'s own per-filter
    `debounce` already uses, so the three surfaces (`<lr-input>`/`<lr-textarea>`, `<lr-filter-bar>`,
    and `<lr-combobox>`'s `sourceDelay`) now document one consistent contract.
- 75a4e6d: `<lr-filter-bar>` is now generic on its `filters` schema, so TypeScript narrows `value` per `filterId`
  
  `LyraFilterBar<Defs extends readonly LyraFilterBarFilterDefinition[]>` narrows `value` (and the
  `lr-input`/`lr-reset` detail `value`) to a keyed record whose per-`filterId` field type follows
  that filter's own definition: a `'select'`, a non-`multiple` `'combobox'`, `'text'`, `'date'`, and
  `'date-range'` type as `string`; a `'checkbox-menu'` and a `multiple: true` `'combobox'` type as
  `readonly string[]`; a `'custom'` filter keeps the full unconstrained field value, since its
  adapter may use either boolean meaning. Declare the schema with `as const satisfies readonly
  LyraFilterBarFilterDefinition[]` and type the element as `LyraFilterBar<typeof FILTERS>` to pick
  it up. The narrowed shape is also exported standalone as `LyraFilterBarValueFor<Defs>`, and the
  value-carrying events as the stable per-event aliases `LyraFilterBarInputEvent<Defs>`/
  `LyraFilterBarResetEvent<Defs>`, so a handler can name one event's type without restating the
  detail shape.
  
  This is types only: the runtime is entirely unchanged. The type parameter defaults to `readonly
  LyraFilterBarFilterDefinition[]`, so an untyped `<lr-filter-bar>` — including everything reached
  through `document.querySelector('lr-filter-bar')` — keeps exactly today's `LyraFilterBarValue`
  record and compiles unchanged.
  
  Migration tip (not required): a project that already carried its own per-`filterId` casts when
  reading `value` off a filter bar with a fixed schema can delete them and let the generic narrow
  instead; its own linter may now flag the old cast as unnecessary.
- de8d9b6: `<lr-filter-bar>`'s `'custom'` filter type now accepts the same optional `debounce` its `'text'`
  and `'combobox'` types already had.
  
  - `LyraFilterBarCustomDefinition.debounce?: number` delays committing whatever the adapter's
    `valueFromEvent` reads off `context.onValueChange`/`onInput`/`onChange`, coalescing a burst of
    rapid commits (keystrokes, toggles, anything the custom control fires) into one delayed `value`
    write and a single `lr-input`. Omitted, `0`, or a non-finite value means no debounce at all —
    every commit lands immediately, exactly as before this field existed.
  - While a commit is pending, `context.value` carries that pending value instead of the
    last-committed one, so a custom control bound to it as a fully controlled value never reverts
    mid-delay.
  - A pending commit is flushed by `context.onFocusout` and cancelled outright by `reset()`,
    removing that filter's active-filter chip, and disconnection — identical to `'text'`'s and
    `'combobox'`'s own debounce.
  
  This closes the gap those two types' debounce left: previously a custom free-text filter had to
  hand-roll the same timer, flush, and cancellation lifecycle itself to get equivalent behaviour.
  Fully optional and additive — an existing `'custom'` filter definition keeps committing
  immediately, unchanged.
- de8d9b6: Every built-in `<lr-document-viewer>` kind now ships a lazy, register-only entry
  (`archive-viewer-register.js`, `ebook-viewer-register.js`, and five new ones:
  `pdf-viewer-register.js`, `docx-viewer-register.js`, `pptx-viewer-register.js`,
  `spreadsheet-viewer-register.js`, `csv-viewer-register.js`, `xml-viewer-register.js`) that installs
  that kind's file-matching and capability declaration without pulling its viewer element's class
  module into the importing graph until a matching file is actually opened. Each entry also exports a
  `<KIND>_VIEWER_TAG` string constant naming the tag it eventually registers, so a consumer can
  reference or query for it without a deep import. A new `document-viewer-kinds.js` entry imports and
  re-exports all eight at once. Every existing `<kind>-viewer.js` entry point is unchanged and keeps
  working exactly as before for a consumer who wants the tag available immediately.
- de8d9b6: `<lr-code-block-core>`/`<lr-markdown-core>`'s `languages` map can now take a lazy loader per key
  (`() => import('@shikijs/langs/bash')` or any `() => Promise<ShikiLanguageInput | { default:
  ShikiLanguageInput }>`) instead of requiring every grammar to already be imported and resolved
  before the map can be bound. A loader is called at most once per key, memoized, the first time a
  fenced block actually requests that language; a fenced block whose language is absent from
  `languages`, or whose loader fails, still renders the existing plain-text fallback. Binding a fixed
  set of loaders instead of eagerly imported grammars lets a bounded-language-set consumer ship one
  small chunk per grammar, fetched only for fences that actually occur, instead of downloading the
  whole set upfront.
- de8d9b6: Added `<lr-streaming-text-core>`, a build-lean `<lr-streaming-text>` variant that composes
  `<lr-markdown-core>` instead of `<lr-markdown>` in Markdown mode, so a consumer whose fenced-code
  language set is bounded no longer needs to pull `<lr-markdown>`'s ~200-language dynamic-import
  table into the graph just to use the streaming renderer. `<lr-streaming-text>` also gains a new
  `languages` property (`Readonly<Record<string, ShikiLanguageInput>>`, unset by default), forwarded
  verbatim to whichever Markdown element it composes — the same fine-grained language-grammar
  scoping `<lr-code-block>`/`<lr-markdown>` already support.
- de8d9b6: The `toast()` helper (`toaster.js`) no longer statically imports `<lr-toast>`/`<lr-toast-item>`'s
  own class implementations. Merely importing the helper — including through the package root — used
  to pull the whole toast subsystem into the eagerly loaded graph even for a consumer that only ever
  shows an occasional single message. The class modules are now dynamically imported and registered
  on the first actual `toast()` call; the returned `ToastHandle.item` promise (and `dismiss()`, which
  already chains off it) absorb the extra tick transparently. Behavior once a toast is shown is
  unchanged.
- 75a4e6d: Publish a generated `registrations.json` artifact describing which `<lr-*>` tags each stable
  per-tag entry registers as a side effect. Importing `@aceshooting/lyra-ui/components/lr-table.js`
  also registers `<lr-empty>`, `<lr-pagination>`, `<lr-skeleton>`, and `<lr-spinner>`, because
  `lr-table`'s registration entry imports those composed children's own registration entries before
  defining `<lr-table>` itself; nothing published previously described that, so verifying it
  statically meant parsing the shipped minified JavaScript for import specifiers and
  `defineElement(...)` call literals. `registrations.json` reads `{ schemaVersion: 1, entries: [{
  tag, entry, registrationModule, registers }] }` for every published component, derived by
  `scripts/generate-registration-graph.mjs` from the same transitive-import analysis
  `scripts/check-component-dependencies.mjs` already performs, rather than a second hand-maintained
  list.
- de8d9b6: `LYRA_SSR_SUPPORT_MATRIX.declarativeShadowDom` (from `@aceshooting/lyra-ui/ssr-loader.js`) gains a
  `staticSafety` record, and `getLyraSsrStaticSafety(tagName)` reads one tag's entry directly. Every
  `render-and-hydrate` tag now classifies as `'static-safe'` (its declarative-shadow-DOM output stays
  visually and functionally complete indefinitely, even if hydration JavaScript never runs at all —
  no script bundle, JS blocked, or a crawler reading raw HTML) or `'hydration-required'` (correctness
  is only ever promised once hydration executes). Previously the docs described only the moment
  around hydration itself (server guess vs. corrected client state) and were silent on the
  zero-JS-forever case, so a static-generation consumer had no documented way to tell which of the
  264 `render-and-hydrate` components were safe to rely on without a script bundle; `LYRA_SSR_SUPPORT_MATRIX.capabilities`
  existed but was a single global record with no per-tag link to it.
  
  The classification is derived from a new evidence-backed per-tag `LYRA_SSR_TAG_CAPABILITIES`
  record (which of `canvas`/`layoutMeasurement`/`mediaPlayback`/`observers`/`remoteContent` a tag's
  own correctness depends on) plus an explicit `LYRA_SSR_AUDITED_STATIC_SAFE_TAGS` list for every
  reviewed tag with none of those. `pnpm test:ssr` now fails closed on any `render-and-hydrate` tag
  present in neither — an unreviewed tag can never pass by silently defaulting to `'static-safe'`.
  `llms/shared.md`'s "SSR and declarative shadow DOM" section documents the guarantee together with a
  worked static-safe example (`lr-details`, whose native `<details>` toggle keeps working with zero
  JavaScript because the open state is server-reflected and the veto listener is never registered
  without hydration) and a worked hydration-required example (a canvas-painted chart, empty until
  script paints it).
- 75a4e6d: Each shipped translation catalog (`translations/<locale>.js`) was one side-effect module
  registering every localizable string for every component in a single ~1,300-entry object. Because
  the keys lived in one object literal reached only through a side effect, no bundler could drop the
  ones an application never rendered — a French, Arabic, German, Spanish, Persian, Hebrew, Japanese,
  Brazilian Portuguese, Russian or Simplified Chinese app paid roughly 14–19 KB gzip per locale for
  components it never imported, the one axis where a non-English user paid for the whole catalog
  while English defaults were already tree-shaken per component.
  
  Every locale now also ships as twelve smaller **family slices** —
  `translations/<locale>/<family>.js` for each of the 11 component families (`agent-tools`, `charts`,
  `conversation`, `data`, `forms`, `layout`, `media`, `overlays`, `retrieval`, `utility`, `viewers`),
  plus a `shared` slice for the messages more than one family reaches (roving-focus/overlay/a11y
  strings like `collapse`, `open`, `search`) — each a side-effect module registering only its own
  keys. Family membership reuses the exact per-component key-reachability data the English
  default-string slices are generated from, so it can never drift from what actually ships in a
  component's own bundle.
  
  `translations/<locale>.js` is unchanged as a public entry point: it is now a thin, generated
  re-export of every family slice for that locale, preserving the existing "one import gets
  everything" behaviour and merge semantics (a later `registerLyraLocale()` call, or a per-instance
  `.strings` override, still wins). Importing only the family slices an application actually renders
  is a new, additive, opt-in way to shrink a non-English bundle:
  
  ```ts
  import '@aceshooting/lyra-ui/translations/fr/forms.js';
  import '@aceshooting/lyra-ui/translations/fr/data.js';
  import '@aceshooting/lyra-ui/translations/fr/shared.js';
  ```
  
  `scripts/check-translations.mjs`'s coverage/order/placeholder/plural-category rules now apply
  per-slice as well as to the (still fully covered) aggregate; `scripts/scaffold-translation.mjs`
  scaffolds a brand-new locale directly in the sliced shape.
- d121ce5: `@aceshooting/lyra-ui/testing` gains `createLyraEvent(tag, name, detail?)`, a typed factory for
  building one specific `lr-*` component's documented `CustomEvent` — the same `bubbles: true`,
  `composed: true`, and per-event `cancelable` flags the real component's own event map declares —
  without hand-rolling one and guessing its shape. An unknown tag, an event name that tag does not
  document, or a `detail` of the wrong shape are compile errors. `detail` is always optional (an
  omitted value normalizes to `null`, matching `LyraElement.emit()`), so a test that only cares about
  the dispatched event's flags does not need a realistic one.
- 152ea02: `@aceshooting/lyra-ui/testing` gains four interaction drivers — `chooseOption()`,
  `submitConfirmDecision()`, `toggleSwitch()`, and `activateStep()` — that go through a component's
  own real activation path (its own shadow-part lookup and `.click()`, the same as its own tests)
  instead of a downstream suite reverse-engineering internal detail shapes or shadow-part selectors.
  `chooseOption()` covers every component sharing the same `[part="option"]`/`data-value` listbox
  pattern (`lr-combobox`, `lr-select`, `lr-model-select`, `lr-locale-picker`, `lr-voice-picker`), and
  `submitConfirmDecision()` covers both components sharing the same
  `[part="approve-button"]`/`[part="deny-button"]` `lr-approve`/`lr-deny` contract (`lr-confirm-bar`,
  `lr-tool-approval-dialog`). Each driver is `async`, resolves after `updateComplete`, and throws a
  plain `Error` rather than silently doing
  nothing when the requested interaction cannot happen (disabled, read-only, or no matching
  row/step currently rendered) — the same class of silent miss `createLyraEvent()` already closes
  for hand-built events. Pure DOM operations only, so these also run under a downstream suite's own
  happy-dom/jsdom environment, not only a real browser.
- 152ea02: `<lr-chat-composer>` gains an `actions-layout` attribute, a `toolbar` slot, and three new themeable
  custom properties.
  
  `actions-layout="stacked"` (default `"inline"`, today's single flex row) arranges the `start` and
  `end` action slots as a compact one-column rail — `start` above `end` — beside a `textarea` that
  spans both rows, via a CSS grid on `[part="row"]`. It's for a multi-row composer (a taller
  `min-rows`) where stretching the action buttons across the row's full cross-axis height looks
  wrong.
  
  The new `toolbar` slot renders inside `[part="base"]`, above the `chips` tray and input row, so
  auxiliary controls — a model or provider picker, for example — can sit inside the composer's own
  frame and share its `:focus-within` affordance, the same way `chips`/`start`/`end` already do.
  Hidden via `[part="toolbar"][hidden]` when empty, matching those existing slots.
  
  `--lr-chat-composer-padding` (default `var(--lr-space-s)`) and `--lr-chat-composer-gap` (default
  `var(--lr-space-xs)`) retune `[part="base"]`'s padding and the row gap between its stacked
  `toolbar`/`chips`/`row` sections, joining the existing `-background`/`-border-color`/`-radius`
  chrome hooks; `frame="plain"` still zeroes the padding as before.
  
  `--lr-chat-composer-focus-shadow` (default `inset 0 calc(-1 * var(--lr-focus-ring-width)) 0 0
  var(--lr-focus-ring-color)`) is the `frame="plain"` focus underline painted on
  `[part="base"]:focus-within`. Override it to reshape the underline (a different width or color),
  or set it to `none` to cede focus chrome entirely to a wrapper you draw and focus-highlight
  yourself. A custom property was chosen over a third `frame` value: every other piece of this
  card's paint is already a cssprop hook rather than a `frame` variant, and `none` already reads as
  "I'll draw my own" without inventing a new literal.
- a653796: `<lr-reorder-list>` gains an opt-in `controlled` mode, and `<lr-reorder-item>` gains a public
  `focusMoveButton()` method.
  
  - `controlled` (reflected, default `false`): an accepted move no longer moves this list's own
    slotted `<lr-reorder-item>` nodes itself. Instead it waits for the host to reorder its own
    backing data and re-render the slotted items to match — the same controlled request `<lr-tree>`'s
    `reorderable` already establishes for its `data`-driven children — and reconciles by each item's
    `value` rather than by element reference, so a host re-render that recreates the moved row (or
    merely rewrites `value` on the elements already at each position, the common outcome of a
    non-keyed `Array.map()`) still completes the move once the resulting order matches. The list
    stays `aria-busy`/`:state(busy)` for the whole wait, matching a `preventDefault()`-held move;
    a re-render that never reaches the emitted order leaves the move pending indefinitely, and one
    that drops the moved `value` entirely cancels it silently, with no announcement.
  - `<lr-reorder-item>.focusMoveButton(direction: 'up' | 'down'): boolean` moves focus onto that
    row's move-up/move-down control (returning whether it did), the same way the owning list already
    restores focus after a move — now also available to a host driving `controlled` mode directly,
    including onto a freshly recreated element instance.
  - `<lr-reorder-list>.revertPendingMove()` accepts an options object,
    `revertPendingMove(options?: { silent?: boolean })`, to suppress the built-in
    `reorderMoveCancelled` announcement when a host is deferring the decision to a flow of its own
    (a confirmation dialog, say) that will communicate the outcome itself.
- de8d9b6: Adds `formatNumber()`, `formatDate()`, `formatRelativeTime()` and `formatBytes()` to
  `@aceshooting/lyra-ui/utilities/format.js` (and the `utilities` barrel): pure, string-returning
  locale formatters over the same memoized `Intl` formatter cache and locale resolution
  `<lr-format-number>`, `<lr-format-date>`, `<lr-relative-time>` and `<lr-format-bytes>` already
  render through.
  
  Previously, locale-aware number, date, byte and relative-time formatting was reachable only as
  those four custom elements — anything needing a formatted *string* instead (interpolating into a
  message template, populating a text-only property on another component, building a search
  predicate, composing an accessibility announcement) had to hand-roll and cache its own `Intl`
  instances. `formatNumber()` and `formatBytes()` also accept a `bigint` or a decimal/integer string
  for exact-precision input (large ids, monetary amounts, exact byte counts beyond
  `Number.MAX_SAFE_INTEGER`) without first collapsing it through a `number`.
  
  ```ts
  import { formatBytes, formatRelativeTime } from "@aceshooting/lyra-ui/utilities/format.js";
  
  const size = formatBytes(12_345_678_901_234_567_890n, "en-US"); // exact, no float rounding
  const updated = formatRelativeTime(item.updatedAt, "en-US"); // "3 days ago"
  ```
  
  The four `lr-format-*`/`lr-relative-time` elements now render through these same helpers instead
  of duplicating the `Intl` construction and locale-fallback logic inline — a pure internal refactor
  with no behavior change.
- 10dace2: Adds `@aceshooting/lyra-ui/theme-bootstrap.js`, a static, non-module script asset carrying the same
  bytes as `lyraThemeBootstrap` (`@aceshooting/lyra-ui/theme.js`) so a Content-Security-Policy that
  forbids `unsafe-inline` — and cannot mint a per-response nonce, such as a static HTML entry — can
  reference the no-flash theme bootstrap with a plain `<script src>` instead of hand-rolling a build
  step that writes it to a fixed-name file. The two are generated from the same build step and are
  guaranteed byte-identical, so hashing (or same-origin-serving) one covers the other. It only ever
  carries the default storage key (`'lyra-theme'`); an application-owned key still needs
  `createLyraThemeBootstrap({ storageKey })` inlined, since a static file can't take a call-time
  argument.
- de8d9b6: New `<lr-drop-zone>` component, plus `<lr-file-input>` aggregate limits and a non-retaining picker
  mode.
  
  - `<lr-drop-zone>`: a drag-and-drop region wrapper with no file input of its own. Wrap it around an
    arbitrary region — a chat composer, a whole conversation viewport, a panel far larger than any
    single control — to make that entire region a file-drop target: it owns the drag-session state,
    renders a themeable drag-over overlay, applies `accept`/size/count limits, and emits the same
    `lr-files` event shape `<lr-file-input>` does. Compose `<lr-file-input>` (or any other focusable
    content) inside it when the region also needs a click-to-browse affordance.
  - `<lr-file-input>` gains `maxFiles`/`max-files` and `maxTotalSize`/`max-total-size`, mirroring
    `maxFileSize`'s existing rejection-UI shape and invalid-override fail-safe fallback (new reasons
    `'maxFiles'`/`'maxTotalSize'` on `LyraFileInputRejectedFile`).
  - `<lr-file-input>` gains an opt-in `nonRetaining`/`non-retaining` mode: an accepted selection still
    fires `lr-files`/`input`/`change`, but is never written to `files` or rendered as a built-in row —
    for a host that persists files elsewhere and renders its own list. A new `valuePresent`/
    `value-present` property lets that host signal required validity externally.
  - `<lr-file-input>`'s `lr-files` event is now typed as `LyraFileInputFilesEvent`, so
    `event.target`/`event.currentTarget` read as `LyraFileInput` without a cast.
  - The drag-session mechanics (nested-depth tracking, accept/reject preview, folder traversal)
    `<lr-file-input>` already had are now shared internal implementation, reused verbatim by
    `<lr-drop-zone>` rather than reimplemented.
- 68451c6: Added `@aceshooting/lyra-ui/reservations.styles.js`, exporting `reservationStyles: CSSResult` —
  the same layout-shift reservations `reservations.css` declares, generated from that same source so
  the two can never drift, but adoptable directly into a shadow root
  (`static styles = [reservationStyles, css\`...\`]`, or
  `shadowRoot.adoptedStyleSheets = [reservationStyles.styleSheet!]`). `reservations.css` is a
  light-DOM stylesheet: `@import`-ing it at document scope cannot reach an `lr-*` element that a
  consumer's own component renders inside its own shadow root, because a document stylesheet never
  crosses a shadow boundary. This export is the fix for exactly that case, matching `theme.css`'s
  existing `theme.js` runtime counterpart.
- 152ea02: `<lr-menu-item>` (and `<lr-dropdown-item>`, which inherits it) gains `type="radio"`, rendering
  `role="menuitemradio"` with exclusive-choice group semantics, alongside the existing
  `type="checkbox"`.
  
  Activating an unchecked radio row fires the same cancelable `lr-menu-item-change` proposal
  `type="checkbox"` already uses (`detail: { value, checked: true }`); once not prevented, the row
  becomes `checked` and every other `type="radio"` row the *same owning `<lr-menu>`* owns directly is
  unchecked, enforcing a single current choice. Activating an already-checked radio is a no-op on
  `checked` — no proposal, no state change — matching native `<input type="radio">` semantics, though
  selection still proceeds through the owning menu exactly like re-activating any other item. A new
  `group` attribute narrows that exclusive scope to only the radio rows sharing the same string, so
  one menu can host several independent single-choice sections; left unset, the scope defaults to
  every radio row the menu owns directly (a nested submenu's radio rows already belong to that
  submenu's own owning menu, so they were never in the outer scope regardless of `group`). The
  checkmark glyph and `[part="checkmark"]`/`[part="checked-icon"]` are reused as-is from
  `type="checkbox"`.
  
  Previously `<lr-menu>` had no way to model an exclusive
  choice (Sort by…, Theme, Currency, Units) without hand-building it from `type="checkbox"` rows plus
  a `preventDefault()` veto on every `checked: false` proposal, re-driving `checked` from application
  state — and assistive technology announcing the result as N independent checkboxes rather than one
  single-choice group. An out-of-vocabulary `type="radio"` previously degraded silently to a plain
  `role="menuitem"` with no `aria-checked` and no checkmark; it is now a fully supported value.
  
  Sibling sweep: no other component declares a `'normal' | 'checkbox'`-shaped type union or branches
  on `.type === 'checkbox'` outside this one, and `<lr-menu-item>` is the only implementer of the
  internal item-to-menu ownership contract this feature extends. The shared `menuitemradio`-aware
  internals (`internal/focus-navigation.ts`'s navigable-role list and
  `internal/form-control-labels.ts`'s label-click activation) already included `menuitemradio`
  ahead of this change, so no further wiring was needed there. `<lr-data-grid>`'s and
  `<lr-filter-bar>`'s own `role="menuitemcheckbox"` rows (column visibility, `'checkbox-menu'`
  filters) are genuine multi-select use cases, not exclusive choice, and are unaffected.
- 68451c6: `<lr-pagination>` gains an indeterminate mode for a server API that never returns a total item
  count — limit/offset and cursor/keyset APIs typically don't. Set `total="-1"` and use the new
  `hasNext` property to report whether one more page exists; the component then renders previous and
  next only, with a page-number field and no numbered page list, item-range summary, or `/
  totalPages` readout, regardless of `format`, `withSummary`, or `withEdges`. Previous is disabled at
  page 1 exactly as in the known-total path; events, focus management, and the applied-page
  announcement all use the same contract, minus the total-pages figure in the announcement text. Any
  other negative `total` still renders the ordinary empty state.
  
  `<lr-table>`'s server pagination mode forwards this through two new properties, `unknownTotal` and
  `hasNext`, so a table backed by a total-less server API gets the same built-in loading state,
  localized labels, focus handling, and `lr-page-change` event it already has for a known total —
  no more hand-rolled previous/next buttons around a bare page readout.
- 152ea02: `<lr-responsive-panel>`'s `shape` property now accepts `'start'`/`'end'` alongside the existing
  `'fullscreen'`/`'bottom-sheet'` values. These anchor the overlay presentation to the matching
  *logical* inline edge instead of covering or spanning the whole viewport — a persistent inline
  navigation panel on a wide screen and a slide-in-from-the-edge overlay on a narrow one, matching
  the visual identity of a docked sidebar rather than a full-screen or bottom-sheet modal. The
  anchored edge, and the panel's rounded free edge, both flip automatically under `dir="rtl"`
  through logical `inset-inline-*`/border-radius CSS properties — no `:dir()` selector is involved.
  
  The change is CSS-only: `mode="auto"`'s shared shadow DOM, single render path, and automatic
  focus capture/restore on presentation changes are exactly as before for every shape, including the
  two new ones. A new `--lr-responsive-panel-side-inline-size` custom property (default
  `var(--lr-size-20rem)`) themes the side panel's width.
- a653796: Add a `maxHeight` property (attribute `max-height`) and `--lr-markdown-max-height` token (default
  `none`) to `lr-markdown`/`lr-markdown-core`, letting either scroll internally past a caller-set
  height instead of growing the page — the same `maxHeight`/token shape already shipped on
  `lr-json-viewer`, `lr-diff-view`, `lr-code-block`, `lr-stack-trace`, and `lr-document-compare`.
  Unset, `[part="content"]` renders byte-identical to before.
  
  Swept every other content-that-can-overflow component for the same gap. `lr-terminal`'s
  `[part="viewport"]` is deliberately excluded: unlike the components above, it is always a
  fixed-height virtualized scrollback region (`--lr-terminal-height`, default `20rem`, already the
  cap), not a "grows until capped" surface, so the same property/token shape would fight rather than
  complement its existing model. That decision is now recorded in `lr-terminal`'s own class doc.
- 68451c6: Sweep for `<lr-thread-list>`'s own now-fixed defect (a suppressed native search-cancel glyph with
  no replacement affordance) across the rest of the library.
  
  - `<lr-command-palette>`: the built-in search field's native `::-webkit-search-cancel-button` was
    suppressed with nothing replacing it. A `part="clear-button"` icon button now renders next to
    `[part="input"]` once it has a value, with a localized `this.localize('clear')` accessible name.
    Clicking it clears the field, re-runs the same active-row bookkeeping typing already triggers,
    and returns focus to the input.
  - `<lr-data-grid>`: both the toolbar's global `[part="search"]` field and the per-column
    `[part="filter-panel"]` search field suppressed their native cancel glyph the same way. Each now
    gets a matching clear button (`part="search-clear"` and `part="filter-panel-clear"`), rendered
    only while the respective field has a value; clicking one clears just that field, fires the
    existing `lr-filter-change` for the column filter, and returns focus to the field. `[part="search"]`
    is now wrapped in a new `part="search-wrapper"` row so the input can shrink to make room for its
    clear button; the input's own visible styling (border, background, radius) is unchanged.
  - `<lr-table>`: the `[part="filter"]` row-filter field had the identical gap. A new
    `part="filter-clear"` button, rendered only while `filterText` is non-empty, clears the field,
    fires the existing `lr-filter-change`, and returns focus to the field.
  - `<lr-emoji-picker>`: the `[part="search"]` field had the identical gap. A new `part="search-clear"`
    button, rendered only while the query is non-empty, clears the field and returns focus to it.
    `[part="search"]` is now wrapped in a new `part="search-wrapper"` row for the same reason as
    `<lr-data-grid>` above.
  
  All four reuse the shared `this.localize('clear')` string and the shared `--lr-icon-button-size`
  hit-area floor, matching `<lr-thread-list>`'s and `<lr-input>`'s own clear-button vocabulary.
  
  Every other native-search-input-with-a-suppressed-clear-affordance already found by
  `grep -rl "search-cancel-button" src/components` (`<lr-eval-dataset>`, `<lr-tool-select-dialog>`,
  `<lr-node-palette>`, `<lr-input>`, `<lr-thread-list>`) already renders its own replacement clear
  control; no further action needed there.
  
  No "fully controlled property whose built-in interaction visibly does nothing without a host
  listener" defect matching `<lr-thread-list>`'s former `collapsedGroupIds` gap was found elsewhere.
  `<lr-dashboard-grid>`'s `layout`, `<lr-branch-picker>`'s `index`, `<lr-conversation-item>`'s
  `label`, `<lr-stepper>`'s per-step `current` state, `<lr-graph>`'s `selectedNodeIds`/
  `selectedLinkIds`, and `<lr-eval-result>`'s `selectedRunId`/`baselineRunId` are all controlled the
  same way, but each is a deliberate design choice with a stated precedent (mirroring
  `<lr-pagination>`'s server-friendly `page`, `<lr-table>`'s/`<lr-flow-canvas>`'s controlled layout
  contract, or `<lr-chat-message>`'s persistence-gated retry convention) rather than an oversight:
  in every case, self-applying the change without the host-supplied content it depends on (different
  branch content, a collision-resolved layout, a persisted rename, an unambiguous next step state, or
  a loaded comparison run) would render an incoherent intermediate state, unlike
  `collapsedGroupIds`, whose self-management needed no external data at all.
- a653796: Sweep of hardcoded `--lr-color-brand-quiet`/`--lr-color-brand` inline-surface colors with no
  override hook (the same shape just fixed for `<lr-markdown>`'s code surfaces). Every
  `src/components/**/*.styles.ts` hit for both literal patterns (`--lr-color-brand-quiet`: 392
  lines; `--lr-color-brand)`: 482 lines; 862 lines once the two are de-duplicated, across 179 files —
  unchanged after this task's own fixes, since each new hook's inline `var()` fallback still contains
  the exact literal default it preserves rather than adding or removing an occurrence) was triaged
  into "already hooked at the point of use" (a nearby or JS-resolved public
  `--lr-<component>-*` custom property already wraps it — the majority, once multi-line `var()`
  nesting and JS-driven private-var indirection are accounted for), "shared idiom, excluded" (the
  same deliberate, library-wide pattern used identically by hundreds of controls — interactive
  `:hover`/`:active` fills; `aria-pressed`/`aria-selected`/`data-active`/`data-current`-style
  selection chrome; an opt-in `variant="brand"`/`appearance="accent"` presentation; a link- or
  toggle-styled text color; a filled primary/CTA button's `background`/`color` pairing; a native
  range/checkbox `accent-color`; a `:checked`/`aria-checked` indicator), or "new public hook,"
  below. Additive new custom properties on 11 components:
  
  - **New:** `<lr-av-player>`'s `--lr-av-player-cue-hover-bg` (default `var(--lr-color-brand-quiet)`)
    retints a hovered transcript cue row; its pressed state now mixes from this same hook instead of
    the bare shared token, matching `--lr-av-player-cue-current-bg`'s and
    `--lr-av-player-marker-bg`'s existing indirection in the same component.
  - **New:** `<lr-file-icon>`'s `--lr-file-icon-bg` (default `var(--lr-color-brand-quiet)`) and
    `--lr-file-icon-color` (default `var(--lr-color-brand)`) retint the format badge — every file
    category previously rendered the same fill with no escape hatch, unlike its `media` family
    siblings `<lr-avatar>`/`<lr-avatar-group>`, which already expose an equivalent hook for their own
    identity-badge fill.
  - **New:** `<lr-pdf-viewer>`'s `--lr-pdf-viewer-toolbar-bg` (default `var(--lr-color-brand-quiet)`)
    and `--lr-pdf-viewer-text-selection-bg` (default `var(--lr-color-brand-quiet)`) retint the
    toolbar background and the native text-selection tint over extracted page text, closing the same
    gap already closed for that component's own `--lr-pdf-viewer-toolbar-button-hover-bg` and
    `--lr-pdf-viewer-search-match-bg` hooks.
  - **New:** `<lr-image-viewer>`'s `--lr-image-viewer-annotation-box-border` and
    `--lr-image-viewer-annotation-box-bg` (defaults `var(--lr-color-brand)` /
    `color-mix(in srgb, var(--lr-color-brand) 15%, transparent)`) retint the in-progress draft
    rectangle drawn while annotating, independent of the saved highlight boxes' own tone colors and
    the annotate-toggle's own active-state hooks.
  - **New:** `<lr-flow-canvas>`'s `--lr-flow-canvas-connection-line-color` (default
    `var(--lr-color-brand)`) retints the in-progress connect-gesture ghost path, independent of a
    finished edge's own `--lr-flow-canvas-edge-*-color` tone hooks in the same file.
  - **New:** `<lr-flow-minimap>`'s `--lr-flow-minimap-viewport-color` (default
    `var(--lr-color-brand)`) retints the viewport rectangle's fill and stroke, independent of the
    per-status node fills (`--lr-flow-status-*-color`) already hooked in the same file.
  - **New:** `<lr-code-block>` (and `<lr-code-block-core>`, which reuses its stylesheet directly)
    gain `--lr-code-block-language-bg` (default `var(--lr-color-brand-quiet)`) and
    `--lr-code-block-language-color` (default `var(--lr-color-brand)`) for the header `language`
    badge, independent of the already-hooked `--lr-code-block-active-line-outline-color`.
  - **New:** `<lr-docx-viewer>`'s `--lr-docx-viewer-table-header-background` (default
    `var(--lr-color-brand-quiet)`) retints a rendered document table's header row, independent of
    the highlight/search-match backgrounds already hooked in the same file.
  - **New:** `<lr-dataset-viewer>`'s `--lr-dataset-viewer-header-row-bg` (default
    `var(--lr-color-brand-quiet)`) retints the sticky header row, independent of the already-hooked
    `--lr-dataset-viewer-highlight-color`.
  - **New:** `<lr-xml-viewer>`'s `--lr-xml-viewer-tag-color` (default `var(--lr-color-brand)`)
    retints every rendered element tag name, independent of the already-hooked
    `--lr-xml-viewer-active-attribute-color`.
  - **New:** `<lr-select>`'s `--lr-select-option-badge-bg` (default `var(--lr-color-brand-quiet)`)
    retints the `[part='option-badge']` "not in catalog" badge `show-unknown-option` renders,
    independent of the already-hooked `--lr-select-unknown-value-border-color`.
  
  All fifteen are inline `var()` fallbacks at the point of use, so every default rendering stays
  byte-identical when unset.
  
  **Deliberately not fixed this round, with a reason:**
  - `<lr-markdown>`'s `[part='table'] th` background and `<lr-combobox>`'s `[part='option-badge']`
    background are the identical shape (an unhooked content surface inconsistent with an
    already-hooked sibling in the same file — `--lr-markdown-code-bg`/`-highlight-*-bg` and
    `--lr-combobox-option-selected-*` respectively) and were found by this same sweep, but both
    files carried another in-flight task's uncommitted changes this round
    (`markdown.styles.ts`/`markdown.class.ts` and `combobox.class.ts`); adding a hook here risked
    colliding with that work mid-flight rather than after it lands. Landed as the tracked follow-up,
    not left as an informal "someday" note: `--lr-markdown-table-header-bg` and
    `--lr-combobox-option-badge-bg`, same shape as the fixes above — see the sibling changeset.
  - `<lr-mind-map>`'s resting node-circle fill, `<lr-agent-run>`'s current-step spinner icon color,
    `<lr-tool-approval-dialog>`'s tool-name label color, and the standalone loading-ring
    `border-block-start-color` in `<lr-tree>`'s tree-item spinner, `<lr-document-preview>`, and
    `<lr-document-viewer>` are each the only brand-colored surface of their kind in their own file —
    none has an already-hooked sibling of the same visual role to be inconsistent with, unlike every
    fix above. Not given a new hook this round; a maintainer ask for any of these specifically is a
    one-line follow-up, not a rediscovery.
  - The retrieval-family sweep from the previous round (`<lr-retrieval-results>`'s
    `[part='load-more']`, `<lr-provenance-panel>`'s and `<lr-community-card>`'s header
    disclosure/member buttons) is re-confirmed excluded as the shared "quiet interactive fill" idiom
    — matching e.g. `<lr-commit-card>`'s `[part='files-toggle']` (resting `--lr-color-brand` text,
    `:hover` background `--lr-color-brand-quiet`), not `<lr-icon-button>`, which uses no brand token
    at all for its own hover/active fill.
- 152ea02: Completes the brand-quiet inline-surface hooks for the two remaining components. Same
  shape as that sweep's other fifteen hooks — an inline `var()` fallback at the point of use, so
  default rendering stays byte-identical when unset.
  
  - **New:** `<lr-markdown>` (and `<lr-markdown-core>`, which shares its stylesheet)'s
    `--lr-markdown-table-header-bg` (default `var(--lr-color-brand-quiet)`) retints every rendered
    `[part='table']` header cell, independent of the already-hooked `--lr-markdown-code-bg`/
    `-highlight-*-bg` tokens in the same file.
  - **New:** `<lr-combobox>`'s `--lr-combobox-option-badge-bg` (default `var(--lr-color-brand-quiet)`)
    retints the `[part='option-badge']` trailing metadata badge on an async row, and the "not in
    catalog" badge `show-unknown-option` renders on a synthetic unmatched-value row, matching
    `<lr-select>`'s already-shipped `--lr-select-option-badge-bg`.
- a653796: `<lr-progress-ring>` gains an opt-in `size` on the library's one six-step size ladder
  (`2xs`/`xs`/`s`/`m`/`l`/`xl`, plus the `small`/`medium`/`large` spellings, which are accepted as
  authored rather than rewritten to the short form). It defaults to `m`, rendering byte-identically
  to before this property existed.
  
  `size` steps the ring's outer diameter, from a compact `1.25rem` at `2xs` up to a roomy `3.5rem`
  at `xl` (`2.5rem` unchanged at the `m` default), matching sibling `<lr-progress-bar>`'s own `size`
  in scope: it scales exactly one dimension. An explicit `--lr-progress-ring-size` (or the upstream
  `--size` alias) still wins over every tier. The track/indicator stroke width and the center
  label's font size are unaffected by the tier, also matching `<lr-progress-bar>` (which likewise
  leaves its label font size untouched by its own thickness ladder).
  
  Sibling sweep: `<lr-progress-ring>` shares its stylesheet module and its test file with
  `<lr-progress-bar>` and sits in the same component directory, but had no `size` property when
  `<lr-progress-bar>` gained its own thickness ladder — this closes that gap with the ring's own
  diameter ladder and a matching regression test suite (unset-default, the default's host-attribute
  reflection, both attribute spellings across the six-step ladder, and
  explicit-override-wins-over-every-tier — four new tests). Checked every other `*.styles.ts`
  module in the library exporting more than one component's stylesheet: `overlay.styles.ts`
  (shared by `popover`/`tooltip`) has no `size` on either sibling, so there is no counterpart gap
  there. `radio-button.styles.ts` (shared by `radio-button`/`radio`) is a different case, not a
  counterpart gap either, but for a different reason than "neither has it": `<lr-radio>` already
  declares a working, reflected `size: LyraSize = 'm'` (`radio.class.ts`), and `<lr-radio-button>`
  extends `LyraRadio` and additionally imports the same shared `sizes` design-token stylesheet in
  its own `static override styles` (`radio-button.class.ts`), so it inherits that property outright
  — `radio-button.test.ts`'s `describe('size and pill', ...)` already proves the tier changes
  rendered geometry across all six tiers and both spellings. That pair is excluded because both
  siblings already have `size`, not because neither does. Several other component families have a
  mixed `size` picture for unrelated, pre-existing reasons (for example `lr-badge` has `size` while
  sibling `lr-tag` does not); those do not share a stylesheet module with the sized sibling and are
  a separate, larger scoping question outside this task.
- de8d9b6: `<lr-swatch-picker mode="gemstone">`'s checked swatch now paints its automatic gemstone glyph
  through `theme/gemstones.js`'s exported `gemstoneSelectedGlyphStyles` stylesheet -- the same
  `CSSResult` a consumer can import and apply to a bare `gemstoneGlyph()` rendered anywhere else on
  the page (for example a header trigger button showing the current selection before it opens the
  picker in a popover) via the shared `data-lr-gemstone-selected` boolean attribute. Previously the
  picker kept its own private copy of the halo/shine treatment, so a glyph rendered outside a picker
  had to be hand-matched and could silently drift from it; now both consume the identical
  stylesheet and keyframe, so they cannot.
  
  Theme the automatic glyph's halo/shine through `--lr-gemstone-selected-color`/`-blur`/
  `-shine-duration` (unchanged defaults, so an unstyled picker looks identical to before).
  `--lr-swatch-picker-gemstone-selected-blur`/`--lr-swatch-picker-gemstone-shine-duration` still
  theme a plain color-fill swatch or a consumer-supplied `icon` override while `mode="gemstone"`,
  and their own defaults are now aliased onto the shared tokens above so the two families cannot
  drift from each other either -- but a consumer who was previously setting one of these two
  picker-specific properties specifically to restyle the automatic glyph's halo should switch to the
  shared `--lr-gemstone-selected-*` property instead, since that override no longer reaches the
  automatic glyph.
- 830ac2d: Three additions to `<lr-thread-list>` and `<lr-typing-indicator>`.
  
  - `<lr-thread-list>`: the built-in search field's native `::-webkit-search-cancel-button` was
    suppressed with nothing replacing it, so Chromium users lost the clear affordance the native
    control gave them. A `part="clear-button"` icon button now renders next to `[part="search-input"]`
    once it has a value, with a localized `this.localize('clear')` accessible name (reusing the same
    key and part vocabulary `<lr-input>`'s own `clearable` contract already uses), a keyboard-reachable
    `<button>`, and the shared `--lr-icon-button-size` hit-area floor. Clicking it clears the field,
    fires the same `lr-filter-change`/`lr-query-change` event the field already fires while typing, and
    returns focus to the input. A sibling button was chosen over composing `<lr-input>`: this search
    field is not a form control, and swapping its raw `<input part="search-input">` for `<lr-input>`
    would turn the long-documented `search-input` part from the actual input element into a wrapper
    custom element (breaking any consumer style written against it) while pulling in
    `<lr-input>`'s label/hint/error/password/form-associated machinery this field has no use for.
  - `<lr-thread-list>`: the built-in group-collapse toggle previously only emitted `lr-group-toggle`
    and never mutated `collapsedGroupIds` itself, so it visibly did nothing unless a consumer manually
    listened and reassigned the property. It now self-manages by default, mirroring
    `<lr-chat-message>`'s and `<lr-code-block>`'s own request/commit event pairs: a new cancelable
    `lr-group-toggle-request` fires first, and unless a listener calls `preventDefault()` on it, this
    component updates `collapsedGroupIds` itself before announcing the accepted change with the
    existing `lr-group-toggle` (same `detail: { groupId, collapsed }` shape as before, unchanged).
    **Compatibility:** an existing consumer that already listens for `lr-group-toggle` and reassigns
    `collapsedGroupIds` itself keeps working unchanged -- nothing calls `preventDefault()` on the new
    request event by default, so `lr-group-toggle` still fires exactly as it always did, and this
    component's own write always happens before that listener runs (same synchronous dispatch), so the
    host's own assignment simply wins last. A consumer that must veto a collapse change and stay fully
    controlled listens for `lr-group-toggle-request` instead and calls `preventDefault()`, which skips
    the built-in write and suppresses the following `lr-group-toggle` entirely.
  - `<lr-typing-indicator>`: adds a `labelPlacement` property (`'none'` default, `'after'`), mirroring
    `<lr-spinner>`'s own `labelPlacement` vocabulary. The default keeps today's screen-reader-only
    rendering byte-identical (a single `.sr-only` text node, no visible label); `label-placement="after"`
    instead renders the label (or its localized "Thinking…" fallback) visibly next to the animated
    shape in a new `part="label"` element.
- 9f0179a: A cancelable toggle request on `<lr-checkbox>`, `<lr-switch>` and `<lr-checkbox-group>`, fired
  before `checked` changes — a refused toggle now leaves the control exactly as the user found it,
  instead of flipping and snapping back.
  
  - `<lr-checkbox>` gains `lr-checkbox-toggle-request` and `<lr-switch>` gains
    `lr-switch-toggle-request`. Both are cancelable, carry `detail: { checked }` describing the state
    the control *would* take, and fire on every user path (click, Space, `<lr-switch>`'s logical
    arrow keys, and the host `click()` activation each forwards) *before* the control writes
    anything. `preventDefault()` keeps the current state: `checked`, `aria-checked` and the `checked`
    custom state never move at all, and none of `input`/`lr-input`/`change`/`lr-change` fire. A
    listener may instead answer by assigning `checked` itself during the dispatch, which suppresses
    the built-in write the same way — including when it assigns the value the control already held,
    which a before/after value comparison cannot detect. Neither event fires for a programmatic
    `.checked` assignment, a form reset, a session-state restore, or while the control is disabled.
  - `<lr-checkbox-group>` translates an owned checkbox's request into its own cancelable
    `lr-checkbox-group-toggle-request`, `detail: { value, previousValue, option }` — the group value
    that would result, the value as it stands, and the `<lr-checkbox>` the user acted on. That is
    what lets a host refuse "uncheck the last remaining option" (`detail.value.length === 0`) with no
    flicker: the option never flips, so there is nothing to revert. The child's own request is
    consumed at the group boundary and republished under the group's name, exactly as the group
    already translates a child's `input`/`change`/`lr-change`. Assigning the group's `value` from a
    listener resolves the request the same way `preventDefault()` does.
  - `<lr-tool-select-dialog>` now raises its own cancelable `lr-change` proposal from those requests
    rather than from the composed controls' settled `lr-change`. Preventing that proposal previously
    let the built-in checkbox or switch flip and then wrote it back; it now never flips. The
    composed controls' request events stop at the dialog's boundary, as their native and prefixed
    input/change events already did.
  - Nothing listening for the new events behaves byte-identically to before: a request nobody cancels
    commits and then fires the same events, in the same order, as today.
- 96f604c: `lr-virtual-list` can virtualize against an external scroll container.
  
  The new `scrollElement?: Element | Window` property points the whole windowing loop at an ancestor
  that already owns a scrollbar — or at the window itself — for a list embedded in a longer scrolling
  page rather than sized as its own panel. Previously the only scrollport was the component's own
  `[part="base"]`, so such a page ended up with a nested second scrollbar, and the only escape was to
  give the list an artificially huge `--lr-virtual-list-height` and lose virtualization entirely.
  
  While `scrollElement` is set, `[part="base"]` stops scrolling and grows to the list's full virtual
  extent, so the page's single scrollbar spans the whole list and `[part="sticky-group"]` sticks to
  that outer scrollport. Everything expressed in list coordinates keeps answering in list coordinates —
  `offsetForIndex()`, `indexAtOffset()`, `scrollToIndex()`, `active-item-id` scroll-into-view, and
  `lr-virtual-scroll`'s `scrollTop` are all still measured from the top of the list, with the component
  converting to and from the external scroller's position. Auto-height scroll anchoring moves the
  external scroller too, so measuring a row above the viewport no longer makes the page jump.
  
  There is deliberately no ancestor auto-detection: the scroller is the element you name and nothing
  else, so adding an unrelated `overflow` rule to some wrapper can never silently take the job over.
  Two consequences are worth knowing. `[part="base"]` drops its `tabindex` and its hover outline,
  because it is no longer a scrollable region and a focus stop that scrolls nothing is worse than none;
  keyboard scrolling belongs to the external scroller. And horizontal scrolling of row content that
  opted out of wrapping becomes the external scroller's responsibility, since CSS cannot leave one axis
  visible while the other scrolls.
  
  Two things stay the consumer's own: while `renderStickyGroup` is set, mirror
  `scroll-padding-block-start` onto the external scroller, because this component writes that inset on
  `[part="base"]`, where it no longer has any effect, and will not style an element it does not own
  (programmatic scrolling already subtracts the band arithmetically; native keyboard scrolling is what
  would otherwise park a row under it). And the list's position inside the scroller is re-read on
  scroll, on either box resizing, and whenever `scrollElement` changes — a layout change *above* the
  list that shifts it without any of those is not observable, so clear and re-set the property
  (`el.scrollElement = undefined; el.scrollElement = scroller`) to force a re-read.
  
  Listeners follow the property: re-pointing `scrollElement`, disconnecting, and reconnecting all
  rebind against the current target and leave nothing behind on the previous one. A value that is
  neither an `Element` nor a `Window` is ignored and the list keeps scrolling its own viewport, so
  wiring this from a ref that is still empty on a first render is safe. Leaving `scrollElement` unset
  is unchanged in every respect.
- d121ce5: `<lr-chart>`: a legend can now show a value AND its share; `<lr-chart>`/`<lr-lite-chart>`/
  `<lr-box-plot>` (and the eight typed subclasses, and `<lr-histogram>`, which all inherit
  `<lr-chart>`'s canvas ticks) get a tick-label font-size hook, all byte-identical when unset.
  
  - **New:** `legendDisplay` accepts `'value-percentage'`, appending both as `label: value
    (percentage)`, alongside the existing mutually-exclusive `'value'`/`'percentage'`. The
    `formatter`/`valueFormatter` callback backing `'value'` and `'value-percentage'` now also
    receives that same share as `percentage` in its `surface: 'legend'` context
    (`LyraChartFormatterContext`), so a custom formatter can build its own combined text without
    recomputing the share from raw data -- it was computed and then discarded before this change.
  - **New:** `--lr-chart-tick-font-size` (default `var(--lr-font-size-2xs)`, any CSS length unit)
    retunes axis tick-label text size independently of the rest of the application, mirroring the
    existing `--lr-chart-tick-color` hook's naming and resolution. `<lr-chart>`'s canvas ticks
    previously had no font-size token at all (a bare Chart.js default); `<lr-lite-chart>`'s SVG
    ticks and `<lr-box-plot>`'s canvas ticks previously read the global `--lr-font-size-2xs` token
    directly, with no per-component override.
- d121ce5: `<lr-app-rail-item>`/`<lr-app-rail>`: four remaining density hooks from the item geometry/
  current-indicator work, all byte-identical when unset.
  
  - **New on `<lr-app-rail-item>`:** `--lr-app-rail-item-font-size` retunes `[part="base"]`'s font
    size independently of family/weight/line-height (declared after the `font` shorthand, mirroring
    `<lr-button>`'s own `--lr-button-font-size`).
  - **New on `<lr-app-rail-item>`:** `--lr-app-rail-item-current-font-weight` retunes the
    `current`/`aria-current="page"` item's font weight independently of the shared
    `--lr-font-weight-semibold` token, mirroring `<lr-stepper>`'s
    `--lr-stepper-current-font-weight` and `<lr-segmented>`'s `--lr-segmented-selected-font-weight`.
  - **Fix:** in `icon-only` presentation, `[part="base"]` now resolves to a square hit target
    matching the icon-button footprint used elsewhere in this library, instead of stretching across
    the rail's icon column.
  - **New on `<lr-app-rail>`:** `--lr-app-rail-header-min-block-size` reserves a minimum height for
    `[part="header"]`, for content that mounts or resizes asynchronously (e.g. an avatar image).
  - **Fix on `<lr-app-rail-group>`:** its own `collapsible` `[part="toggle"]` had the identical
    stretch-instead-of-square defect in `icon-only` presentation; it now resolves to the same square
    hit target as `<lr-app-rail-item>`'s.
- d121ce5: `<lr-document-viewer>` gains two documented sizing hooks directly on the host element:
  `--lr-document-viewer-width` forwards straight to the nested `<lr-dialog>`'s own
  `--lr-dialog-width`, giving the panel an assertive width without reaching through to the dialog's
  own internal custom properties; `--lr-document-viewer-min-height` sets `[part="body"]`'s minimum
  block size (default `var(--lr-size-12rem)`, unchanged from before), matching the existing
  `--lr-document-viewer-max-height` hook on the same axis.
- 10dace2: `<lr-textarea>` gains `--lr-textarea-focus-border-color`, the last missing member of the
  resting/hover/focus border-colour quartet its siblings already publish. Unset, it resolves to the
  field's own resting border colour, so a textarea with no override renders exactly as it did before
  — only the shared focus halo and the native outline told a focused field apart from a resting one
  until now.
  
  The same asymmetry — a border or fill that only some interaction states could retint — was also
  found and closed on `<lr-model-select>` (`--lr-model-select-trigger-border-color`,
  `--lr-model-select-trigger-fill`), `<lr-voice-picker>` (`--lr-voice-picker-trigger-border-color`,
  `--lr-voice-picker-trigger-fill`), `<lr-code-editor>` (`--lr-code-editor-border`,
  `--lr-code-editor-fill`), `<lr-emoji-picker>` (`--lr-emoji-picker-search-border-color`,
  `--lr-emoji-picker-search-fill`) and `<lr-color-picker>` (`--lr-color-picker-border-color`) — each
  already had an independently themeable hover, open, or invalid state but a hardcoded resting one.
  Every new hook is read through an inline `var()` fallback and never declared on `:host`, so nothing
  renders any differently until a consumer sets one, and each can still be set on an ancestor to
  retheme a group at once.
- a653796: `<lr-combobox>`'s `lr-source-error` event now carries the query that failed
  
  `detail: { error, query }` — `query` is the exact text the rejected `source` call was made with,
  not necessarily the live filter text, which may have moved on (or been cleared, for example by
  closing the listbox) by the time the rejection settles. `error` keeps carrying the raw rejection
  as before, so existing listeners are unaffected.

### Patch Changes

- 68451c6: `<lr-drawer>` inherits `<lr-dialog>`'s `size` property, but it previously had no effect on the
  rendered panel. `size` now caps the panel's `max-inline-size` on `start`/`end` placements on the
  same six-step ladder documented under `<lr-dialog>` (`top`/`bottom` placements are unaffected,
  since those axes are already unconditionally full-viewport). `size` still defaults to `m`, which
  renders byte-identically to before this fix since that tier's cap already exceeded the panel's own
  default width. An explicit `--lr-dialog-max-width` override still wins over every tier.
- 152ea02: Fixed a WebKit-only regression where a consumer's forwarding-slot wrapper around optional named
  slot content (hint, footer, avatar, and similar chrome slots) could make that chrome disappear
  after an unrelated text mutation deep inside the forwarding chain.
  
  The affected components (`<lr-switch>`, `<lr-checkbox>`, `<lr-agent-run>`, `<lr-artifact-panel>`,
  `<lr-browser-frame>`, `<lr-result-card>`, `<lr-tool-result-dialog>`, `<lr-tool-select-dialog>`,
  `<lr-chat-message>`, `<lr-handoff-divider>`, `<lr-usage-badge>`, `<lr-flow-node>`, `<lr-stat>`,
  `<lr-color-picker>`, `<lr-emoji-picker>`, `<lr-locale-picker>`, `<lr-phone-input>`, `<lr-app-rail>`,
  `<lr-menu-item>`, `<lr-chip>`, `<lr-dialog>`, `<lr-source-card>`, and `<lr-document-preview>`)
  tracked a named slot's presence by re-reading `HTMLSlotElement.assignedElements()` inside that
  slot's own `slotchange` handler. WebKit has been observed reporting that live snapshot as
  transiently empty when a mutation happens deep inside a nested forwarding `<slot>` placed inside
  the assigned element, even though the assigned element's own `slot` attribute never changed. Every
  listed component now re-derives presence from the light-DOM `slot` attribute directly, the same
  technique each of them already used for its own initial mount/reconnect computation, so the two
  now agree and neither depends on the browser's live slot-assignment snapshot.
- 9f0179a: `<lr-activity-feed>` no longer throws during a server render.
  
  Its focus-repair capture runs in `willUpdate()` and read `this.renderRoot`
  directly. `willUpdate()` runs before the first render, and under
  `@lit-labs/ssr` there is no render root at all at that point, so any server
  render of the component crashed with `Cannot read properties of undefined
  (reading 'querySelector')`. It fired on the very first update because the guard
  around it keys off `entries`, and a defaulted reactive property is always
  reported as changed on first update.
  
  Focus repair is meaningless before anything is painted, so an absent render root
  now simply means "nothing to repair".
  
  Swept the rest of the library for the same shape: every other `willUpdate()`
  that reaches a render or shadow root either uses optional chaining, routes
  through the shared active-element helper (which returns null for a nullish
  root), or sits behind a focus-ownership precondition that cannot hold before
  first paint. `<lr-activity-feed>` was the only genuine instance.
- 71265a6: Five bug fixes across `<lr-animation>`, `<lr-card>`, `<lr-segmented>`, `<lr-tree-item>`, and
  `<lr-locale-picker>`.
  
  - `<lr-animation>`: the host declared no `color`/`font` alongside its `display: contents`, so it
    inherited the library's base default text color/font instead of the ambient color/font of
    whatever it is slotted into. Bare, unstyled slotted content (exactly what this component's own
    stories use) therefore rendered in the library default color inside a dark card or a coloured
    alert instead of blending in. The host is now `color: inherit; font: inherit;`, matching how
    `<lr-tab>` was fixed for the identical mechanism.
  - `<lr-card>`: with `href` set, `[part="base"]` becomes an absolutely-positioned empty
    stretched-link overlay behind the real, visibly clipped content in `.linked-content`. Both
    independently repeated `var(--border-radius, var(--lr-radius))`, so a consumer overriding
    `::part(base) { border-radius: … }` directly (rather than through the documented
    `--border-radius` hook) reshaped only the invisible overlay, producing mismatched corners
    between the card's visible chrome and its content clip. Both now read a single
    `--_lr-card-radius` token; the linked overlay's declaration is pinned with `!important` (the one
    case that still outranks an outer `::part()` rule) so it can never drift from
    `.linked-content`'s again. The `--border-radius` hook is unaffected and still reshapes both
    identically. Non-linked cards are untouched — `::part(base)` continues to work normally there,
    since there is no `.linked-content` twin to fall out of sync with.
  - `<lr-segmented>`, `<lr-tree-item>`, `<lr-locale-picker>`: the interactive segment/item/toggle/
    trigger/option surfaces changed `background`/`color` on `:hover`/`:active` with no `transition`
    declared anywhere in the file, so their paint snapped while `<lr-button>`/`<lr-copy-button>`/
    `<lr-icon-button>` ease. Each now declares
    `transition: background-color var(--lr-transition-fast)` (plus `color` for `<lr-segmented>`'s
    segment, which also recolors on hover/active). No component-local
    `prefers-reduced-motion` block is needed: `tokens.styles.ts` already flattens
    `--lr-transition-fast` to `0.001ms` and applies a blanket `transition-duration: 0.001ms` across
    the whole shadow tree under reduced motion.
- 10dace2: Corrects `--lr-chart-tick-font-size`'s unset default on `<lr-chart>` (and its eight typed
  subclasses, `<lr-histogram>`, and `<lr-box-plot>`) so it once again renders byte-identically to
  before the token existed.
  
  The token's own canvas default had briefly resolved an unset `--lr-chart-tick-font-size` to
  `var(--lr-font-size-2xs)` (about 10px). These canvas charts never had a font-size token before,
  so Chart.js's own built-in 12px tick font size was what actually rendered; `--lr-font-size-2xs` is
  correct only for `<lr-lite-chart>`'s SVG axis labels, which already used it before this token
  existed and still do. `<lr-chart>`/`<lr-box-plot>` ticks now default to `var(--lr-font-size-xs)`
  (12px), matching Chart.js's own default again.
  
  The radar/polarArea `r`-scale `pointLabels` (the spoke labels) follow the same rule but land on a
  different pre-existing number: Chart.js's `RadialLinearScale` gives `pointLabels` its own built-in
  10px default, distinct from its 12px global tick default, so `pointLabels` now stays unset (and
  therefore at that 10px) until `--lr-chart-tick-font-size` is explicitly set, at which point both
  the ticks and the point labels adopt the same size together, as documented.
  
  Setting `--lr-chart-tick-font-size` explicitly is unaffected by any of this and continues to work
  as already documented.
- 5a8c8b8: `<lr-map>` is now developed and tested against maplibre-gl 6.10. The published peer range is
  unchanged (`>=5 <7`), so no consumer has to move; this only records which version the component's
  own tests and the bundled peer-graph checks now run against.
- 91e81ec: Fix two `storage-key` persistence bugs:
  
  - `lr-app-rail`: a mount with a persisted `preferred-mode` could emit a real `lr-mode-change`
    carrying the wrong, pre-restore breakpoint-derived mode, immediately followed by a second,
    correct event once the restore landed — `connectedCallback()`'s breakpoint computation always ran
    before that same mount's `willUpdate()` had a chance to restore the persisted preference. The
    first mount's settled mode (whether from the initial breakpoint match alone, or a persisted
    `preferred-mode` overriding it) now always resolves before anything is announced, so a listener
    observes exactly one `lr-mode-change`, carrying the final mode, once the first render has landed.
    Live breakpoint crossings after mount and explicit `forceMode` assignments are unaffected.
  - `lr-table`: `willUpdate()`'s persisted `priorityColumnsVisible` restore had no guard at all, so
    `<lr-table storage-key="…" priority-columns-visible>` (or a `.priorityColumnsVisible=${true}`
    binding) was silently overwritten by stale `localStorage` state on first update. An explicitly
    declared `true` now wins over the persisted value.
  
  Also corrects the `storageKey` documentation for `lr-app-rail`, `lr-table`, and `lr-widget`, which
  previously claimed all three implement an identical persistence-restore guard. They do not:
  `lr-app-rail`'s undefaulted `railWidthPx`/`preferredMode` fields let it key the guard off
  `willUpdate()`'s `changed` map; `lr-table`'s `priorityColumnsVisible` defaults to `false`, which
  already reads as "changed" on every mount, so its guard instead checks the property's own current
  value; and `lr-widget` uses a coarser single-shot flag set by any assignment, including its own
  restore write.
- 9826111: Fixes a WebKit-only positioning bug affecting every hoisted overlay (`<lr-select hoist>`,
  `<lr-dropdown hoist>`, `<lr-combobox>`, `<lr-popover>`, `<lr-tooltip hoist>`,
  `<lr-color-picker>`, `<lr-date-input>`, and any other consumer of the shared positioner's default
  `fixed` strategy): when an ancestor of the trigger had `backdrop-filter` or `filter` applied, the
  hoisted popup could render hundreds to well over a thousand pixels away from its trigger —
  unreachable by pointer or keyboard, and often entirely off-screen — while Chromium and Firefox
  positioned the same markup correctly. The popup's own `left`/`top` ended up computed against the
  viewport, then the browser resolved them a second time against the filtered ancestor's box,
  because current WebKit (unlike older Safari/WebKitGTK builds) now treats `backdrop-filter`/
  `filter` as establishing a containing block for `position: fixed` descendants, matching Chromium
  and Firefox's existing behavior — a case the underlying positioning library's own containing-block
  detection still explicitly excludes on WebKit. The shared positioner now detects that ancestor
  itself and positions against it, so every hoisted overlay lands next to its trigger in all three
  engines.
- 9f0179a: `<lr-multi-split>`: `lr-multi-split-collapse-change` now fires after the collapsing panel is
  decorated, not before.
  
  The panel's own decoration — its `data-collapse-state` marker, the `hidden` flag the closed
  `'floating'` drawer uses, and its owned inline sizing — was applied in the component's update
  callback, which runs after the event had already been dispatched. A listener reading the panel
  synchronously inside its own handler therefore saw the previous state's decoration (or none at
  all) while the event and the host attribute already reported the new one, forcing consumers to
  defer every panel-dependent read past the element's update completion and to add CSS masking the
  undecorated frame.
  
  The decoration pass now runs immediately before the event is emitted, so the panel a handler reads
  is already in its new state. The event is still synchronous and still fires only on a real
  transition — nothing about when it fires relative to the caller changed, only what the DOM looks
  like by then.
- 9f0179a: Sweep: a component-owned `hidden` flag on a slotted node now wins over an author `display` rule,
  and two slotted-content allocations stop overflowing.
  
  - `<lr-random-content>`: a candidate the rotation has selected out is flagged `hidden` by the
    component itself, but its `::slotted([hidden])` rule was normal-weight. Per CSS Cascade 5's
    shadow-tree encapsulation-context ordering, any author `display` rule matching the candidate in
    its own light-DOM tree already outranked it, so every rotated-out candidate stayed painted and
    the rotation went visually inert while only assistive technology followed the selection. The
    rule now uses `display: none !important`, matching `<lr-multi-split>`'s owned panels,
    `<lr-toast>`'s queued items and `<lr-avatar-group>`'s overflow, which already force their own
    state that way. Author-driven `hidden` elsewhere in the library deliberately stays
    normal-weight, so an author who sets both `hidden` and `display` on their own node still wins.
  - `<lr-browser-frame>`: the slotted viewport surface is given `inline-size: 100%`/`block-size: 100%`
    but `box-sizing` does not inherit across the slot boundary, so a surface with its own padding or
    border resolved those as its content box and overflowed `[part="viewport"]` (a block container,
    which cannot shrink it back). It is now `border-box`.
  - `<lr-carousel>`: each slide is given a definite, non-shrinking flex basis
    (`flex: 0 0 <slide basis>`), so a padded or bordered slide overran the track and pushed its
    scroll-snap edge past the next slide. Slides are now `border-box`, which also makes a looped
    clone exactly as wide as the slide it copies — clones live in the shadow tree and were already
    `border-box` through the shared reset.
  - `<lr-chip-group>`: the overflow collapse writes its own `hidden` on slotted children (that is what
    the "+N" pill stands in for), but the group had no `::slotted([hidden])` rule at all, so any
    normal-weight author `display` rule matching those children re-revealed them next to a pill
    claiming to be standing in for them. A child that is an `lr-*` element was already covered by its
    own `:host([hidden])` reset; this slot documents any content, including plain elements and SVG.
    The forcing rule carves out `hidden="until-found"` so find-in-page can still reveal an author's
    own collapsed child.
  - `<lr-timeline>`: `scale="time"` absolutely positions each slotted item at `inline-size: 100%`
    (`calc(100% - lane indent)` under `collision="stack"`), and `box-sizing` does not inherit across
    the slot boundary, so an item with its own padding or border resolved that as its content box and
    overflowed the host — nothing shrinks an absolutely positioned box back. Slotted time-scale items
    are now `border-box`; an `<lr-timeline-item>`, already `border-box` through its own reset, renders
    identically.
  - `<lr-dashboard-grid>`: a cell's slotted tile is given `inline-size: 100%`/`max-inline-size: 100%`
    in the same content-box trap. The flex cell already shrank an ordinary padded tile back inside, so
    this changes nothing for one; a tile the author pinned with `flex-shrink: 0` had nothing to absorb
    the overshoot and overran its cell. Tiles are now `border-box`.
- 96f604c: `<lr-app-rail>` actually restores a persisted `open` state again, and `<lr-table>` stops overwriting
  a `priorityColumnsVisible` binding that pins the columns hidden.
  
  Both bugs come from the same question a `storage-key` component has to answer before it restores
  anything: did the consumer set this property, or is it still sitting on its declared default? Only
  the second may be overwritten.
  
  - **`<lr-app-rail>`:** the `open` restore was gated on whether Lit had recorded a change for `open`
    during the first update. Lit records a declared default there as well, so the gate was closed on
    every mount and a persisted `open` was never restored at all — silently, for every rail using the
    default `persist="open width"` allowlist. `open` (and `rail-width-px` and `preferred-mode`
    alongside it) now track whether the property was actually assigned, so a stored `open` state is
    applied on the next mount, and no `lr-toggle` fires for it — a restore is not a user action.
    Worth knowing if you relied on the broken behaviour: a rail left open at the mobile breakpoint is
    now open again on the next mount — but only on a mount whose breakpoint-derived `mode` is already
    `'mobile'`, the one mode `open` means anything in. Reopen the same page at a desktop width and the
    stored `open` is dropped instead of sitting there primed to throw a focus-trapping overlay over
    the page the first time the viewport narrows. `persist="width preferred-mode"` keeps the layout
    preference and leaves the transient overlay out of storage, exactly as its documentation already
    described.
  - **`<lr-table>`:** the `priority-columns-visible` restore was gated on the property's own current
    value ("still `false`, so nobody set it"). That cannot see a binding that deliberately assigns the
    default, so `.priorityColumnsVisible=${false}` — a host pinning the priority columns hidden — was
    overwritten on mount by a stale stored `true`. An explicit binding now wins, whichever value it
    carries.
  
  Unchanged in both: an explicit binding or a parse-time attribute still beats stored state; a
  component with no `storage-key` still touches storage neither to read nor to write; the restore
  still happens once, before the first paint, and never re-runs on a reconnect.
  
  `<lr-widget>`'s `collapsed` restore already tracked assignments correctly and behaves exactly as
  before; it now shares the mechanism rather than keeping its own copy of it. That includes the
  timing: `collapsed` still writes its reflected `[collapsed]` attribute during the assignment, so a
  `lr-collapse-change` listener reading the attribute still sees the state the event announced.
- 34cdefb: `<lr-table>` no longer discards a column width that a `lr-column-resize` listener applied for
  itself while vetoing the proposed one.
  
  Both committed resizes — the keyboard step on `[part="resize-handle"]` and the drag-end commit —
  apply the new width optimistically, dispatch the cancelable `lr-column-resize`, and roll the width
  back when a listener calls `preventDefault()`. That rollback is a write that lands *after* the
  synchronous dispatch, so it also overwrote a width the listener had just resolved for itself from
  inside that same dispatch (refusing the proposed step and driving the handle to the width it
  actually wanted), leaving the column on the stale pre-dispatch width instead. A veto now rolls back
  only a width that nothing else replaced during the dispatch; a veto that writes nothing still
  restores the previously committed width exactly as before, including when that width differs from
  the one originally declared on the column.
  
  The distinction is tracked as "did a write happen", not as a before/after width comparison, so a
  listener that re-applies the width the column already carried is honoured rather than read as
  having touched nothing.
- ceeaf12: Two bug fixes across `<lr-tool-approval-dialog>` and `<lr-tool-timeline>`, mirroring the veto-clobber
  fix already shipped for `<lr-confirm-bar>`.
  
  - `<lr-tool-approval-dialog>`: `onApprove`/`onDeny` dispatched the cancelable `lr-approve`/`lr-deny`
    event synchronously, then unconditionally overwrote `.pending` with their own built-in value —
    silently clobbering a synchronous listener that had called `preventDefault()` and then resolved
    the decision itself (by calling `close('approve'|'deny')` or setting `.pending` directly) instead
    of waiting for the built-in pending/loading presentation. Unlike `<lr-confirm-bar>`, nothing else
    in this component reconciled the stuck value afterwards. That listener's own state now wins: the
    built-in `pending` fallback only applies when the listener left both `.pending` and `.open`
    untouched.
  - `<lr-tool-timeline>`: `onDialogApprove`/`onDialogDeny` unconditionally set the private
    `approvalPending` state whenever a host canceled the wrapper `lr-tool-approval-decide` event, even
    when the host had already resolved the entry synchronously by reassigning `entries` (the documented
    alternative to calling `finalizePendingApproval()`/`revertPendingApproval()`) from inside that same
    listener. The stale pending flag left the shared `<lr-tool-approval-dialog>` showing a pending
    spinner over an already-resolved entry. The entry's live state is now re-checked immediately after
    dispatch, directly against the current `entries`, so a pending flag is never set or kept for an
    entry that no longer needs a decision.
- 10dace2: Fix two `<lr-heatmap>` documentation defects, none of which change runtime behavior:
  
  - The reference claimed "Slots: none." The `legend` slot — custom content rendered inside the
    built-in legend row — has actually been available since 13.0.0; the doc simply never disclosed
    it.
  - The capped-grid ("Known gotchas") advice to "position it with ordinary CSS on the host" when
    `fit-to-width` clamps the canvas below the host's own width was itself broken: `fit-to-width`
    derives the canvas size from the host's own measured width, so making the host shrink-to-fit
    (`inline-block`, `fit-content`, floating it) to chase the capped canvas creates a circular sizing
    dependency that settles at the wrong size. The doc now recommends the recipe that actually works —
    `::part(canvas) { margin-inline: auto }` to center it, `margin-inline-start: auto` to end-align it
    — and now also calls out that `::part(base) { align-items: center }` is a tempting but different
    fix: it centers the canvas too, but shrink-wraps every other child of the base flex column along
    with it, collapsing the legend row's width in the process.
- 19c6e3e: Ease hover and press paint consistently across the library. Dozens of components repainted their
  background, text or border colour instantly on `:hover`/`:active` while `<lr-button>`,
  `<lr-copy-button>` and `<lr-icon-button>` eased theirs over `--lr-transition-fast`, so an interface
  mixing them felt inconsistent — a toolbar button would ease while the menu item next to it snapped.
  
  Every affected part now carries the same `--lr-transition-fast` transition on its resting rule. Only
  the interpolation changes: resting and hovered colours are untouched, so nothing renders differently
  once a transition settles. Parts whose hover state only changes `opacity`, `outline`, `filter`, an
  SVG `fill`/`stroke`, or a range input's `accent-color` were deliberately left alone.
  
  This needs no `prefers-reduced-motion` handling of its own: the token layer already collapses
  `--lr-transition-fast` to `0.001ms` under that query and applies a blanket near-zero
  `transition-duration` across every component's shadow tree.
- 34cdefb: Documents why `<lr-icon-button>` has no `size` attribute, now that the shared six-step size ladder
  reaches most of the library.
  
  Its dimension is an accessibility floor, not a density dial. `--lr-icon-button-size` states the
  minimum tappable target the whole library sizes its icon controls against, and the ladder's small
  tiers sit at or under WCAG 2.5.8's 24px minimum (`2xs` resolves to 20px, `xs` to 24px), so wiring
  the target to the ladder would let `size="2xs"` ship an untappable control — silently, because the
  ladder is the mechanism every neighbouring control uses correctly. A smaller icon button therefore
  stays an explicit, single-purpose decision: override `--lr-icon-button-size`, which reads as the
  accessibility trade-off it is. Scaling the glyph inside that floor remains `--lr-icon-size`.
- 90dbfd2: `<lr-lite-chart>`: the first and last category-axis tick labels no longer overhang the plot's own
  clipped edge.
  
  Both boundary ticks previously centered (`text-anchor="middle"`) at the plot's own left/right
  boundary, with only a small fixed inline padding beyond it — half of a sufficiently wide label
  (a typical date string, for example) painted past that edge and was clipped by the `svg`'s
  `overflow: hidden`, most visibly on the trailing tick (e.g. "Sep 14" rendering as "Sep 1"). Label
  decimation (`maxLabels`) only reasons about label-to-label collision, so nothing previously
  accounted for the plot's own boundary. The two boundary ticks now anchor toward the plot's
  interior (`text-anchor="start"` for the first, `"end"` for the last) instead of centering; every
  other tick still centers, unchanged. Because SVG's `start`/`end` anchors already mirror with an
  inherited `direction: rtl`, and the plot's own small-padding side swaps with it too, this stays
  correct under RTL without inverting which rendered category gets which keyword.
- 61c2d66: `<lr-lite-chart>`: category-axis tick labels no longer overlap each other, or clip past the
  plot's own boundary, on a font whose glyphs are wider than the library's internal size estimate.
  
  Ellipsizing a category-axis label used a fixed per-character pixel estimate
  (`APPROX_LABEL_CHARACTER_WIDTH`) to decide how much of a label's text fits in its slot. That
  estimate is only ever a guess: real glyph widths vary by platform and font, so on a browser whose
  default font renders wider than the estimate assumed, a label kept more characters than its slot
  actually had room for and visibly overlapped its neighbor -- most reproducibly with `maxLabels`
  decimating a long category list at a narrow chart width. The character estimate now only sizes a
  label before the chart has any real layout to measure (server rendering, the pre-hydration paint);
  once the chart is in the DOM, each tick is re-fit against the browser's own measured text width
  (`SVGTextContentElement.getComputedTextLength()`), the same technique the x/y axis title already
  used. The per-tick budget this fit targets is also now each survivor's REAL distance to its actual
  rendered neighbor rather than an evenly-averaged decimation stride, so a boundary tick (the first
  or last surviving label, which already anchors toward the plot's interior) can still render a long
  label in full when there's room, without starving the interior neighbor it borders.
- df335ad: `lr-markdown` / `lr-markdown-core`: a fenced-block highlight batch whose tokenization rejected unexpectedly (for example a lazy `languages` loader whose memoized failure is re-thrown from its reporting hook) is now recorded as failed for every key in the batch, so the block settles on its plain-text fallback. Previously such a key was neither cached nor failed, and every re-render re-queued it into the same rejection in a microtask-tight loop that never yielded to the page.
- 152ea02: Dev-mode unknown-attribute diagnostics no longer warn about framework-owned scoping/debug
  attributes
  
  Angular's default emulated view encapsulation writes `_ngcontent-*`/`_nghost-*` scoping markers,
  its dev builds add `ng-reflect-*` input reflections and `ng-version`, and Vue's scoped styles add
  `data-v-*` — none of these are unknown attributes anymore. A genuinely misspelled or unsupported
  attribute still warns.
- 96f604c: Document that a nested `<lr-details>` or `<lr-accordion>` does not scope its events to itself, so a
  listener on an outer instance must filter by target.
  
  Every event both components emit bubbles and is composed, and that is deliberate — non-bubbling
  disclosure events would be a breaking change, and `event.target`/`event.currentTarget` already tell
  an inner instance from an outer one. What was missing was saying so where consumers read the API:
  
  - `<lr-details>`: the class documentation now carries the nested-disclosure example and the
    `event.target !== event.currentTarget` guard, matching the note `<lr-dialog>` already ships for
    `lr-close`. A `<lr-details>` nested in another one — in the default panel or in `header-actions` —
    sends its `lr-show`, `lr-hide`, `lr-toggle`, `lr-after-show` and `lr-after-hide` straight through
    the outer panel, where an inner disclosure opening otherwise looks identical to the outer one
    opening.
  - `<lr-accordion>`: the same gap existed for nested groups, and it bites harder there, because
    `lr-expand`/`lr-collapse`/`lr-toggle-request`/`lr-after-expand`/`lr-after-collapse` carry a
    `detail.item` belonging to the inner group. An unguarded outer handler that looks that item up
    among its own children finds nothing, or acts on a panel it does not own. Coordination itself was
    always scoped — an outer group never applies its single-panel invariant, roving keyboard model, or
    lifecycle to an inner group's items — so only the listener ever needed the guard.
  
  Each individually affected event also carries a one-line pointer back to that guard, the shape
  `<lr-dialog>` already uses on `lr-show`/`lr-after-show`/`lr-hide`/`lr-after-hide`: a consumer reading
  only the entry for `lr-show`, `lr-after-hide`, `lr-expand` or `lr-after-collapse` now sees the filter
  without having to find the longer note under a different event.
  
  No runtime behaviour changed in either component; both now have a regression test asserting a nested
  instance's events reach an outer listener and that the documented target filter separates the two.
  `<lr-details>` additionally gained a test pinning that a vetoed `lr-hide` announces neither
  `lr-toggle` nor `lr-after-hide` and still settles its `hide()` promise.
- a653796: Fixes `<lr-table>` so a `sticky` column's own header cell now shows
  `--lr-table-header-sorted-bg`/`--lr-table-header-sorted-color` while that column is sorted, instead
  of always painting the plain surface color.
  
  A sticky sortable column's `<th>` carries both the internal sticky-positioning marker and
  `aria-sort` at once, and the sticky rule's opaque background declaration out-specified the sorted
  rule regardless of source order, hiding the sorted-header tint on exactly the columns most likely
  to use it (pinned identifier columns are also the ones a consumer sorts by). This pairs with the
  earlier fix that let a sticky column's body cell show its row's stripe/hover/selected background
  instead of a flat surface color -- the header half of that same defect is now closed too.
- de8d9b6: The default Oniguruma engine that `<lr-code-block-core>`/`<lr-markdown-core>` (and the fine-grained
  `languages` path of `<lr-code-block>`/`<lr-markdown>`) build their fenced-code highlighter with now
  fetches the binary `shiki/onig.wasm` asset instead of importing `shiki/wasm`, the base64-inlined
  module that specifier resolves to — roughly 82 KB gzip smaller and streaming-compilable. Also added
  `setShikiCoreEngine('oniguruma' | 'javascript' | factory)`, letting a consumer opt into shiki's
  pure-JS regex engine (no WebAssembly at all) or supply a pre-instantiated engine of their own; the
  two engines never share a cached highlighter. This is an implementation detail with the same public
  component API and smaller build output — no consumer action needed. A consumer who previously
  aliased the internal `shiki/wasm` peer specifier directly should switch that alias to
  `shiki/onig.wasm`, or call `setShikiCoreEngine('javascript')` to avoid WebAssembly entirely.
- 68451c6: Fixed dangling cross-references in the generated per-component reference
  (`llms/components/<tag>.md`): a "see the ... note above" (or "this file's ... section") pointer
  only resolves while reading the whole family file, and is lost once a component's own section is
  split into its self-contained page. `<lr-stepper>`'s "picking a basis" gotcha now inlines the full
  explanation and worked example that previously lived only under `<lr-multi-split>`'s own section,
  and `<lr-knowledge-graph-explorer>`'s `nodes`/`links`/`fitTo`/`nodeLabels`/hull-color entries now
  link straight to `<lr-graph>`'s own published reference instead of pointing at "this file"'s
  `lr-graph` section, which the split reference page never contains. The generator now also fails
  the build on a future positional "above"/"below" reference, or a future "this file's `<tag>` ...
  section" reference, that would dangle the same way once split.
  
  Added a target-filtering note to `<lr-drawer>`'s own `@event lr-close` JSDoc, matching
  `<lr-dialog>`'s: the name is not drawer-scoped, so a listener bound on `<lr-drawer>` should guard
  on `event.target` before reading a descendant's close.
  
  Published the `--lr-theme-form-control-height-2xs`…`-xl` tier to the design-token reference
  alongside the existing `--lr-theme-form-control-radius` entry, matching the shipped CSS these
  override hooks already control.
- 68451c6: Documents a compact-disclosure recipe for form-control hint text in `llms/shared.md`, under a new
  "Presenting hint text as a compact disclosure" section: compose an icon-only `<lr-icon-button>`
  inside an `<lr-tooltip>` (or `<lr-details>` for an inline expand/collapse) and slot it into a
  control's `label` slot, keeping a visually-hidden copy in the control's own `hint` slot so the text
  stays wired into `aria-describedby`. `<lr-checkbox>`/`<lr-switch>` — whose default slot is the
  clickable label — place the same trigger as a DOM sibling instead. No component code changed, and
  no `hint-display`/`hint-placement` attribute exists or is planned; the library keeps a single,
  predictable "hint renders as permanent text" default, and this composition is how to build a
  compact presentation on top of it today.
- 152ea02: `<lr-drawer>` no longer stretches its body to fill the panel and push `footer` to the panel's far
  edge. `<lr-drawer>` extends `<lr-dialog>` and had silently inherited the `[part="body"]` growth
  `<lr-dialog>` gained for its own `--lr-dialog-height` opt-in; unlike `<lr-dialog>`'s panel, a
  drawer's panel is always a definite size, so that inherited rule always took effect instead of only
  once a consumer opted in. `<lr-drawer>` now keeps `body` at its natural content size again, matching
  its documented, unaffected behavior.
- a653796: Sweep of the `::slotted()`-driven `[hidden]` guard and `::slotted(*)` `box-sizing` shapes that
  `<lr-multi-split>`'s "floating" panel already carries, across every other component with the same
  shapes:
  
  - Fixed: `<lr-card>`'s `[part~="media"] ::slotted(*)` allocates a definite `inline-size: 100%` to
    the slotted media child but never declared `box-sizing: border-box`. Because `box-sizing` does
    not inherit across the slot boundary, a padded or bordered media child (an `<img>` wrapper, a
    custom placeholder, etc.) silently overflowed the media frame by its own padding and border
    instead of filling it exactly, the same defect already fixed for `<lr-multi-split>`,
    `<lr-carousel>`, `<lr-dashboard-grid>`, and `<lr-timeline>`.
  - Reviewed, not changed: `<lr-video-playlist>`'s slotted `<lr-video>` children are toggled via
    `video.hidden`, matching the shape that needed a `::slotted([hidden])` restatement elsewhere —
    but every `lr-*` element already carries its own `:host([hidden]) { display: none !important }`
    from the shared base stylesheet, so an outside `display` rule (however specific) cannot re-show
    it; adding a redundant `::slotted([hidden])` rule here would do nothing.
  - Reviewed, not changed: `<lr-split-panel>` and `<lr-page>`'s `::slotted(*)` rules only cap an
    otherwise-`auto` width with `max-inline-size: 100%`, never a definite `inline-size`/flex-basis
    allocation. A block-level slotted child's `width: auto` already resolves to exactly fill its
    container regardless of `box-sizing`, so no padding/border overflow is possible there; confirmed
    with a padded/bordered slotted child in both components before deciding not to add a no-op
    declaration. The same reasoning excludes the remaining `::slotted(*)` rules in the library that
    only set `max-inline-size`/`min-inline-size` (icon/adornment/action-row slots in
    `<lr-badge>`, `<lr-chip>`, `<lr-alert>`, `<lr-callout>`, `<lr-toast-item>`, `<lr-kbd>`,
    `<lr-spinner>`, `<lr-dialog>`, `<lr-tool-approval-dialog>`, `<lr-tool-select-dialog>`,
    `<lr-agent-workspace>`, `<lr-flow-run-status>`, `<lr-input>`, `<lr-time-input>`,
    `<lr-menu-item>`, and others) — none allocate a definite size to the slotted node, so
    `box-sizing` cannot change their rendered result.
- d121ce5: `<lr-widget>`: documented that an explicit `collapsed` assignment before the first update —
  an attribute, a property, or a framework binding, including one that pins the property to its own
  default `false` — always wins over a `storageKey`-restored value and skips the restore for that
  mount. The `storageKey` and `collapsed` doc comments (and the generated reference) no longer claim
  an "identical" pattern with `<lr-app-rail>`/`<lr-table>`; instead they name the shared
  explicit-beats-persisted guarantee, which `<lr-table>`'s own `storageKey` doc already states in
  matching language (`<lr-app-rail>` implements the same guard but does not yet spell out the
  precedence in its own public docs — that gap is unchanged by this fix). A new Known gotchas entry
  covers uncontrolled persistence: don't bind `collapsed`, read the restored value back after
  `updateComplete` and track further changes from `lr-collapse-change`. No behavior changed — the
  guard itself already shipped.
- d121ce5: `<lr-typing-indicator>`: the visible `part="label"` rendered by `label-placement="after"` is now
  `aria-hidden="true"`, matching `<lr-gauge>`'s own `part="label"` treatment. The host's `aria-label`
  already carries the identical string as this component's accessible name, so the visible copy was
  reachable a second time as its own accessibility-tree node for the same text; it is now purely
  decorative, as `<lr-spinner>`'s equivalent default-slot label already effectively is (there the
  slotted content is the one and only source of the name, so no duplicate node exists). No visual
  change, and `label-placement="none"`'s screen-reader-only rendering is unaffected.
- 96f604c: `<lr-timeline orientation="horizontal">` is now reachable by keyboard while its strip actually
  overflows.
  
  Timeline items are deliberately passive — no roving tabindex, no per-event selection — so a
  horizontal timeline is a scroll container with nothing tabbable inside it. Its `[part='base']`
  carried `tabindex="-1"`, which is focusable only by script, so every event that had scrolled past
  the edge was pointer-only content: a keyboard user could neither reach the strip nor scroll it.
  
  `[part='base']` now rises to `tabindex="0"` while (and only while) a `horizontal` strip genuinely
  overflows, with a `::part(base):focus-visible` ring drawn from the shared focus-ring tokens. A
  strip that fits, and the `vertical` default — whose rules never give that box a scrolling axis at
  all — return to `tabindex="-1"` exactly as before, so a timeline that has nothing to scroll costs a
  keyboard user nothing. Nothing else about the component changes: the items stay passive, cluster
  markers remain the only activatable controls, and the edge-fade mask is unaffected.
- 10dace2: `<lr-input>` documents, with its reason, the deliberate decision not to add a password-purpose
  preset: the one thing such a preset would actually save — `autocomplete` — has no single correct
  value for "a password field" (`new-password` on a set/change/reset flow, `current-password` on a
  login one, and one is never derivable from the other), so a `purpose`/`preset` property would still
  need a second parameter carrying that same distinction, in exchange for a non-standard vocabulary a
  migrating `wa-`/`sl-`/native `<input type="password">` author would have to learn instead of
  carrying over unchanged. Compose `type="password"`, `password-toggle`, `autocomplete="new-password"`,
  and the existing `match` cross-field constraint directly for a set/change/reset confirmation pair; a
  login field needs only `type="password"` and `autocomplete="current-password"`. No other component
  ships a `type="password"` mode, so no sibling needed the same decision; `<lr-otp-input>` already
  defaults its own `autocomplete` to `one-time-code` because it has exactly one purpose, unlike
  `<lr-input>`'s many `type`s.
- a653796: Fixes `<lr-combobox>` so a committed value never flashes its raw, unbadged string on the trigger
  (or a `multiple`-mode tag) while an async `source` fetch has never yet resolved for it.
  
  Previously, from mount through the debounce delay and the in-flight call itself, the "not in
  catalog" badge and `getUnknownLabel` were already suppressed as "not yet known" -- but nothing
  filled the gap they left, so the raw value (a machine key, a numeric id) rendered unexplained in
  the meantime. That window now shows the same `loadingText` placeholder the listbox's own loading
  row uses. Once the fetch settles, success or failure, the value resolves normally: its real label
  if a row claims it, or the raw value with the badge if it still matches nothing.

## 15.0.1

### Patch Changes

- 314604b: Fix `<lr-button href>` rendering an underlined label for every non-link appearance. In anchor mode
  the root is a real `<a>`, which the user-agent stylesheet underlines, and the shared `[part="base"]`
  rule never reset `text-decoration` — so an `appearance="accent"` link button was underlined while the
  same button without `href` was not. The base now declares `text-decoration: none`, matching
  `<lr-icon-button>`. `appearance="link"` keeps its underline in both modes, and a consumer
  `::part(base)` `text-decoration` rule still wins.
- 21c63aa: Raise the optional `@aiden0z/pptx-renderer` peer range to `^1.3.0` (from `^1.2.4`), used by
  `<lr-pptx-viewer>`.

## 15.0.0

### Major Changes

- 4bce219: Library-wide consistency release: fixes across all 285 components, plus five deliberate public
  API changes.
  
  Most of this release extends earlier single-component fixes to every structurally identical
  sibling, so behaviour that was already correct on one element is now correct across the family.
  
  ### Breaking changes
  
  **`<lr-responsive-panel>`: `variant` is now `shape`.** The property and its reflected attribute are
  renamed, and the exported type `LyraResponsivePanelVariant` becomes `LyraResponsivePanelShape`. Its
  values (`'fullscreen' | 'bottom-sheet'`) are unchanged. `variant` in this library is the shared
  semantic-tone vocabulary (`neutral`/`brand`/`success`/`warning`/`danger`) that `<lr-button>`,
  `<lr-badge>` and others use; this control's `variant` described a presentation shape instead, so it
  was overloading a shared name with a different member set. Migration: rename the attribute and
  property, and the type import if you use it.
  
  **`<lr-chart>` (and every chart subclass): `legendMode` now defaults to `'auto'`.** Previously
  `'dataset'`. `'auto'` resolves to `'datum'` on pie, doughnut and polar-area charts and to
  `'dataset'` everywhere else. A single-dataset slice chart previously rendered one aggregate legend
  row swatched with only the first slice's colour, leaving the remaining slice colours unidentified;
  it now labels every slice. Pass `legend-mode="dataset"` to keep the old behaviour.
  
  **`<lr-chart>`: `annotations` and `hiddenDatasets` are now clone-owned frozen snapshots.**
  Assignment stores a bounded frozen copy, matching the contract `<lr-box-plot>` and
  `<lr-lite-chart>` already had for the same-named properties. The getter returns a different
  reference than the one assigned, and an in-place mutation now throws a `TypeError` instead of
  silently doing nothing. Migration: reassign a new array (`el.annotations = [...el.annotations, next]`)
  rather than mutating in place — code that was already working had to do this anyway, because Lit's
  reference-equality change detection never observed the in-place mutation.
  
  **`<lr-tool-param-form>`: a number/integer field's `control` part is now `<lr-number-input>`.** It
  was a raw `<input type="number">` whose native spin buttons were hidden with no replacement
  affordance. The documented contract (`.value`, `.errors`, `lr-input`, `lr-validity-change`,
  `lr-invalid`, and the `control`/`field` part names) is unchanged. Migration only affects CSS or DOM
  queries written specifically against the control being a native `<input>`.
  
  **`<lr-tool-result-dialog>`: `lr-maximize-change` now fires before the state changes, and is
  cancelable.** It previously fired after `el.maximized` already reflected the new value. Migration: a
  handler reading `el.maximized` synchronously should read `event.detail.maximized` instead.
  
  ### Accessibility
  
  - `<lr-icon-button>` now forwards `aria-pressed` and `aria-current` to its internal semantic control,
    matching the fix `<lr-button>` received. An icon-only toggle previously left the attribute inert on
    the host, so assistive technology announced no pressed or current state at all.
  - A host-authored `aria-describedby` is now merged into the internal control's own
    `aria-describedby` on the components that were dropping it, so an external description association
    reaches the element that owns the role.
  - Computed accessible names are no longer written back as an empty `aria-label`, which suppressed the
    native text fallback rather than deferring to it.
  - `<lr-heatmap>` announces a keyboard range selection after the selection is extended, so the added
    cell is actually confirmed, and no longer overrides an author's deliberate `aria-label=""`.
  - `<lr-card>`'s activation control gets a localized fallback name when its content has no text of its
    own, so an image-only or chart tile is never an unnamed button.
  
  ### Correctness
  
  - Property setters that clamped or validated against a sibling property no longer depend on
    assignment order. Lit assigns template bindings in source order, so `.value` bound before its
    domain properties could silently normalise to a different value; `<lr-data-grid>` and
    `<lr-flow-canvas>` could lose an initial selection entirely this way.
  - Components composing `<lr-virtual-list>` now pass stable collections, so an unrelated re-render no
    longer clears measured row heights and forces a full offset recompute — visible as a sticky-header
    height snap on `<lr-thread-list>`.
  - Generated chart dataset defaults now defer to authored `config` beyond bar charts, so an explicit
    `borderWidth`/`borderRadius`/`pointRadius` of `0` is honoured. At dense geometry a generated
    transparent stroke could consume a thin bar entirely and paint nothing.
  - `<lr-chart>`'s spoken value now honours `valueFormatter`, so a keyboard or screen-reader user hears
    the same formatted value sighted users see.
  - `getElementById` on a `renderRoot` typed `HTMLElement | ShadowRoot` is replaced with `querySelector`.
  
  ### New public surface
  
  `<lr-data-grid>` gains `selectionMode` (aligning its vocabulary with `<lr-table>`) and a cancelable
  `lr-sort-request`; `<lr-menu-item>` and `<lr-dropdown-item>` gain the full `href`/`target`/`rel`/
  `download` link surface with a non-removable `noopener noreferrer` guard whenever `target` is set;
  `<lr-box-plot>` gains the eighteen-token palette and border/radius hooks its sibling chart types
  already had; `<lr-message-parts>` gains `maxRenderedParts`; `<lr-subagent-panel>` and
  `<lr-flow-minimap>` gain the `compact`/`frame` density escape hatches their siblings had;
  `<lr-document-compare>` gains `max-height`; `<lr-alert>` gains close-button hover/active tokens;
  `<lr-export-button>` gains a `trigger-error` part so an export failure is visible and announced.
  
  ### Tooling
  
  - The manifest no longer drops a subclass's property type override, which was publishing the base
    class's type plus a false `inheritedFrom` — and flowing that wrong type into the generated React,
    Vue and Svelte declarations.
  - `<lr-histogram>`'s derived `labels`/`datasets` are published as read-only. They were absent from
    the manifest entirely, so assigning them type-checked against the inherited writable signature and
    silently did nothing.
  - The attribute-polarity migration gate now checks the real shipped surfaces. It previously iterated
    a hand-maintained rename list that contained no polarity-bearing pairs at all, so it could not
    reject an inverted rename — the quietest possible parity break, since the migrated markup still
    parses and the component behaves the other way round.
  - Optional-peer install coverage in the packed-consumer check goes from one peer to eleven, and every
    remaining peer now needs a written reason or the check fails.
  - `scripts/test.sh` keeps its lane logs when a run fails, instead of deleting the directory whose
    path it just printed.

## 14.3.2

### Patch Changes

- 0314633: Keep accessible names computed from slotted content independent of whether the component is
  currently displayed. A row inside a closed dropdown, a tab in a hidden group, or a chip, tag, toast
  or progress bar inside any `visibility: hidden` container previously computed an empty name, because
  the shared accessible-text walk applied visibility inherited from the container to the content it
  was naming.
  
  `lr-menu-item` and `lr-dropdown-item` additionally wrote that empty result back as an authoritative
  `aria-label=""`, which suppresses the row's own content-derived name and left it permanently
  unnamed — most visibly on a dropdown that is already `open` on its first render. An empty computed
  name now removes the attribute instead of emptying it, for both the row and its submenu's
  `role="menu"`, so the browser falls back to the visible label. `getTextLabel()` returns that label
  for a closed menu too, restoring type-ahead.
  
  `lr-tag` also never named its remove button from an element-wrapped label, because its first sample
  runs before layout exists; it now takes the same pre-layout reading `lr-chip` already did.
  
  Authored `aria-label`, `aria-labelledby` and explicitly empty consumer labels stay authoritative,
  and `display: none`, `aria-hidden`, `inert` and `hidden` branches are still excluded from every
  computed name.

## 14.3.1

### Patch Changes

- 422a035: Fix nested dropdown submenus that were visible but could not receive pointer clicks in WebKit. Both direct submenu items and nested menus now remain interactive outside the parent popup, with or without hoisting and in LTR/RTL. Long dropdowns scroll their inner menu list within the popup's height limit while keeping menu headers and footers visible.

  Refresh development tooling and optional-peer compatibility verification against current dependency releases.

## 14.3.0

### Minor Changes

- 3d58e7a: Add numeric feature-field radius scales to declarative map points, with threshold or linear interpolation, bounded stops and explicit fallbacks. Categories remain on one clustered source while cluster-count sizes stay independent.
  
  Add fill, stroke and combined modes to safe path-only map icons, including viewBox-scaled stroke widths and cap/join options. Preserve filled-path defaults, local bounded rasterization, theme updates, resource cleanup, clicks and canvas export.
  
  Keep expanded attribution clear of opposite map controls on narrow layouts, align navigation to its logical edge, and reserve legend space even when attribution is the only control. Preserve the gradient's visible width in shrink-to-fit legends and keep scale labels inside their background without changing the peer's distance-bar width.
- d7aa2c0: Add opt-in category legends and controlled slice visibility to pie, doughnut and polar-area charts. Legend text can show labels, values or percentages independently of tooltip and axis formatting. Category toggles preserve source indexes across sampling and multiple rings, with immutable, cancelable visibility events.
  
  Keep extreme finite pie and doughnut values from overflowing native arc geometry while retaining original values in Lyra readouts and exports.
  
  Forward button pressed and current-item ARIA states reactively to the internal native control, including state removal, href changes and disabled controls.
  
  Add opt-in persistent table sort indicators, restore the compact map attribution information glyph,
  and document a responsive details-header recipe for independent controls.

### Patch Changes

- 249d8a1: Keep sampled chart category legends and accessible tables responsive by sharing dataset projections and color probes within each render. Data and theme updates still receive fresh values, including percentage labels and repeated token-based category colors.
  
  Let map scale labels fit different fonts and text sizes while drawing the ruler at the native geographic width, including after zoom changes.

## 14.2.0

### Minor Changes

- f19e5d3: Add controlled heatmap multi-cell selection with drag paint/erase, keyboard ranges, and row/column toggles. Add declarative numeric line color scales, width, and opacity to map data layers. Style standard MapLibre navigation and scale controls inside the map shadow root, with localized button names, public parts, and space reserved around the legend.
  
  Add opt-in formatted slider readouts beside the label, complete cartesian axis visibility and compact chart plots. Align date-input action geometry with the shared control size ladder while retaining accessible pointer targets.
  
  Preserve fractional slider values across value-first Lit bindings and prevent compact checkbox label overlap while retaining accessible hit targets.
  
  Add typed category point colors, radius/stroke and safe SVG path icons to map data layers. Categories retain one shared clustered source; theme changes, style reloads, feature clicks and canvas exports preserve their appearance.
  
  Respect configured bar border defaults when generating datasets, preserving visible dense histograms and explicit per-series widths.
  
  Add a compact flag-only locale-picker trigger with an accessible current-language description, a public trigger-label part, and independent label/flag-label modes.
  
  Add configurable heatmap matrix gaps and rounded corners, with sparse visual column labels that preserve full tooltip and keyboard semantics.

## 14.1.1

### Patch Changes

- 9cb130c: Update development dependencies and restore dependency-upgrade compatibility with pnpm 12, including its separate environment and workspace lockfile documents.

## 14.1.0

### Minor Changes

- Add `promptStudioPreviewLimit` to the public localization keys, with an English fallback and translations in all ten shipped catalogs.

### Patch Changes

- Correct component behavior and keep the published reference aligned with the supported APIs.

  - agent-tools/commit-card: Removing the message attribute clears the displayed subject and body; the property retains the normal null readback of a removed string attribute.
  - agent-tools/compare-panel: Disabled vote buttons retain their resting colors during hover and press, including an existing selected vote. Stacked response panes keep short content readable at narrow widths while preserving the configured height limit for long responses.
  - agent-tools/prompt-studio: Bound prompt preview expansion depth, substitution work, resolved output and memoized text. Show a localized preview fallback when a limit is exceeded while preserving raw messages, variables, editing and save/run payloads.
  - agent-tools/result-card: Removing heading or a result field label hides that text while preserving the card actions and field value.
  - agent-tools/stack-trace: Removing the trace attribute clears parsed content and copies empty text; the trace property retains null until assigned again.
  - agent-tools/terminal: A search with no matches clears earlier rendered match markers. Removing the content attribute clears output and preserves the normal null property readback.
  - agent-tools/tool-call-chip: Removing category or summary hides that optional text; assigning either attribute again restores its content.
  - agent-tools/tool-param-form: Choosing Boolean Unset keeps an explicit undefined value when a schema default exists; deleting that key or replacing the value with an absent key restores default materialization. Without a schema default, Unset removes the key.
  - agent-tools/tool-result-dialog: Assigning accessibleLabel directly names the inner dialog when no host aria-label is present. A host aria-label retains its separate host ownership, and an empty direct value uses the tool title.
  - agent-tools/tool-select-dialog: Programmatic selectedToolIds and useDefaults replacements synchronize the live checkbox and switch state after user edits without emitting change events. Removing search-placeholder restores localized copy, while an explicitly empty placeholder stays empty.
  - agent-tools/trace-tree: Span views omit whitespace-only IDs and preserve every nonblank business ID exactly, including surrounding whitespace. The first valid admitted duplicate continues to win.
  - charts/chart: Correct horizontal tooltip values, preserve cyclic segment palettes when sampling, and defer chart canvas construction until the first visibility decision. Keep grouped SVG bars visible and map logarithmic stacks by their positive totals while preserving authored minimum-height floors. Fit long axis titles to their allocation while retaining full accessible names and refreshing after font changes. Use positive logarithmic grid ticks to prevent overlapping labels without changing data geometry. Preserve populated native SVG tooltips through server rendering and hydration.
  - conversation/chat-message: Make the chat message Copy example copy its actual source through the shared copy control and describe fulfilled clipboard success accurately.
  - conversation/code-block: Keep code and Markdown rendering safe after source attribute removal, refresh highlighted gutter labels and digits with localization changes, reuse highlighters for equivalent immutable grammar maps, and preserve the parser options contract in examples.
  - conversation/conversation-item: Inline rename keeps IME composition keys in the editor; ordinary Enter commits and Escape cancels after composition.
  - conversation/generation-metrics: Start the live metrics example on mount and make its first Stop and repeated Restarts retire their producer timers correctly.
  - conversation/message-actions: ArrowLeft/ArrowRight/Home/End from a slotted feedback comment editor remain native editing keys. Roving navigation still operates on the actual toolbar and thumb actions.
  - conversation/message-feedback: Asynchronous finalization or reversion preserves focus on an outside control. Settlement retains the existing thumb/submit fallback when focus remains within the feedback or was lost as its pending controls became disabled.
  - conversation/message-parts: Clarify that streaming text and reasoning display accumulated plain text until Markdown parsing and highlighting resume on completion.
  - conversation/model-select: Removing label, hint, or error-text clears that optional copy in both catalog and free-text modes. Removed string properties retain null readback, and later assignments restore their content.
  - conversation/push-to-talk: Use the supported microphone icon slot in push-to-talk customization examples.
  - conversation/thread-list: A conversation row with slot="" belongs to the default slot exactly like a row with no slot attribute, including after reconnect. Named slots retain their distinct roles.
  - conversation/transcript-feed: Activating the focused jump action resumes follow and transfers focus to the scroll base when the action disappears, unless a newer outside focus move takes precedence.
  - conversation/typing-indicator: Removing label restores the localized thinking name while preserving an explicit host aria-label. Later label assignments remain reactive.
  - conversation/usage-badge: Removing cost-text or summary safely omits that content; explicit empty values remain empty and later values restore it. An open tooltip participates in shared Escape ordering even while only hovered, and dismissal preserves focus elsewhere. Closed badges retain their first hydration update timing.
  - conversation/voice-picker: Removing label, hint, or error-text clears that optional copy in both catalog and free-text modes. Removed string properties retain null readback, and later assignments restore their content.
  - conversation/widget-renderer: Exercise malformed streamed documents at the renderer boundary and keep default authoring diagnostics silent outside development.
  - data/condition-builder: Contain field/operator select value and listbox lifecycle aliases so one selection emits one complete condition model.
  - data/context-meter: Preserve populated ring segment tooltips through server rendering and hydration.
  - data/data-grid: Keep repeated data-grid rows canonical in processed output, reserve fixed-column widths during size-to-fit, and preserve click ownership for controls inside open shadow roots.
  - data/document-library: Retain documents with undefined optional tags and safely clear search filtering when the search-term attribute is removed.
  - data/file-tree: Keep valid files when an earlier same-path record has unreadable optional metadata, preserving first-valid path ownership and neighboring files.
  - data/flow-canvas: Keep flow cards readable in horizontal RTL layouts, preserve valid handles after rejected duplicate records, and omit unreadable run decorations without rejecting valid neighboring status records.
  - data/funnel: Fill main and comparison tracks when a positive ratio between finite stage values overflows the numeric range.
  - data/gauge: Safely remove gauge labels in every shape while preserving localized and authored accessible names. Preserve long native SVG tooltip text through server rendering and hydration.
  - data/graph-query-builder: Emit one complete graph query per hop selection without leaking child select aliases. Keep chip removal and saved-query updates working when focus observation is unavailable.
  - data/heatmap: Preserve heatmap no-data values when domain settings change and neighboring cell pixels during keyboard focus repaint. Limit invalid authored color diagnostics to development without changing color fallbacks.
  - data/sparkline: Use fallback values safely after the sparkline data attribute is removed.
  - data/stat: Safely remove stat caption and sub-line attributes, and preserve modified-link activation from passive slotted content while keeping nested controls independent.
  - data/table: Keep table activation, focus, and editing aligned with locale-sorted pages, safely handle removed filter text, and hide priority footer cells with their columns.
  - data/timeline: Allocate horizontal time timeline height from actual item content and stack lanes, updating it when content grows or shrinks. Defer browser measurements during server rendering.
  - data/tree: Bound object-tree normalization to 10,000 lazily inspected array positions while preserving valid depth-first order. Give otherwise unnamed data rows a stable ID-based accessible name without changing their visible content or authored names.
  - data/word-cloud: Keep word-cloud geometry finite and retain eligible words across extreme signed finite domains.
  - forms/button: Keep button and icon-button external descriptions current through source replacement, removal, reconnection, document adoption, and native button/link changes.
  - forms/code-editor: Respect wrapping allocation, monospace font tokens and explicit tab width across native text, measurement and line numbers. Keep the gutter compact and tall enough for the source, preserve the editor frame as the scroll surface for ordinary resize modes, and reveal the caret clear of scrollbars and line numbers in either text direction. Retain requested selections through native focus. External descriptions now reach the native textarea, form reset restores pristine interaction feedback, and removing label, hint, or error text renders safely.
  - forms/color-picker: Discard disabled color-picker drafts without committing, block disabled palette and format actions immediately, and keep external trigger descriptions synchronized with their source elements. Render enabled and disabled inline controls safely on the server.
  - forms/combobox: Synchronize mounted option selection with select and combobox values and form submission while preserving reset defaults. Keep single combobox selection semantics consistent, ignore composition keys, honor inert options, refresh changed adornments, and forward external descriptions. Safely render removed form guidance attributes and clarify silent filter range edits.
  - forms/date-picker: Fix calendar range admission, preset identity, bounded navigation and focus recovery. Preserve explicit action labels and external date-input guidance, support early calendar years and all-day ICS dates, and keep selection and agenda actions readable with long preset labels fitting narrow layouts.
  - forms/emoji-picker: Keep emoji picker descriptions on the value listbox, preserve localized built-in headings through filtering and live string changes, refresh reused item content on groups reassignment, and restore pristine interaction feedback on form reset. Composing search keys no longer pick or navigate emoji, and removing label, hint, or error text renders safely.
  - forms/input: Use current pending native step constraints in input, number input, and native time input. Safely render removed form guidance attributes in those fields and the segmented time input.
  - forms/locale-picker: Resolve external descriptions onto the locale picker trigger and safely render removed label, hint, and error text attributes.
  - forms/otp-input: Resolve external descriptions onto the native OTP input while retaining local guidance and tracking referenced elements across replacement and document changes.
  - forms/phone-input: Honor explicit country and validation copy over locale strings, preserve phone copy defaults on attribute removal, safely remove default-country, and resolve external descriptions onto the telephone input.
  - forms/radio: Preserve explicit radio checked writes, resolve external radio and switch descriptions on their semantic controls, and safely remove checkbox and switch guidance attributes.
  - forms/rubric-form: Synchronize rubric category checkboxes after controlled value replacements, safely remove aggregate label and hint attributes, and keep external descriptions on the aggregate group current.
  - forms/slider: Keep slider descriptions and removable form copy synchronized, ignore secondary mouse buttons, cancel disabled or readonly gestures, and commit an enabled keyboard edit once when focus leaves its handle.
  - forms/swatch-picker: Honor disabled changes immediately when activating or focusing a swatch picker.
  - forms/textarea: Safely remove textarea label, hint, help-text, and error-text attributes while preserving their absence readback.
  - forms/time-range: Ignore right and middle mouse presses when seeking or dragging a time range while preserving primary mouse, touch, and pen interaction.
  - forms/token-input: Preserve IME editing in token inputs, synchronize external descriptions and removable form copy, vertically center editable token labels, and correct the story to use native focus and blur events.
  - layout/app-rail: Preserve listener state when an app rail resize request disables resizing, changes mode, or disconnects the rail, without publishing an accepted resize.
  - layout/carousel: Keep Arrow, Home, and End keys with focused editors and controls inside carousel slides while retaining viewport keyboard navigation.
  - layout/command-palette: Keep valid commands available when optional keywords are malformed, ignoring invalid keyword entries without reading accessors.
  - layout/dashboard-grid: Snap dashboard pointer move and resize proposals to painted tracks and gutters, including public CSS geometry overrides in LTR and RTL.
  - layout/details: Clarify that Details toggle events report accepted changes and that accordion grouping applies to direct accordion-item children.
  - layout/filter-bar: Ignore malformed filter options and custom definitions without a callable renderer while retaining valid sibling controls.
  - layout/menu: Keep focus on outside, header, and footer controls when menu items change, and clarify direct item enrollment.
  - layout/multi-split: Keep feasible panel pixel minimums intact after divider gutters, including live font-relative gutters, without changing percentage state or overflowing a smaller allocation. Preserve constrained bounds during responsive collapse transitions without resize-observer errors, including constraints assigned from a collapse-change handler.
  - layout/reorder-list: Refresh reorder movement controls when item identities change and safely render removed standalone values.
  - layout/split-panel: Report proposed split-panel percentages from the selected primary edge, matching pixel details and accepted values.
  - layout/tab-group: Restore focus when a manually focused unselected tab is removed while retaining the selected panel.
  - layout/widget: Safely omit widget title and secondary copy when their attributes are removed.
  - media/animation: An initial playing mount emits one start lifecycle; target changes, timing changes, reconnect and replay retain their own starts.
  - media/attachment-chip: An error already present on reconnect renders silently as history; a new connected upload failure still announces once.
  - media/av-player: Removing mime-type restores automatic video fallback. Open-ended transcript cues use the next strictly chronological start with linear reconciliation per seek, and unavailable timeline controls retain resting paint under hover and press.
  - media/avatar: Removing label safely restores the unnamed fallback while preserving host accessible naming and later label updates.
  - media/file-input: Removing label or hint safely removes its text chrome and association. Disabled file removal controls retain resting paint under hover and press, including fieldset disablement.
  - media/image-viewer: Keep image highlight activation separate from annotation commands, show annotation focus, and retain disabled toolbar paint. Honor direct pan/zoom and iframe labels and distinguish iframe focus from zoom-control focus.
  - media/map: Align map markers and popups to geographic coordinates in RTL and refresh choropleth stop and step-base colors when ancestor themes change.
  - media/media-card: Removing mime-type restores the generic file fallback. A property-only accessibleLabel reactively names the action while host labels retain separate ownership.
  - media/sequence-playback: The play button retains its disabled resting paint when too few items are available; enabled pointer feedback remains visible.
  - media/video: An unavailable progress control and its visible timeline track retain resting paint on hover and press; enabled controls retain feedback. Keep the poster play icon visible against its surface in light and dark themes while preserving custom foreground and background overrides.
  - overlays/chip: Collapsed groups reapply max-visible when assigned children are replaced or reordered at the same count, preserving authored hidden/inert state and releasing departed visibility leases.
  - overlays/dialog: Removing label safely omits its fallback title. Open dialog and drawer names follow supported name and exclusion attribute changes on direct unslotted headings, while host naming retains precedence; nested and slot-empty headings remain outside automatic discovery.
  - overlays/empty: Removing heading or description safely omits that text; explicit empty values remain empty and later values restore the corresponding content.
  - overlays/kbd: Removing keys safely clears the shortcut. Unknown tokens, including constructor and __proto__, render and name themselves verbatim; recognized modifiers keep their localized labels.
  - overlays/overlay: Open popovers, dropdowns and tooltips reposition when their effective host or inherited text direction changes, preserving open state and lifecycle events. Removing content safely omits tooltip fallback text; preserve null readback and later recovery.
  - overlays/rating: Readonly transitions synchronize validity and aria-invalid in the same completed update. Form reset restores the independent default-value rather than the live value attribute.
  - retrieval/embedding-explorer: Preserve populated point tooltips through server rendering and hydration.
  - retrieval/entity-card: Update entity-card heading levels immediately when the existing aria-level override changes or is removed.
  - retrieval/entity-chip: Keep entity-chip accessible names renderable when text or type attributes are removed.
  - retrieval/graph: Fix graph keyboard focus across hidden links, live canvas zoom bounds, and canvas node/link hover events. Zero-width canvas links now omit both strokes and arrowheads while retaining graph topology.
  - retrieval/grounding-summary: Correct grounding-summary documentation to describe citation activation containment and the richer selection event.
  - retrieval/ingestion-queue: Render and announce malformed ingestion failure details with the localized failure label instead of rejecting updates.
  - retrieval/knowledge-graph-explorer: Route path-node activation into explorer selection and details, tolerate removed search queries, and correct the preset-search example.
  - retrieval/node-palette: Make the node-palette canvas example commit placements to the public canvas model so new nodes render.
  - retrieval/provenance-panel: Ignore malformed provenance type rows so valid entities and later matching type labels still render.
  - retrieval/source-picker: Restore a visible source tree entry when re-enabling search with a retained filter.
  - utility/copy-button: Keep the CopyFailure example effective after held pointer presses and restore the original inherited or own clipboard after each action.
  - utility/format: Keep currency formatting usable after removing the currency attribute, using the existing USD fallback.
  - utility/json-viewer: Clear removed JSON search safely and begin backward navigation at the last match while preserving manual branch collapse.
  - utility/known-date: Restore localized known-date labels after attribute removal, resolve external guidance on the date group, and preserve disabled field paint.
  - utility/live-region: Make both Basic live-region example actions announce reliably on first and repeated clicks.
  - utility/mention-popover: Preserve input-method composition when forwarding mention keys and safely clear a removed query attribute.
  - utility/poll-status: Wrap long localized polling labels within narrow layouts while preserving the pause and resume target size.
  - viewers/dataset-viewer: Keep populated dataset headers sticky to the page in page mode while preserving horizontal page overflow and default contained scrolling.
  - viewers/document-preview: Handle removed MIME attributes safely, recognize parameterized JSON previews, and keep capped image content reachable from its scroll origin.
  - viewers/ebook-viewer: Restore text-selection events for genuine native selections inside EPUB chapter iframes while retaining bounded text/rectangles, CFI anchors, stale-rendition guards, and containment of arbitrary peer getters.
  - viewers/email-viewer: Reveal matching folded plain-text and HTML email quotes for padded, Unicode-normalized, and whitespace-normalized search queries while retaining shared search limits.
  - viewers/include: Apply passive markup restrictions to nested template contents before remote include caching and fragment extraction.
  - viewers/notebook-viewer: Fix notebook anchors to scroll identified virtual cells into the visible viewport, retain active-cell paint, and support repeated jumps to the same cell.
  - viewers/svg-viewer: Keep oversized SVG content inside the reachable scroll range of capped viewers while retaining centered fitting content.
  - viewers/xml-viewer: Reopen the selected XML search match and its ancestors before scrolling, including manually collapsed branches, while preserving later manual collapse.

  Shared behavior fixes preserve fieldset disablement, composing keyboard input, passive embedded markup, XML renderer dispatch and development-only diagnostics. Correct migration guidance and complete public type descriptions.

  Restore factory-created document renderer registries and retain the supported cue IDs in the audio/video payload example.

  Preserve trusted document and audio/video renderer adapters through registry snapshots while retaining callback validation and collection limits.

## 14.0.0

### Major Changes

- 8985e9a: A library-wide correctness and accessibility pass across all component families. Message-feedback
  settlement gains a stable transaction id (one narrow, opt-in breaking change for consumers who
  relied on repeated no-argument settlement), `lr-details`'s internal DOM structure changes so
  header actions no longer nest inside the native `<summary>` (all documented parts/slots are
  preserved, just relocated), and dozens of components gain new events, CSS custom properties,
  exported types, and hardening around malformed input, prototype getters, and browser-capability
  failures. No default values changed and no public part, slot, or event name was removed.

  ### Breaking changes

  **1. Feedback settlement now requires a `submissionId` for anything beyond the first transaction.**
  `lr-feedback-submit`'s frozen detail gained `submissionId: string`. `lr-message-feedback`'s
  `finalizePendingSubmit()`/`revertPendingSubmit()` changed from `(): void` to
  `(submissionId?: string): boolean`. The no-argument form still works, but **only** while the
  component is on its first-ever submission and that submission was never invalidated or
  superseded (by an external `rating`/`detail`/`detailFor` change, disconnect, adoption, or
  ownership change); after a second submission starts or any pending one is invalidated, the
  no-argument form fails closed and returns `false` forever after.

  Migration: read `event.detail.submissionId` in your `lr-feedback-submit` listener, pass it to the
  settle call explicitly, and check the boolean result instead of assuming success:
  `panel.finalizePendingSubmit(event.detail.submissionId)`. `lr-message-actions` (which embeds a
  thumbs-only `lr-message-feedback`) also gained its own id-scoped, read-only `feedbackPending`
  getter plus `finalizePendingSubmit(submissionId: string): boolean` /
  `revertPendingSubmit(submissionId: string): boolean` for the request it currently owns.

  **2. `lr-details`'s internal structure changed — `base`/`details`/`header`/`content` moved to
  different elements.** No part or slot was removed; only their owning node changed:

  | Part | Before | After |
  | --- | --- | --- |
  | `base`, `details` | both on the native `<details>` | both on a new outer wrapper; native `<details>` is now private (no exposed part) |
  | `header` | the flex wrapper *inside* `<summary>` | the complete summary + actions row |
  | `header-actions` | rendered inside the `<summary>`/`<details>` content box | a following sibling of the now-private native `<details>`; stays enabled and non-toggling even while `disabled` |
  | `content` | inside the native `<details>` | outside it, behind a private `hidden="until-found"` gate (adds in-page-find support) |

  This was necessary because interactive `header-actions` content (e.g. a trailing "add" button)
  cannot legally nest inside a native `<summary>`. Plain color/spacing/border rules against
  `::part(base|details|header|summary|header-actions|icon|content)` keep working unchanged. Update a
  rule only if it depends on the old DOM relationship: anything selecting through `::part(details)`
  as if it were the real `<details>` (e.g. `:open`, or assuming it parents `<summary>`) should
  target the host's `[open]`/`aria-expanded` instead; anything assuming `::part(header-actions)` is
  a descendant of `::part(details)` should treat it as a sibling within `part="header"` instead;
  never set `hidden` on `::part(content)` yourself — the component now manages it.

  ### Changes

  #### Agent tools
  - `lr-approval-close.detail.reason` can now be `'request-invalidated'` when a selected pending
    request disappears or resolves elsewhere.
  - `lr-thinking-panel` gained `--lr-thinking-panel-compact-header-font-size`.
  - `lr-terminal` gained `--lr-terminal-surface-color`, `--lr-terminal-toolbar-button-hover-bg`,
    `--lr-terminal-toolbar-button-active-bg`, `--lr-terminal-line-hover-bg`, and
    `--lr-terminal-line-active-bg`.
  - `lr-activity-feed` now lets a host `aria-labelledby`/`aria-describedby` reach the owned entry
    list in both plain and virtualized rendering; `renderText` now replaces text inside the
    persistent `entry-text` part rather than removing that part.
  - `lr-tool-select-dialog` and `lr-json-schema-viewer` now cap traversal at 10,000 supplied
    positions and safely skip malformed/accessor-backed rows. `lr-tool-param-form` now recurses only
    into safe own-data fields, omitting unsafe branches while valid siblings still render/submit.

  #### Charts
  - `formatter`'s context object documents its full `statistic` vocabulary: `x`, `y`, `r`, `min`,
    `q1`, `median`, `q3`, `max`, `total`.
  - `dataTableToggle` now keeps a supplied `slot="data-table"` synchronized with the built-in
    table's open/closed state, and a reader's own toggle choice stays authoritative afterward.
  - `lr-chart`, all eight chart variants, and `lr-box-plot` gained
    `--lr-chart-canvas-hover-outline-color` (default `var(--lr-chart-grid-color)`).
  - `preloadCharts()` gained an `annotations` option/result field.
  - Charts that set no annotations, and `lr-box-plot`, no longer break when a sibling chart on the
    same page loads `chartjs-plugin-annotation`: every live chart built before that global
    registration now rebuilds its Chart.js instance instead of throwing inside the plugin on its
    next update.
  - `lr-lite-chart`'s `selectedIndices` is now documented and enforced as **source category
    indices**, not positions in the (possibly sampled) rendered output.

  #### Conversation
  - `lr-markdown`: GFM task-list checkboxes get an accessible name from their primary inline text;
    `lr-highlight-activate` now fires before `lr-link-click` when an intercepted link overlaps a
    painted highlight.
  - `lr-model-select` and `lr-voice-picker` gained a reflected `readonly: boolean = false` property
    (blocks typing/catalog commits; preserves focus, popup navigation, selection/copy,
    submission/reset, and programmatic writes).
  - `lr-audio-visualizer`: `barCount` normalizes to an integer in `[1, 64]`; `gain` treats a
    non-finite value as `1`; values clamp to `[0, 1]`/`[-1, 1]` as appropriate. The host now supplies
    `role="img"` only when the author hasn't set one, and rendering now pauses off-screen and
    resumes on re-entry via `IntersectionObserver` where available.
  - New exported type `MessagePartsContentMode = 'plain' | 'markdown'`; an unsupported
    `content-mode` value now normalizes/reflects to `'markdown'`.
  - `lr-chat-composer`, `lr-prompt-input`, and `lr-agent-workspace` now normalize an invalid
    status value to `'idle'` instead of leaving it unrecognized.
  - Toolbar actions (e.g. on `lr-message-actions`) may now expose `releaseTabIndex()` so a parent
    can stop managing tab index and restore an untouched author-supplied `tabindex`.
  - `lr-thread-list` gained eight custom properties theming group-toggle and row-action
    hover/pressed states independently.
  - `lr-prompt-input`/`lr-prompt-queue`: an open mention/command popup now reliably closes once
    focus leaves the textarea or popup; Enter/Tab-accept is now handled before textarea submission.
  - `lr-widget-renderer`'s `syncActiveDescendant()` now returns `false` for a `<textarea>` anchor
    without touching `aria-activedescendant` (focus moves via `focusActiveOption()` instead); a
    single-line `<input>` anchor is unaffected.
  - Several "assign to update" collection properties (`lr-widget-renderer` registries,
    `lr-thread-list.threads`, `lr-prompt-queue.items`, `lr-document-compare` versions,
    `lr-document-preview.highlights`, `lr-map`'s `markers`/`dataLayers`/`choropleth`) now
    consistently document that data is snapshotted on assignment — mutating an already-assigned
    array/object in place is not observed; assign a new value to update the view.

  #### Data
  - `lr-data-grid` gained six custom properties for independent hover/pressed theming of
    controls, the page-size selector, rows, and sortable headers.
  - `lr-calendar`'s `events[].date` now accepts an ISO string, finite epoch milliseconds, or a
    `Date`; invalid rows are omitted while `lr-event-select`'s detail keeps the original object.
  - `lr-calendar` gained six custom properties for nav/day/agenda-event hover/pressed states.

  #### Forms
  - `lr-date-picker` gained `--lr-date-picker-preset-selected-border` and
    `--lr-date-picker-preset-selected-color`.
  - `lr-input`, `lr-time-input`, and `lr-checkbox-group` now project a host `aria-describedby` onto
    the native control ahead of built-in hint/error/required-description ids.

  #### Layout
  - `lr-multi-split` gained a new cancelable `lr-toggle` event
    (`detail: LyraMultiSplitToggleDetail = { open: boolean }`) firing before Escape/backdrop closes
    the floating panel; `preventDefault()` or a synchronous reentrant `open` write aborts the close.
    A forced close from a responsive collapse transition fires it noncancelably afterward. Direct
    `open` writes and no-op dismissals stay silent.
  - `lr-details`'s structure changed — see Breaking changes above.

  #### Media
  - `lr-map`: a malformed earlier `markers`/`dataLayers` row no longer reserves its `id`, so a later
    valid row with the same id is now admitted.
  - `lr-qr-code` now defers painting until it has a valid intersecting `IntersectionObserver` entry
    where available, pausing off-screen and resuming on re-entry; unresolvable paint colors now fall
    back to documented safe values.

  #### Overlays
  - `lr-rating`: for its managed slider name only, an empty/whitespace `label` now counts as absent
    and falls through to the localized default; the raw `label` property still reads back exactly as
    assigned, and an authored `aria-label` still wins.

  #### Retrieval
  - `lr-knowledge-graph-explorer` gained a new cancelable `lr-before-visibility-change` event
    (`detail: { hiddenTypes }`) firing before a node-type visibility toggle changes state or
    announces it; the existing `lr-visibility-change` still fires after an accepted change.
  - `lr-knowledge-base` now forwards 13 row-related parts from its internal table via `exportparts`
    (e.g. `lr-knowledge-base::part(actions-trigger)` now works directly). An explicitly empty
    `label` still keeps the visible heading empty, but the nested table now takes the localized
    default as its accessible name instead of rendering an unnamed grid.
  - Palette-style catalog inputs cap traversal at 10,000 positions and skip accessor-backed rows; an
    omitted/empty/whitespace `label` now falls back to the localized default without changing the
    raw property readback.

  #### Utility
  - Toolbar/menu actions (including `lr-copy-button`'s `getToolbarActions()`) gained an optional
    `releaseTabIndex()` method mirroring the conversation-family addition above.
  - `lr-intersection-observer` and `lr-mutation-observer` now fail closed instead of throwing when
    the underlying browser API is unavailable or throws, retry construction once with safe defaults,
    and cap `threshold`/`attributeFilter` collections at 10,000 entries.
  - `lr-json-tree` now snapshots data from own enumerable data descriptors on assignment — it never
    invokes getters or conversion hooks, and later mutation of the original object no longer affects
    the displayed/copied value.
  - `lr-diff-view`'s `languages` map is now read from own enumerable data fields only.
  - `lr-format-date`, `lr-relative-time`, and related components now reject non-primitive,
    non-`Date` input without invoking its conversion hooks.
  - `lr-export-button`'s multi-format menu now reads descriptor fields from direct data only and
    behaves as a proper nonmodal overlay (only the topmost open menu handles
    Escape/outside-pointer/Tab; rebinds correctly after document adoption).

  #### Viewers
  - `lr-document-preview` and `lr-document-viewer` gained download-link hover/active background
    custom properties (`--lr-document-preview-download-link-hover-bg`/`-active-bg` and the
    `-viewer-` equivalents).
  - `lr-email-viewer` now copies accepted attachment bytes before use and creates a fresh `Blob` on
    every `lr-attachment-open`, so mutating the original source afterward can no longer change a
    previously emitted attachment's bytes.
  - `lr-ebook-viewer` now verifies the loaded `epubjs` peer exposes its required capabilities before
    use, caps table-of-contents projection (10,000 positions/nodes, depth 100), and its
    `matchCountExact: false` now also covers malformed/holey spine data.
  - `lr-document-compare`'s `oldVersion`/`newVersion` are now captured as a frozen snapshot per
    assignment; a non-string runtime `language` value is now treated as `''`.
  - `lr-docx-viewer` (Mammoth-based) now validates the conversion result shape before using it and
    caps diagnostic messages at the first 100.

  #### Shared
  - New registration-free helper modules, usable without registering their owning component: stack
    trace parsing, span projection, agent-status presentation, approval-state helpers, and the
    default widget-type registry (see `llms/shared.md` for exact import paths).
  - Every component can now be adopted into another document (for example an `<iframe>`) and keep
    rendering there, including children it creates after adoption: a shadow root that lives in
    another document receives inline `<style>` elements instead of the shared constructed
    stylesheets that Chromium refuses to adopt across documents. Same-document rendering is
    unchanged.
  - `attachInternalsSafely()` now inspects data descriptors without invoking accessors, returning
    safe fallback internals for a missing, accessor-backed, non-callable, or throwing
    `attachInternals` implementation.

## 13.0.1

### Patch Changes

- 9629013: Gate the accuracy of the published API reference's property defaults. The package ships its own
  `llms/` reference, and nothing compared a documented `name: type = default` (or `name?: type`)
  against the declaration recorded in `custom-elements.json` — so a property could change between
  optional and defaulted while the shipped reference kept describing the old shape. That misleads
  concretely: `label: string = ''` tells you an unset read is `''`, so `el.label.trim()` is safe and
  `?? fallback` is dead code, when the real readback is `undefined`. The 13.0.0 corrections fixed
  fifteen such entries; this adds the check that keeps them fixed, in both directions. No runtime
  change.

## 13.0.0

### Major Changes

- 70ddaac: Harden accessibility, overlay stacking, form semantics, localization, remote-content boundaries,
  and rendering across the component library. Add opt-in light dismissal to the tool dialogs and a
  named accessible-label slot to the progress ring, and improve generated API and editor metadata.

### Patch Changes

- 77d0c25: Correct published API reference entries where a documented IDL default contradicted the shipped
  declaration. `accessibleLabel` on `lr-breadcrumb`, `lr-carousel`, `lr-command-palette`, `lr-table`
  and `lr-timeline`, `label` on `lr-breadcrumb`, `lr-pagination`, `lr-file-tree`, `lr-artifact-panel`,
  `lr-subagent-panel` and `lr-pptx-viewer`, and `lr-attachment-chip`'s five label overrides are all
  optional: an unset property reads back `undefined` and falls through to a localized default, rather
  than the empty string or the literal English text the reference claimed. `lr-details` now documents
  `header-actions` as rendering inside the `<summary>` header row — the shape that actually ships and
  keeps the controls hit-testable while collapsed — and `lr-eval-dataset`'s `lr-example-add-request`
  is documented with the `detail: null` that `emit()` produces.
- Updated dependencies [70ddaac]
  - @aceshooting/lyra-flags@2.3.0

## 12.1.2

### Patch Changes

- a58f119: Fix accessibility naming, interaction guards, data normalization, remote media safety, and generated contract coverage across the component library.

## 12.1.1

### Patch Changes

- 50754fa: Keep clone-owned collection properties referentially stable when a declarative renderer rebinds
  the same unchanged input, avoiding redundant work such as resetting an unchanged map style while
  preserving explicit change detectors and updates made by assigning a new collection.
- 92cdb49: Correct the first-release annotations for the 12.1 date preset, filter-bar, and map-layer APIs in
  the packaged LLM reference.

## 12.1.0

### Minor Changes

- 6504adc: Expand `lr-map` with zoom-responsive heatmap radius and intensity, heatmap opacity,
  construction-time world-copy control, allocation-aware resizing, and accessible declarative marker
  activation events.
- 81dbb6a: Add opt-in automatic value-axis gutter sizing and category-label density to `lr-lite-chart`.
- acf4c5f: Avoid redundant `lr-heatmap` redraws for unchanged legend snapshots, and keep stable-ID `lr-map`
  GeoJSON additions, removals, and order changes on MapLibre's incremental update path.
- 5e801f9: Add an opt-in `scroll-mode="auto"` to `lr-table` that keeps page flow while content fits and contains horizontal overflow when the table narrows.
- 12b4791: Add opt-in collision clustering to time-scaled timelines, with allocation-aware accessible count markers and a typed member activation event.

### Patch Changes

- b36d915: Defer anchored-positioning and form-label runtime paths until they are needed, keeping granular
  popover and combobox registrations lighter and documenting first-interaction progressive loading
  for strict initial-JavaScript budgets.

## 12.0.0

### Major Changes

- bd0f05f: **Breaking:** `LyraElement` no longer declares the static `getPropertyDescriptor()`, so that member
  is gone from every element's public surface in `custom-elements.json`.

  This is the release's only substantive breaking change, and in practice nothing consumer-callable
  was removed: `getPropertyDescriptor()` is Lit's own finalization hook, called *by* `ReactiveElement`
  during `finalize()` and never by application code. It appeared on all 285 tags purely because this
  library overrode it, and the manifest projects an inherited static onto every subclass. Only code
  that subclassed an `lr-*` element and overrode the hook itself is affected — a path Lit has already
  deprecated and states will not be called under standard decorators.

  The major is nonetheless correct rather than pedantic. By this package's own definition of public
  surface, a public static was removed from every element, and the reachable-declaration set of every
  export shrank as a result. The semver gate reports that honestly, and the alternative — shipping it
  as a minor behind a blanket exception — would have meant weakening the gate to let one change past
  it.

  `@lit/reactive-element` 2.1.2 deprecates that hook and warns during `finalize()`, so every consumer
  saw an unactionable dev-mode warning on every page load that mounted any `lr-*` element, not
  silenceable without disabling Lit's dev warnings wholesale.

  The more important half was invisible. That override was what implemented the documented
  clone-owned/bounded/frozen collection contract on 182 enrolled property names across 87 modules
  (`colorSteps`, `legendStops`, `annotations` and their equivalents): it wrapped every reactive setter
  and routed owned values through the snapshot helpers. It worked only because the published dist
  ships experimental decorators, which still call the hook. Lit states plainly that standard
  decorators will not — so a migration, or a consumer build applying them, would have silently
  reverted every one of those properties to storing the caller's live array by reference. No clone, no
  freeze, no error, no warning, and no test would have caught it.

  The contract now rides a decorator-agnostic seam that re-defines the already-finished prototype
  accessor. Legacy `@property`, standard `accessor`/setter decorators, a `static properties` block and
  hand-written getter/setter pairs all end in a prototype accessor by finalization, so this walks the
  finished accessor rather than the hook Lit refuses to call. It installs from the finalization
  trigger Lit itself documents, and registration strictly precedes every instance — constructing an
  unregistered custom element throws — so no assignment can reach an unwrapped setter.

  `finalize()` was deliberately not hooked: that would add a static method to the class surface, which
  the component inventory records per component and a pinned-manifest gate grades. The chosen seam
  changes no static surface at all.

  **No migration is expected.** If you do not override `getPropertyDescriptor()` on an `lr-*`
  subclass, there is nothing to do.

### Minor Changes

- 103922d: `<lr-chip-group>` gained `accessibleLabel` (attribute `aria-label`) and now renders
  `role="group"` on `[part='base']` whenever a name is supplied.

  A chip group is a group, and every peer grouping primitive in this library already said so —
  `<lr-radio-group>` renders `role="radiogroup"`, `<lr-segmented>` the same, each forwarding a host
  `aria-label` inward to the element that owns the role. `<lr-chip-group>` rendered a roleless
  container and read no accessible name at all. Because a host `aria-label` does not cross a shadow
  boundary, a consumer labelling the host named nothing: the chips were announced as unrelated
  toggle buttons with no indication of what set they belonged to.

  This surfaced from a real multi-select filter row, where the consumer had to hand-write
  `role="group" aria-label="…"` onto the host to get a named group. That workaround is the evidence
  the capability was wanted and was reachable only by reaching around the component.

  The role is applied only *with* a name, deliberately. An unnamed group role adds verbosity without
  adding information, and applying it unconditionally would change the accessibility tree of every
  decorative chip row already shipped. An explicit unset-regression test pins that.
- bd0f05f: Completed the date-preset story across the three components that share it.

  `<lr-date-input>` now mirrors the nested picker's `appliedPreset` as a read-only getter. 11.0.0 added
  `presets` to `<lr-date-picker>`; 11.1.0 then separately forwarded `presets` to `<lr-date-input>` and
  added `appliedPreset` to the picker — but never joined the two halves, so the readback landed on the
  component that does not need it and the component that does could set presets and not read the
  result. `appliedPreset`'s own documentation describes the dashboard time filter ("'Last 7 days' must
  still mean the last 7 days after tomorrow's reload"), and that shape is the compact
  text-field-plus-popover input, not the inline calendar.

  There was no workaround. The nested picker lives in the input's shadow root with no documented
  readback path: `input`/`change` are deliberately native events and carry no detail, and every
  alternative the docs already reject applied — matching `value` back against the preset list is "the
  mapping table `presets` exists to delete" and is ambiguous (Today and This month coincide on the
  1st), while reaching for `[part='preset-button'][data-active]` depends on private structure and on
  the popover having been opened at least once.

  The mirror is the input's own field rather than a shadow-root lookup, so it is correct (`undefined`)
  when the popover has never been opened. It carries both halves of the picker's contract — set before
  `commit()`, so a consumer reading it inside their own `change` handler sees the causing preset, and
  cleared on a hand-pick — plus three clear paths the picker cannot see because typing, clearing and
  resetting never reach it: a typed commit that actually changes the value (deliberately not a no-op
  re-commit, which would otherwise silently drop the preset), `clear()`, and `formResetCallback()`.

  `<lr-filter-bar>` can now pass `presets` on its `date-range` filter and reports the resolved preset
  on the `lr-input` detail as `appliedPreset`. The bar already composed `<lr-date-input>` and already
  forwarded that control's `min`/`max`, but had no path at all for `presets` — so the quick-range row
  and the component built for the same dashboard shape could not be combined. `type: 'custom'` was a
  poor substitute: hand-rendering the control plus a full adapter to set one property, and forfeiting
  the built-in date-range chip localization the docs themselves flag as non-trivial.

  `presets` is declared on the `date-range` definition only, not the shared base: a preset names two
  dates and the picker ignores the list outside range mode, so putting it on `'date'` would type-check
  a guaranteed-inert field.
- bd0f05f: `<lr-heatmap>` gained `sticky-labels` (`'none' | 'rows' | 'cols' | 'both'`, default `'none'`), which
  paints the matrix label bands into their own layers instead of into the scrolling bitmap.

  Matrix labels shared one canvas with the cells, so a tall grid lost its column header on scroll: a
  160-row matrix at cell-size 32 is about 5,100px of bitmap, and a header baked into it cannot be
  `position: sticky` on its own. The only workaround was a light-DOM mirror row, which had to follow
  the gutter width and cell size — and before `matrixGeometry` shipped it had to hardcode them, which
  made the workaround mutually exclusive with `row-label-width="auto"`: a consumer got the automatic
  fit or the sticky header, never both. (Scale on why the gutter matters: against the component's own
  10px label font, 160 country names ellipsized 37 times in the built-in 60px gutter and 3 times in a
  120px one.)

  A closed set rather than a boolean, because a boolean cannot express one axis at all and a later
  one-axis need would force either a second property or a breaking type change; and rather than a
  `sticky-row-labels`/`sticky-col-labels` pair, which is two attributes and four states for one
  concept with no single reflected value to select on in CSS. `rows`/`cols` name the axes this
  component already names everywhere else (`rowLabels`, `row-label-width`, `colLabels`,
  `col-label-height`), which `freeze-axis="x|y"` would have clashed with.

  Default `'none'` reproduces today's single-canvas output exactly, including in calendar mode, and an
  unrecognized value normalizes back to `'none'` and repairs the attribute.
- bd0f05f: New component `<lr-funnel>`: a conversion funnel — an ordered set of stages, each drawn as a bar
  whose length is that stage's share of the FIRST stage, read top-to-bottom as progressive drop-off.

  Nothing in the catalogue expressed this. A funnel is not a sorted bar chart: it normalizes to the
  first stage rather than the data maximum, its value axis carries no information worth drawing, and it
  is read as stage-to-stage retention rather than category comparison. Reaching one through
  `lr-bar-chart` meant switching off axes, grid and legend, hand-computing every percentage, and still
  pulling the Chart.js peers for what is a handful of rectangles. `lr-span-waterfall` encodes time
  offset, not share; `lr-flow-canvas` draws a graph, not a quantitative comparison; `lr-stepper` and
  `lr-progress-bar` express position or completion, not per-stage magnitude.

  It lives in the `data` family beside `lr-heatmap` and `lr-gauge` as an analytics primitive, and pulls
  no peer at all.

  Each stage carries both its absolute value and its share, because the interesting question is usually
  the percentage but the credibility check is the count. `comparison` draws a second series behind each
  bar, normalized to ITS OWN first stage, so a cohort's funnel *shape* can be read against a baseline
  whose absolute volumes are not comparable — comparing one entity against a many-entity peer group is
  the common case, and per-series normalization is what makes it legible. `dropoff` (on by default)
  renders the consecutive-stage change.

  The chart is plain HTML — an ordered list of stages with real text and a percentage-width bar — so
  the accessible representation *is* the chart rather than a transcript bolted onto a sighted-only
  drawing.

  Degenerate cases are defined and tested rather than left to chance: an empty series renders a
  localized empty state, a single stage renders one bar and no drop-off, a zero or negative first stage
  suppresses shares instead of dividing by it, a stage larger than its predecessor (real in funnels
  with re-entry) is not clamped, and a comparison series of a different length is matched by position.
- bd0f05f: `<lr-map>`'s `dataLayers` gained declarative marker clustering and a heatmap layer kind. Both are
  strictly additive — today's behaviour is the default in each.

  `cluster?: { radius?, maxZoom?, radiusSteps?, colorSteps?, countFont? }` opts an entry into
  MapLibre's native clustering: the source gains `cluster`/`clusterRadius`/`clusterMaxZoom` and the
  entry emits a cluster circle layer, a count symbol layer, and a circle layer for points that stayed
  unclustered. `markers` creates one `maplibregl.Marker` per entry, which is right for tens of pins and
  wrong for thousands — a consumer rendering up to 5,000 listings in a country-sized viewport got 5,000
  DOM nodes and an unreadable map. `radiusSteps`/`colorSteps` are `['step', …]` breaks on `point_count`
  in the same ascending `[value, output]` vocabulary `choropleth.stops` already uses, including the
  same "the first stop's output is also the base" rule.

  `kind?: 'auto' | 'heatmap'` plus `heatmap?: { weightField?, weightRange?, stops?, radius?, intensity? }`
  reaches MapLibre's first-class `heatmap` layer type. `dataLayers` emitted exactly three
  geometry-filtered layers — fill, line and circle — so a weighted-point density surface was
  unreachable declaratively even though the peer implements it. The colour ramp reuses the same
  `[value, color]` stop vocabulary `choropleth.stops` and `legendGradient` share.

  Between them these were the only remaining reason for raw MapLibre in at least one consumer, which
  carried roughly 212 lines behind the `.map` escape hatch — plus a `style.load` listener and
  idempotent remove-then-add, because a basemap swap wipes every layer and `<lr-map>` restored only its
  own. Both new renderings join the component's existing re-application path, so a `mapStyle` swap
  restores them too.
- bd0f05f: New opt-in stylesheet `@aceshooting/lyra-ui/tokens-root.css` publishes a curated subset of the
  resolved `--lr-*` layer at document scope, so an application's own custom elements can read the
  kit's tokens.

  `theme.css` ships the `--lr-theme-*` INPUT layer at `:root`, but the resolved OUTPUT layer
  (`--lr-color-*`, `--lr-space-*`, `--lr-radius`, `--lr-shadow-*`, `--lr-font-*`) is declared only
  inside each `lr-*` component's own shadow `:host`. An app's own elements are not descendants of any
  `lr-*` host, so nothing inherits it to them. Consumers measured the consequence in Chromium rather
  than inferring it: at document scope `--lr-color-brand`, `--lr-color-border` and `--lr-focus-ring`
  all resolve to the empty string while `--lr-theme-focus-ring-width` resolves fine. One project found
  550 `var(--lr-*)` references in its own components reading nothing — 358 with no fallback at all,
  the rest silently running on a literal fallback that never tracked the theme. Neither failure is
  detectable without reading computed styles in a browser, because an undefined custom property is not
  an error.

  The subset is curated rather than complete, deliberately: `--lr-*` is documented as the internal
  output layer precisely so it can change without a major, and publishing all of it would make several
  hundred names permanent public API. 114 names are in — ambient surfaces/text/borders, the semantic
  colour grid and its flat aliases, the spacing scale, radii, border widths, elevation, font sizes and
  weights, the focus-ring parts, and the base motion pair — each with a stated reason in the file, as
  is each deliberate omission.

  It is generated from the same canonical token source as everything else, so it cannot drift, and a
  fail-closed validator in the existing `check:design-tokens` gate rejects a curated token whose value
  reaches an internal name the file does not declare — the case that would otherwise ship an empty
  `var()` at `:root`. Ramp references resolve to literals at generation time and stay behind their
  `--lr-theme-*` input, so the file is self-sufficient without `theme.css`, still fully rethemable,
  publishes no ramp names, and computes byte-identical values to what a component reaches through the
  ramp.

  Opt-in, and layered in `lr-theme` like `theme.css`, so it changes nothing for anyone who does not
  import it and an app's own unlayered rules still win.
- bd0f05f: `web-types.json` now carries `js.properties`, `js.events` and `slots` alongside its attributes.

  It previously declared attributes and nothing else: 0 of 284 tags had properties, events or slots,
  while `custom-elements.json` in the same tarball described 1,029 events, 3,102 public fields and 445
  slots. 865 of those fields are `attribute: false` and were therefore invisible to JetBrains
  completion entirely — and they are frequently the primary API rather than an edge case
  (`lr-chart.datasets`, `.labels`, `.config`, `lr-heatmap.legendStops`, `.colorSteps`, `.cellColor`,
  `lr-lite-chart.datasets`).

  That gap mattered more here than it would for a typical component library: these are Lit components,
  so the idiomatic usage is `.prop=${…}` and `@event=${…}` in a template, not attributes. The shipped
  metadata covered the minority binding style and omitted the majority.

  The web-types schema the file already declared supports all three directly, and the data was already
  generated for the manifest, so this was a projection gap rather than missing information. It now
  emits every public instance field with its type and default, every declared event with its
  `CustomEvent<…Detail>` handler type, and every slot. Static fields and methods are deliberately
  excluded (a `.formAssociated=` completion would be wrong, and web-types has no IDE-integrated method
  kind).

  The sibling `vscode-html-data.json` stays attributes-only, which is correct: the VS Code custom-data
  format defines no properties/events/slots concept.

### Patch Changes

- 103922d: Documented that `<lr-chart>`'s `description` **replaces** the generated accessible summary rather
  than adding to it.

  Unset, the component builds an sr-only per-series summary from the actual data; set, it discards
  that summary entirely and substitutes the supplied text. That is the right behaviour for a full
  override, but the property was documented only as "Accessible chart description", which reads as
  additive — and a consumer adding a one-line caveat to five charts would have silently traded away
  the data summary on all five. They caught it by reading the source, and applied it only where the
  trade was actually wanted.

  No behaviour change; the JSDoc and the family reference now state the trade and point at the better
  tool for a caveat, which is visible text beside the chart rather than a note only screen-reader
  users hear.
- 2a156eb: Added the missing package-export route for
  `@aceshooting/lyra-ui/components/media/flag/flag-peer-bulk.js`. 11.2.0 led with that module as the
  opt-in bulk peer-registration entry point for `<lr-flag>`, and `llms/components/lr-flag.md` and
  `flag.class.d.ts` both told readers to import it — but it was never listed in `package.json`'s
  `exports`, and an exports map blocks everything it does not list. Following the documentation was a
  hard build error (`"…/flag-peer-bulk.js" is not exported under the conditions […]`), so the
  release's headline `<lr-flag>` feature was unreachable by any consumer.

  The derivation that exists to prevent exactly this — every `*-loader.ts` / `*-peer.ts` /
  `*-register.ts` / `registry.ts` module must be explicitly classified as public or internal — missed
  it because a *qualified* suffix (`-peer-bulk`) is not the bare suffix (`-peer`). The convention now
  accepts qualified variants; across the whole source tree that widening catches this file and
  nothing else.

  A second, independent instance surfaced in the same sweep and is fixed too:
  `@aceshooting/lyra-ui/components/data/flow-canvas/flow-types.js` is shown as an import in
  `llms/data.md` and in the generated `llms/components/lr-flow-canvas.md`, and was likewise
  unlisted. (Those types were still reachable through `flow-canvas.class.js`, so this adds the route
  the docs already named rather than any new surface.)

  Both were promises made in documentation, which no naming convention over the source tree can see.
  So a new release gate, `check:doc-specifiers`, now reads the promises instead: every
  `@aceshooting/lyra-ui/…` specifier a shipped file tells a reader to import must resolve through the
  exports map. It understands prose instructions as well as fenced code examples — the
  `flag-peer-bulk.js` promise was a sentence, not a code block.
- 8084d04: Added `flag-peer-bulk.js` (and the new `flag-peer-bulk-standard.js`) to `package.json`'s
  `sideEffects` list.

  These modules exist purely for their import-time side effect: a consumer writes a bare
  `import '…/flag-peer-bulk.js'` and never reads an export, so a bundler honouring `sideEffects` drops
  the module outright unless it is declared. The generator that derives these entries matched the bare
  suffix `-peer.ts` but not the qualified `-peer-bulk.ts` — the same blind spot that left the module
  out of the `exports` map.

  This half failed more quietly than that one. The missing export route was a hard build error; a
  missing `sideEffects` entry compiles cleanly and then simply does nothing in a production build, so
  `<lr-flag>` would fall back to no resolver with no diagnostic at all.
- bd0f05f: `<lr-flag>` now distinguishes a peer that is not installed from one that is installed but does not
  carry the capability the chosen entry point needs.

  Both cases previously produced the same warning — "install it with `pnpm add
  @aceshooting/lyra-flags`" — which is advice a reader in the second case has already followed, and
  which sends them looking for the wrong problem entirely.

  That case stops being exotic from this release on. `flag-peer-bulk-standard.js` requires
  `createFlagUrlResolver()` on the tier-committed `./standard` subpath, which older peers do not
  export at all, so a consumer who upgrades `@aceshooting/lyra-ui` while pinning
  `@aceshooting/lyra-flags` reaches it by the ordinary route. The peer-range floor moves in the same
  release to make that a resolution warning rather than a silent one, and this makes the runtime
  message match: it now says the package is present, that this is a version mismatch, and where to
  look for the floor it expects.
- 2a156eb: `<lr-heatmap>`'s `matrixGeometry` now returns the geometry the last matrix-mode draw actually
  painted with, instead of recomputing from current layout on every read.

  It is documented as "the gutter/cell geometry the last matrix-mode draw actually painted with", and
  11.2.0's notes claimed it "can never disagree" with the canvas because it reuses the same internal
  getters `drawMatrix()` calls. Reusing those getters is precisely what made it disagree: they read
  *current* layout, not the last paint, so any interval where layout has moved but no draw has
  happened made the getter describe a canvas that does not exist. Two such intervals are routine —
  full redraws pause while the host is outside the viewport (documented behaviour of this component),
  and `rowLabelWidth`/`colLabelHeight` are not redraw-triggering properties at all, so assigning one
  moved the getter *permanently* ahead of the canvas rather than for a transient window.

  That landed squarely on the use case the property was added for: a light-DOM sticky-header mirror
  for a tall matrix — i.e. exactly the component most likely to be scrolled out of view. A mirror
  synced from the getter while the grid was off-screen lined up with geometry the canvas was not
  using, which is the same misalignment the property exists to eliminate. It also silently disagreed
  with `lr-matrix-geometry-change`, which fires only from the draw path and was always correct.

  The getter now returns the frozen object the draw stored and the event carried, so the two are
  equal by construction. The returned object is frozen, so a consumer cannot corrupt the component's
  own change detection by mutating it.
- 2a156eb: Fixed two defects in `<lr-map>`'s `maxBounds`, reported together because the first was the only
  thing hiding the second.

  `maxBounds` never reached maplibre-gl when set declaratively. It is `attribute: false`, so a
  property binding is the only way to set it, which puts its one and only appearance in `changed` on
  the first update — before the component's asynchronous peer import and WebGL initialization have
  produced a map. The `updated()` guard `changed.has('maxBounds') && this._map` therefore
  short-circuited, and because the property never changed again it was never retried: a documented
  property that read back as set, did nothing, permanently, and warned about none of it. It is now
  applied from the map-ready path as well, so a declaratively-set box reaches the peer; a later
  reassignment still goes through `updated()` as before.

  The property's guard also could not run in the case it was written for. It applies the bounds and
  then reads the camera back to detect a non-finite zoom — but at the conditions its own warning text
  names (sub-1 fractional zooms in wide containers) maplibre-gl 6.x throws synchronously out of
  `setMaxBounds()` instead, so the readback line was never reached. With no `try`/`catch` the
  exception escaped `updated()` into the consumer's render cycle, degenerating into repeated throws
  from the peer's own matrix math on every later `resize`/`setZoom` and a canvas that never painted
  again. A throw now routes into the same drop-the-constraint-and-restore-the-camera path the
  non-finite-camera branch already used, so the documented worst case — an unconstrained map plus one
  dev-mode warning — is now the real worst case.
- 2a156eb: The release process now fails when the published upgrade feed lags npm.

  The documented upgrade workflow tells consumers — and upgrading agents — to fetch
  `https://www.lyra-ui.com/changelog.json` and read every release between their installed version and
  its `latest`. That feed is built from this package's `CHANGELOG.md` by the sibling website and
  deployed separately, after the release, so between `npm publish` and that deploy it advertises the
  *previous* release as current.

  Consumers reported that window twice, from two different projects, on two consecutive releases: the
  site said 11.0.0 while npm had 11.1.0, then 11.1.0 while npm had 11.2.0. It fails silently and it
  inverts the workflow's own advice — a reader who trusts the feed concludes they are already current
  and never reads the new release. One release skipped that way contained a fix the reader was
  waiting for. Both reporters caught it only by reading the installed tarball's `CHANGELOG.md`
  instead, which is what the workflow tells them they should not have to do.

  `release-integrity.mjs verify-site-freshness` now checks npm's dist-tag, the published feed's
  `latest`, the presence of the new version in its `releases` array (it went missing entirely once,
  which defeats even a reader who ignores `latest`), and the component catalog's `catalog_version` —
  which rides the same deploy and was caught a release behind at the same time. The release script
  waits on it, so a stale feed is now a loud, actionable release failure rather than something a
  consumer discovers weeks later.

  No published component surface changes.
- 8084d04: Restored two public property names that were renamed with no alias, no changelog entry and no
  deprecation record, silently breaking shipped consumers.

  - `<lr-app-rail-item>`: `active` is back as a deprecated alias for `current`, read alongside it —
    the item is current when either is true, in both property and attribute form.
  - `<lr-widget>`: `activeView` is back as a deprecated alias for `activeViewId`, which it seeds.

  Both were the members' *original* public names. `active` shipped documented as public API ("add an
  `active` property that reflects `aria-current="page"` onto the item"), and a later release's notes
  still described it as `active` after the rename had already happened. `activeView` never appears in
  `CHANGELOG.md` at all, so its rename was never announced in any form.

  The breakage was invisible by construction: a Lit `.prop=${…}` binding on a custom element is
  untyped, so `.active=${…}` and `.activeView=${…}` did not error — they became dead expandos. No
  consumer type check, test suite or build step could see it. One consumer's app rail consequently
  had no current-item indicator and a permanent `aria-current="false"` — an accessibility regression
  — and its widgets fell back to their first view, with everything still passing.

  This is what the house rule about mirrored members already required in general: a rename adds a
  second name, it does not swap one out from under shipped consumers. The compatibility window runs
  long (`removalNotBefore` two majors out) because these aliases are not new API — they are the names
  consumers already wrote.

  `activeView` seeds rather than being read alongside, because unlike a boolean flag it is a property
  the component itself writes (a view-toggle click, and the fallback when `views` no longer contains
  the active id); a read-alongside alias would undo a later interactive change on the next update.
- Updated dependencies [bd0f05f]
  - @aceshooting/lyra-flags@2.2.0

## 11.2.0

### Minor Changes

- a44e6e1: Added a first-class bulk-resolution path for `<lr-flag>`, for a page that renders most/all flags at
  once (a country table, a full locale picker) instead of independently resolving each instance:

  - `@aceshooting/lyra-flags` gained `createFlagUrlResolver()`, a `flagUrl`-shaped resolver factory
    backed by one shared `flagUrls()` fetch instead of a fresh per-code lazy resolution per call.
  - `@aceshooting/lyra-ui` gained `flag-peer-bulk.js` (`components/media/flag/flag-peer-bulk.js`), an
    opt-in alternative peer-registration entry point to the default `flag-peer.js` — import one or the
    other, never both. Only worthwhile when the page renders most/all flags; `flag-peer.js` remains
    the right default for a handful of flags. `fidelity="compact"/"detailed"` on individual elements
    still resolves correctly either way — only the standard tier is bulk-fetched.
- a44e6e1: Two consumer-filed `<lr-heatmap>` gaps:

  - **`matrixGeometry` readback + `lr-matrix-geometry-change` event.** Matrix mode's resolved gutter/
    cell geometry (`padLeft`, `padTop`, `cellSize`) was entirely private, so a consumer building a
    sticky light-DOM header mirror for a tall matrix had to hardcode numbers that `row-label-width`/
    `col-label-height`'s `"auto"` resolution could silently change out from under them. `matrixGeometry`
    now exposes exactly what the last matrix-mode draw painted with (reusing the same internal getters
    `drawMatrix()` itself calls, so it can never disagree), and `lr-matrix-geometry-change` fires
    whenever a redraw actually changes it.
  - **`HeatmapLegendStop.partOfRamp`.** The dev-mode ramp/legend-mismatch warning had no way to
    express a legend swatch that is intentionally outside `colorSteps` — e.g. a calendar heatmap's
    fixed neutral "no data" color shown alongside an N-step sequential ramp. Set `partOfRamp: false`
    on that stop to exclude it from the comparison; every other stop (and every existing consumer
    that never sets this) keeps today's exact behavior.

### Patch Changes

- f5dde67: Bumped the optional `dompurify` peer dependency range floor from `^3.4.13` to `^3.4.14` (a
  DOMPurify patch release). Every other dependency change in this release is dev-tooling only
  (Storybook, Vite, publint, oxc-parser) and carries no published surface.
- a44e6e1: Four documentation gaps reported against 11.1.0 by a real-world consumer audit:

  - **`<lr-flag>` sizing.** The host sizes from `font-size` (`block-size: 1em`, `inline-size` derived
    via `aspect-ratio`), never documented anywhere. Setting `width`/`inline-size` directly makes both
    axes definite, which defeats `aspect-ratio` and squashes the image instead of scaling it. Now
    documented on the class JSDoc and in `llms/media.md`.
  - **`<lr-flag>` bulk rendering.** Nothing pointed a consumer rendering many flags at once (a country
    table, a locale picker) at `@aceshooting/lyra-flags`'s existing `flagUrls()` — one call resolving
    every flag, instead of each `<lr-flag>` instance independently calling `flagUrl()`. Now
    cross-linked from the class JSDoc and `llms/media.md`, alongside the new per-tier peer-resolver
    entry points (see the paired `@aceshooting/lyra-flags` changeset).
  - **`accessibleLabel`'s two conventions.** Most components alias it directly onto native
    `aria-label`; a minority (e.g. `lr-callout`, `lr-table`) that separately compute an internal
    accessible name expose it through a bespoke `accessible-label` attribute instead, so a host
    `aria-label` can still override it. Both are individually correct, but nothing stated the split,
    so `accessible-label="…"` on an `aria-label`-only component was a silent no-op. Now documented in
    `llms/shared.md`'s accessibility contract section.
  - **Shadow-scoped resolved tokens.** The quick-start theming snippet (README and `llms/shared.md`)
    never warned that the resolved `--lr-color-*`/`--lr-space-*`/`--lr-radius`/`--lr-shadow-*`/
    `--lr-font-*` layer is declared only on each `lr-*` element's own shadow `:host` — unreachable
    from plain application CSS or a consumer's own custom elements. The deeper explanation already
    existed in "Where an override actually reaches"; it's now also stated up front, at the first
    theming snippet.
- a44e6e1: `LyraElement` no longer statically imports `internal/form-control-labels.js` (the external-label
  bridge + form-internals capture that only a form-associated component ever uses). Every
  presentational component — `lr-flag`, `lr-popover`, and everything else that doesn't opt into form
  association — no longer ships that module in its reachable bundle graph (measured previously at
  ~6KB gzip on `lr-flag`). Form-associated components register it themselves (the `FormAssociated`
  mixin and 19 hand-rolled form controls each now import it explicitly), so every form control's
  label/hint/error/reset/validity behavior is unchanged.
- Updated dependencies [a44e6e1]
- Updated dependencies [a44e6e1]
  - @aceshooting/lyra-flags@2.1.0

## 11.1.0

### Minor Changes

- 555154e: Four follow-ups to 11.0.0, all reported against the shipped release:

  - **`<lr-date-input>` forwards `presets`** to its nested picker, and exports the `presets` /
    `preset-button` parts. 11.0.0 landed the feature on the inline calendar only, while the compact
    text-field-plus-popover shape is the one a dashboard time filter actually uses — and there was no
    consumer-side escape hatch, since a CSS part cannot set a JS property.
  - **`<lr-date-picker>` gains a read-only `appliedPreset`**, reporting which preset produced the
    current value (`undefined` for a hand-picked range). 11.0.0 presented commit-path
    indistinguishability as a feature; it is, for serialization and clamping, but it destroyed the one
    fact a dashboard filter needs, because "Last 7 days" must stay *relative* across a reload.
    Re-deriving it by matching `value` is both the mapping table `presets` exists to delete and
    ambiguous — Today and This month coincide on the 1st.
  - **`LyraDateRangePreset.start`/`.end` are now optional**, meaning an open bound that resolves to
    `min`/`max`. The changelog and doc comment advertised an "All time" preset that the type could not
    express and `applyPreset` silently ignored, so that button rendered and did nothing. Where the
    matching `min`/`max` is unset the button now renders **disabled** rather than looking live.
  - **`<lr-lite-chart>` gains `showDataTable` and `dataTableToggle`** with the same semantics and the
    same `data-table-toggle` part as `<lr-chart>`. It extends `LyraElement` directly and inherited
    nothing from the 11.0.0 addition, which left the component that exists to avoid the Chart.js peers
    as the only one still needing a hand-rolled `<details>` — or Chart.js, for a button.

### Patch Changes

- 555154e: Corrected 14 documentation annotations that named **10.1.0**, a version that was never published.
  Those members shipped in 11.0.0: the docs were written while the release was expected to be a
  minor, the public-API semver gate then required a major, and nothing restamped the annotations.

  This was worse than a typo. A consumer on 10.0.1 reading "new in 10.1.0" either installs a version
  that does not exist, or assumes their 10.0.1 install already has the feature and debugs an
  attribute that silently does nothing — Lit accepts an unknown attribute without error, so there is
  no failure signal at all.

  Also corrects the generated per-component "Optional peers" header, which attributed peers reached
  only through an erased `import type`. `lr-lite-chart` was listed under all four Chart.js peers
  despite existing precisely to avoid them, inverting the choice the component offers; the same fix
  drops several other over-attributions (the d3 peers were credited to 12 tags and genuinely belong
  to 2). Side-effect registration edges still count, so transitive peers are unaffected.
- 555154e: **Fixes a silent focus-ring regression introduced in 11.0.0.** `--lr-focus-ring` was added as a
  composite outline shorthand explicitly to replace the Web Awesome `outline: var(--wa-focus-ring)`
  idiom — but it was declared only inside each component's `:host`, and that idiom is written by a
  consumer against their *own* element. At document scope the token resolved to the empty string,
  which makes the whole `outline` declaration invalid at computed-value time; because `outline` does
  not inherit, the ring did not fall back, it **disappeared**. No console warning, no test signal —
  a WCAG 2.4.7 failure that looked correct in review. The library evidenced the gap itself:
  `styles/native.css` hand-expanded the ring rather than using the composite.

  `theme.css` now declares `--lr-focus-ring` and its three parts at document scope, on `:root` and on
  both mode selectors — not `:root` alone, because `.lr-dark` / `[data-lr-theme='dark']` may sit on
  any ancestor, and resolving the colour once at `:root` would freeze the light value for a subtree
  that later switches. Components are unaffected: their own `:host` declarations still win, which is
  now asserted.

  `styles/native.css` deliberately keeps its fallback-chained expansion so it continues to work for
  consumers who load it without `theme.css`.

  Reported twice independently, with a live `getComputedStyle` repro showing `outlineStyle: "none"`.
- 2821af9: Three defects reported against 11.0.0:

  - **`<lr-pdf-viewer>` text layer, reopened.** 10.0.0 fixed only half of it. The chunk bounding
    guarded against copying an `undefined` style over a good one, but it also *rebuilt* the style map
    from the fonts of the items retained in that chunk — so a style PDF.js announces ahead of the
    items that use it was dropped and never re-sent. Both failures end the same way: a later lookup
    reads `undefined.vertical` and aborts the rest of the page. Measured by the reporter on a 9-page
    document as 4 affected pages and 101 of 271 spans orphaned. Now every own entry the chunk carries
    is copied and only `undefined` is skipped, so falsy-but-defined styles (`null`, `0`, `''`) still
    survive and an inherited `constructor`/`toString` stays unreachable.

  - **`<lr-table>` no longer dies on a column missing its `cell` renderer.** `cell` is typed and
    documented required, but columns arrive through a lit `.columns=${...}` binding, which `tsc` does
    not type-check — so required-ness was unenforced where it is written *and* unguarded at runtime.
    A single malformed column threw out of lit's `repeat`, taking the whole table down with a stack
    naming neither the column nor the table. It now degrades to an empty cell and reports once per
    column, naming the key, the tag and the missing member.

  - **The shared scratch canvas is created with `willReadFrequently`.** `<lr-heatmap>`'s colour
    resolution does a 1×1 `getImageData()` readback for any colour the canvas normalizes into a form
    its string parsers reject (`color-mix()`, `oklch()`, `lab()`), which Chrome warns about on every
    page carrying a heatmap. A `color-mix()` ramp takes that readback per cell.

## 11.0.0

### Major Changes

- 5066d4b: **Version note: this major carries no known breaking change for consumers.**

  Everything in this release is additive or a bug fix — no public member was removed, renamed, or
  had its behaviour or default altered. The major bump is taken because the public-API semver gate
  (`check:public-api`) classifies 328 changes as breaking, and every one of them is fingerprint or
  generated-type churn rather than a real break:

  - 248 `:dependencies` and 39 `:contract` hash changes — a symbol's transitive-dependency
    fingerprint moves whenever a widely-composed base class gains a member, so adding one property to
    `LyraChart` rewrites the hash of every chart subclass and every subpath that re-exports it.
  - 39 generated React/Vue/Svelte props **type strings**, widened by the newly added props. The
    differ compares the printed type text, which cannot distinguish an additive union widening from a
    removal.
  - 2 `lr-popover` `popup-role` default entries moving `null → 'dialog'`. The default did not change;
    this release simply documents it with `@default` for the first time, so the manifest records a
    value where it previously recorded none.

  Consumers upgrading from 10.x should not need code changes. If you use the generated framework
  prop types, the unions gained members but lost none.

### Minor Changes

- 2fb4af7: `<lr-chart>` (and every chart subclassing it — bar, line, pie, doughnut, radar, polar-area,
  scatter, bubble, histogram) and `<lr-box-plot>` gain `dataTableToggle` (`data-table-toggle`), which
  renders a localized disclosure button above the accessible data table.

  `showDataTable` was all-or-nothing: the table was either permanently screen-reader-only or
  permanently visible, so a sighted reader who wanted the numbers behind a chart could only get them
  if the consumer hand-rolled a `<details>` around a duplicated copy of the table. With the toggle
  on, `showDataTable` becomes the disclosure's initial state rather than its whole behavior. The
  table stays in the DOM in both states, so assistive technology never loses it, and the button
  carries `aria-expanded` plus `aria-controls`. A new `data-table-toggle` CSS part styles the
  control. Unset, nothing renders and behavior is unchanged.
- e084afb: `<lr-combobox>` now renders `<lr-option>`'s adornment slots, and gains `visibleOptions` for bounding
  the suggestion popup's height.

  **Adornments (a fixed contract, not just a new feature).** `<lr-option>` documented `start`/`end`
  slots, their `prefix`/`suffix` aliases, and four matching CSS parts — but `<lr-combobox>` builds its
  popup from normalized row *data* rather than from the light-DOM nodes, so inside the one component
  `<lr-option>` exists to feed, none of them rendered. A row could show a colour dot, a badge and a
  sub-line but not a 16px image, which is the one adornment a country, currency, language or user
  picker most often wants, and neither documented workaround was available (`::part(option)` cannot be
  compounded past the part, and `dot-color` rejects `url()`).

  Adornments now render as new `option-start` / `option-end` parts, inert and `aria-hidden` so they
  never join the option's accessible name. The nodes are **cloned** into the row, so the author's own
  `<lr-option>` subtree is left exactly where they put it rather than being moved into a shadow root
  as a side effect of opening a dropdown. Async `source` rows can supply the same `start`/`end`
  fields alongside the existing `icon`.

  **`visibleOptions`** (`visible-options`) bounds the popup to about that many rows, leaving the rest
  reachable by scrolling. It is measured from where row N actually starts, since a row's height varies
  with sub-lines, adornments and group labels. Unset, the listbox keeps exactly its previous
  max-height behavior.

  The doc comments on all three caps — `visibleOptions`, `maxRender`, and `maxOptionsVisible` — now
  each state how they differ from the other two, which was the confusion that prompted this.
- 3b3af14: `<lr-date-picker>` gains `presets`, a quick-range button row for the dashboard time-filter shape
  (Today / Last 7 days / Last 30 days / This month / All time).

  The pieces for this existed but were split across two components that each held half the contract:
  the date components had the calendar, locale and range logic but no preset affordance, while
  `<lr-time-range>` had exactly the wanted preset API but is a two-handle numeric brush with no date
  logic, so a caller had to map a time axis onto `[min, max]` themselves and got no calendar. Building
  it by hand meant a ~260-line control plus its own preset/custom state machine.

  `LyraDateRangePreset` is deliberately the same `label`/`start`/`end` shape as `TimeRangePreset`, so
  the library has one preset vocabulary rather than two — only the unit differs (ISO `YYYY-MM-DD`
  instead of numbers). Range mode only; unset renders nothing. Applying a preset commits through the
  same path a two-click selection uses, so ISO serialization, `min`/`max` clamping and the
  `input`-then-`change` pair are identical. A reversed preset normalizes, and a malformed one is
  ignored rather than clearing the value, so a bad entry in a config-driven list never reads as "the
  user picked nothing". New `presets` and `preset-button` CSS parts.
- c915980: Added `--lr-focus-ring`, a composite outline shorthand (`var(--lr-focus-ring-width) solid
  var(--lr-focus-ring-color)`) alongside the three existing parts, which stay exactly as they are.

  Web Awesome exposes `--wa-focus-ring` as a ready-made outline value, so the common consumer idiom
  is `outline: var(--wa-focus-ring)`. Migrating it meant hand-expanding every site, which is easy to
  get subtly wrong — omitting the `solid` keyword yields an outline that renders in some engines and
  not others — and each hand-expanded copy stops tracking any future change to how the ring is
  composed. `--lr-focus-ring-offset` stays separate because `outline-offset` is its own property, not
  part of the `outline` shorthand.

  `llms/tokens.md` also now documents why an ancestor `--lr-*` override does not survive a nested
  component boundary: every component re-derives that layer from `--lr-theme-*` on its own `:host`,
  so the override is reset at the first `lr-*` inside another `lr-*`'s shadow root and degrades
  silently. The `--lr-theme-*` input layer is the one that inherits.
- 3d7a6a5: `<lr-heatmap>` matrix mode gains `colLabelRotation` (`col-label-rotation`) and an `'auto'` value for
  `colLabelHeight` (`col-label-height`), giving column labels the escape hatch the row gutter got in
  10.0.0.

  Column labels were horizontal-only in a fixed 20px band, so in a dense matrix — where every column
  is far narrower than a typical label — adjacent labels collided and the axis became unreadable,
  with no rotation or angle property anywhere in the surface. Each label now rotates about an anchor
  at its own column's centre with the label's end at that anchor, so it leans back over the columns
  to its left and the last column's label cannot overflow the canvas. `col-label-height="auto"`
  measures the labels and projects their width through the rotation, so the band sizes itself.

  Unset, both are inert and painting is unchanged. Values outside `[0, 90]` clamp and non-finite
  values normalize to `0`. Rotation is deliberately not mirrored under `dir="rtl"`, matching the
  documented rule that both grid modes retain physical LTR geometry.
- 20728fb: `<lr-map>` gains a `'step'` choropleth interpolation and independent fill/stroke colours on
  `dataLayers` — the two declarative gaps that stopped an application migrating off a first-party
  MapLibre wrapper after every other property already matched.

  **`interpolation: 'step'`** emits maplibre's `['step', …]` instead of `['interpolate', …]`, giving
  discrete bands rather than a continuous ramp. A ramp is wrong whenever the legend advertises a fixed
  set of ranges with one swatch each: it puts colours on the map that appear nowhere in the legend and
  renders two regions in the same advertised band as visibly different colours. `stepBaseColor` sets
  the colour below the first threshold (which `['step', …]` requires) and defaults to the first stop's
  own colour.

  **`dataLayers[].color` / `.strokeColor`** override `tone` for the fill and for the line/circle
  layers respectively, falling back to `color` and then `tone`. They are separable because a fill and
  its outline want opposite things on a choropleth-plus-overlay map: the fill competes for area and
  must sit quiet, while the 1px outline competes with nothing and is the only thing keeping a no-data
  region's shape readable once the fill is that faint. Deriving one from the other measured 1.41:1
  against a light basemap, under WCAG 1.4.11's 3:1 floor for graphical objects. A `var(--lr-…)`
  reference is resolved against the host first, since MapLibre paints to a WebGL canvas and never sees
  the CSS cascade.

  Both are additive: an unset `interpolation` still interpolates linearly, and a `tone`-only data
  layer paints exactly as before.
- 4a701e7: `<lr-popover>` gains a third `popupRole` value, `none`, so the library can express the WAI-ARIA
  disclosure-navigation pattern. Previously `popupRole` was `dialog | menu` only, which left a header
  nav flyout with no correct option: `menu` announces "menu, menu item" and expects `menuitem`
  children, while a navigation flyout is a list of links, and `dialog` implies an interruptive
  surface. Consumers had to abandon the library's overlays and hand-roll a
  `button[aria-expanded][aria-controls]` plus a plain list.

  Under `popup-role="none"` the popup surface renders no `role` and no generated `aria-label`, and
  the trigger carries no `aria-haspopup`, so the slotted `<nav>` owns the semantics and the
  accessible name. Everything else — `aria-expanded`/`aria-controls`, light dismiss, Escape, focus
  return, positioning — is unchanged. Purely additive: `dialog` remains the default, and
  `lr-dropdown` still pins its own role to `menu`.

### Patch Changes

- e6ed0ca: The migration codemod now warns about four classes of `wa-*`/`sl-*` reference it does not rewrite,
  instead of leaving them silent: tag selectors inside a `` css`` `` tagged template, `::slotted()`,
  DOM selector strings reached through `this`/`this.shadowRoot`, and `--wa-*`/`--sl-*` custom
  properties.

  Each of these fails silently at runtime after a migration — a CSS rule keyed on a tag that no
  longer exists matches nothing, `::slotted()` likewise, `querySelector` returns null, and a `var()`
  naming a removed token falls back to its second argument or to nothing. Nothing throws, nothing
  fails a build, and a typechecker cannot see inside a template literal. Because `--check` is
  documented as a CI gate, the silence meant CI certified a migration that had visibly broken the
  component's styling.

  Tokens are deliberately reported rather than rewritten: the two spacing scales are offset by one
  step (Web Awesome `m` is 1rem, Lyra `m` is 0.75rem), so renaming by name alone silently tightens
  every gap, while mapping by value has no target for 1.5rem or 2.5rem. Warnings are filtered against
  the rewrites the same pass produced, so a reference the inventory does map is never both rewritten
  and warned about, and a self-declared `--wa-*` property (the consumer's own, merely sharing the
  prefix) is exempt.

  `<lr-dialog>`'s docs also now warn that `lr-close` is not a dialog-scoped name — nine components
  emit it, several of which are routinely nested inside a dialog, and library events bubble and are
  composed, so a listener bound on the dialog also receives a descendant's close.
- c5baec5: `<lr-flag>` now warns once in the console when `country`/`language` is set but no flag resolver has
  been registered, naming the offending code and the `flag-peer.js` import that fixes it. Previously
  this failed to the visible `[part="error"]` state in complete silence, which is indistinguishable
  from missing flag data — the resolver is deliberately absent from the core component's module
  graph, so an unimported peer entry is the single likeliest cause and was the hardest to diagnose.
  The warning is emitted once per resolver-registration generation, so a page of many flags does not
  repeat it. An already-resolved `src`, a registered resolver, and a well-formed-but-unmapped code
  (which is data, not a defect) all stay silent.
- b573859: The `lr-locale-picker` Storybook page now registers the optional flag peer, so its rows render real
  flags instead of silently empty frames. Found by the new `<lr-flag>` missing-resolver warning on its
  first run — `flag.stories.ts` had always imported `flag-peer.js` for exactly this reason, and the
  locale-picker page never did.
- c915980: The `lyra-ui-migrate` CLI no longer silently does nothing when launched through a package manager's
  bin shim. Its entry guard compared `process.argv[1]` to `import.meta.url` as raw paths; under pnpm
  the package directory is a symlink into the virtual store, so the two never matched and `run()`
  never executed. The process printed nothing — not even `--help` — rewrote nothing, wrote no report,
  and exited 0.

  The serious half is that `--check` is documented as a CI gate that "exits nonzero while rewrites or
  warnings remain". A silent exit 0 is indistinguishable from success, so on every pnpm project the
  gate passed unconditionally — worse than having no gate, because it is trusted. npm and yarn were
  unaffected, which is why it survived. The guard now compares realpaths, and a regression test
  invokes the CLI through a symlink that mimics the pnpm layout.

## 10.0.1

### Patch Changes

- 84a28c2: Fixed `lr-combobox`, `lr-token-input` and `lr-radio-button` declaring a
  `text-overflow: ellipsis` that could never fire.

  `text-overflow` only applies to content that overflows its line box inline. Each
  of these parts was left at `white-space: normal`, so the text wrapped instead of
  overflowing and the box never had horizontal overflow at any label length --
  `scrollWidth === clientWidth` in every case. `lr-combobox`'s and
  `lr-token-input`'s labels additionally set `overflow-wrap: anywhere`, which put
  the wrap *inside* a word.

  The visible effect was worst on `lr-combobox`, whose tag caps at
  `--tag-max-size` (80px by default): a selected `Received` rendered as `Receiv/ed`
  across two lines, and a wrapped tag row could overflow a trigger pinned with
  `--lr-combobox-trigger-height`.

  All three now carry `white-space: nowrap`, matching `lr-select`'s
  `[part='tag-label']`, which has always had it. Content that fits today is
  unchanged; content that used to wrap now truncates, which is what the existing
  declaration asked for.

  `--tag-max-size` still defaults to 80px on `lr-combobox` against `lr-select`'s
  12rem. That difference is deliberate for now -- changing it alters default
  rendering rather than fixing a dead declaration -- and is tracked separately.

## 10.0.0

### Major Changes

- c640e0a: **10.0.0.** This release removes the members deprecated during 9.x, which is the whole of its
  breaking surface. Everything else in 10.0.0 is additive — no component's default rendering changes,
  and no existing property, event, slot, part or CSS custom property was renamed or repointed.

  Removed, each with a like-for-like replacement that has shipped since 9.x:

  - `confirm()`: the `tone` option on `ConfirmOptions` → `variant`. (An earlier draft of this note
    attributed the rename to `<lr-confirm-bar>`; that component's `tone` → `variant` landed in 9.x
    and left no alias, so nothing changes there in 10.0.0. The member removed here is the one on the
    `confirm()` helper in `overlays/dialog/confirm.ts`.)
  - `<lr-swatch-picker>`: `options` → `items`, `label` → `accessibleLabel` (or the host `aria-label`),
    and the `SwatchOption` type → `SwatchPickerItem`.

  Deliberately **kept**, so migrating consumers are not caught out:

  - `<lr-icon>`'s `autoWidth` / `auto-width` stays, deprecation notice and all. Web Awesome's own
    pinned manifest still publishes `auto-width` on `wa-icon`, and a mirrored tag owes its whole
    upstream surface — dropping it classifies `wa-icon` as an `unsupported` mapping, which is a
    release blocker. Prefer `canvas="auto"`; the alias goes when upstream's does.
  - The same holds for **seven more** deprecated aliases whose records say `removalNotBefore: 10.0.0`
    and which are therefore, on paper, removable now: the `base` part on `<lr-accordion-item>`,
    `<lr-file-input>`, `<lr-qr-code>`, `<lr-sparkline>` and `<lr-video-playlist>`, and the `label`
    part on `<lr-file-input>` and `<lr-known-date>`. Every one is published by the pinned upstream
    manifest, and removing them was measured against the real comparison pipeline: each produces an
    `unsupported` mapping. `<lr-qr-code>`'s `base` is the sharpest case — `sl-qr-code` publishes it as
    its ONLY part and does not deprecate it at all.

    That `10.0.0` is not a plan anyone made. Policy requires a removal to clear one whole subsequent
    major, so `10.0.0` is simply the earliest legal value for a deprecation dating to 8.x. The records
    now say so, because read literally they promised a removal that will never happen while upstream
    ships the same names.

  - `lr-geojson-view` stays. It is a permanent compatibility class for the pre-v9 tag, not a
    deprecation.
  - `base` / `wrapper` on `<lr-switch>` and `<lr-checkbox>` stay pointing at the control owner. They
    are Web Awesome / Shoelace compatibility names, and the library's parity contract is that a
    mirrored name keeps its meaning; `row` (new in this release) names the row wrapper instead.
- 082b885: **10.0.0.** A set of public-contract corrections that need a major boundary, plus a larger set of
  additive fixes. Breaking items first; each one states what to change if you relied on the old
  behavior.

  ### Breaking

  **`<lr-calendar>` derives the week start from the locale.** `firstDayOfWeek` defaulted to a
  hardcoded `1` (Monday) and never consulted the locale — while the very same component already
  threaded `effectiveLocale` through its weekday *label* formatting. Measured, same `en-US` page:
  `<lr-calendar>` rendered `Mon Tue Wed…` while `<lr-date-picker>` rendered `Sun Mon Tue…`. The
  default is now `'auto'`, resolved through the same `resolveFirstDayOfWeek()` contract
  `<lr-date-picker>`/`<lr-date-input>` already use. The type is now exactly
  `'auto' | 'sun' | … | 'sat'`: the bare `0`–`6` integer form is gone rather than kept as a second
  spelling, so there is one way to express a week start instead of two that had to be sanitized and
  wrapped against each other. Replace `first-day-of-week="1"` with `first-day-of-week="mon"` to keep
  the old rendering. There is no `wa-calendar`, so no upstream parity is affected.

  **`<lr-progress-ring>` gains `show-value`, defaulting to `false`.** A determinate ring rendered its
  percentage unconditionally, with no way to suppress it short of slotting replacement content — while
  its sibling `<lr-progress-bar>` has had opt-in `show-value` all along, and the reference has always
  claimed the two share "the same value contract". They now actually do. Add `show-value` to keep the
  percentage. `aria-valuetext` still carries it either way, so the accessible value is unchanged.

  **`<lr-media-card>`'s `alt` becomes optional, so a decorative image is expressible.** It was
  `alt: string = ''`, and the render read `this.alt || this.filename || <localized generic>` — so an
  explicit `alt=""` was indistinguishable from an absent one and came out as `alt="Image attachment"`.
  There was no way to mark the image decorative, which is the one thing `alt=""` means in HTML. The
  type is now `alt?: string` and the render uses `??`, matching `<lr-image-viewer>` and
  `<lr-document-preview>`, which already documented that contract. Omitting `alt` is unchanged; only
  the value read back from an unset property differs (`''` becomes `undefined`), so a consumer
  comparing `el.alt === ''` should read `el.alt ?? ''`. The nested `<video controls>` label
  deliberately does NOT follow: an empty `alt` there would leave an interactive player with no
  accessible name, and "decorative" is not a state a media control can be in.

  **`<lr-attachment-chip>`'s `lr-preview-request` is no longer cancelable.** It was advertised as a
  veto point, but the chip never read `defaultPrevented` and owns no preview default action to
  cancel — its own docs say it "never registers or owns a viewer/overlay" — so `preventDefault()` was
  a no-op. The flag is removed rather than left as a promise the component cannot keep.

  ### Event vocabulary: one name per event

  Several events had two spellings. 10.0.0 keeps the canonical name and **removes the old one
  outright** rather than shipping a deprecated alias into a library that has no released consumers
  yet — a dual-emit alias is a permanent tax paid to protect users who do not exist.

  Rename the listener; the detail object is unchanged in every case.

  | Removed | Use instead | On |
  |---|---|---|
  | `lr-entity-activate` | `lr-entity-select` | `<lr-entity-card>`, `<lr-entity-chip>`, `<lr-neighbor-list>` |
  | `lr-visible-range-changed` | `lr-visible-range-change` | `<lr-virtual-list>` |
  | `lr-run-select` | `lr-run-change` | `<lr-rag-eval-dashboard>` |
  | `lr-dialog-close` | `lr-close` | `<lr-dialog>`, `<lr-drawer>` |

  `lr-visible-range-changed` was the only past-tense `-changed` spelling among 58 `-change`-family
  events, so a convention-driven listener silently missed it — on a component embedded in ten viewers.

  Two deliberate non-removals. `<lr-community-card>` and `<lr-path-strip>` keep `lr-entity-activate`:
  it is their only name and never was an alias. `<lr-accordion>` keeps `lr-expand`/`lr-collapse`,
  which mirror `wa-accordion`'s real event names — removing them would have broken upstream parity
  rather than tidied it. `lr-citation-badge` was also left alone: `lr-citation-select` is an
  established *container*-level event with a richer `{ citation }` detail that containers translate
  its `{ sourceId, index }` into, so unifying there would have delivered two shapes under one name.

  ### Interaction, focus and visibility corrections

  A sweep with a CSS-specificity analyzer found rules that were supposed to win losing to another rule
  in the same shadow stylesheet, so their declarations never applied. The code read correctly and the
  tests were green; only a rendered probe showed the difference.

  - **The keyboard highlight is visible on the selected row again** in `<lr-select>`, `<lr-combobox>`,
    `<lr-model-select>` and `<lr-voice-picker>`. Each had `[aria-selected="true"]` written after the
    active-descendant rule at equal specificity, so arrow-keying onto the already-selected option
    produced no visible highlight at all.
  - **`appearance="filled"` has a focus indicator again** on `<lr-combobox>` and `<lr-date-input>`.
    Both had none: the appearance rule out-ranked `:focus-within`, and the only `outline` in the focus
    rule was `solid transparent`. Both now express appearance as private custom properties, so no
    `[part]` rule can out-rank another and the failure mode is structurally impossible.
  - **Pointer feedback restored** where a state rule or a resting rule was swallowing it:
    `<lr-code-block>`'s line-gutter button (neither hover nor press, ever), `<lr-pagination>`'s page
    input, `<lr-table>`'s sticky sortable header, `<lr-time-range>`'s active preset,
    `<lr-agent-trace>`'s active handoff, `<lr-compare-panel>`'s cast vote, `<lr-flow-canvas>`'s
    selected edge, `<lr-conversation-item>`'s open session, `<lr-option>`, `<lr-entity-chip>` and
    `<lr-approval-queue>`.
  - **Focus rings restored** on `<lr-calendar>`'s today cell, `<lr-sequence-strip>`'s selected cell,
    `<lr-embedding-explorer>`'s selected point, and `<lr-dashboard-grid>`/`<lr-flow-canvas>` cells in a
    collision or drop state.
  - **`hidden` works again** where the component's own stylesheet was defeating the UA default:
    `<lr-flag>` painted a full-size broken image beside its skeleton while loading, `<lr-video>` kept
    the controls play button both painted and focusable behind a poster, and nine components let a
    consumer's `hidden` slotted child stay visible.
  - **Disabled controls look disabled**: `<lr-entity-chip>` and `<lr-approval-queue>` rendered their
    disabled buttons pixel-identical to enabled ones, with a pointer cursor and full hover feedback.
  - **`<lr-random-content>` actually hides** the candidates it is not showing; its rotation was
    previously observable only to assistive technology.
  - **`<lr-video>` keeps captions** for a `<track>` with no `kind` attribute, whose HTML missing-value
    default is `subtitles`.

  ### Additive

  - **`lr-search-change` detail is consistent again.** `<lr-terminal>` and `<lr-av-player>` now emit
    the canonical `LyraSearchChangeDetail` including `matchCountExact`, which 18 of 21 emitters already
    did. This matters most on `<lr-terminal>`, which truncates at 10,000 matches and previously had no
    way to signal that its count was a lower bound. `<lr-knowledge-graph-explorer>`'s detail is now exactly
    `{ query, matchCount, matchCountExact }` — `searchQuery` is replaced by the canonical `query`
    rather than carried beside it; it deliberately has no `activeIndex`, being a live node filter
    rather than a cursor-based search. (The `searchQuery` *property* is unaffected.)
  - **`<lr-token-input>` can veto all three mutations.** `lr-add` and `lr-token-edit` are now
    cancelable, matching `lr-remove`, which already was. A vetoed add keeps the typed draft so the user
    can correct it; a vetoed edit leaves the inline editor open with the edited text intact.
  - **`<lr-dialog>`'s close event is `lr-close`** (`DialogCloseReason` detail, cancelable);
    `<lr-drawer>` inherits it. See the removal table above.
  - **`<lr-accordion>` also emits a cancelable `lr-toggle-request`** (`{ collapsed, item }`) alongside
    its upstream-mirroring `lr-expand`/`lr-collapse`, matching the convention
    `<lr-code-block>`/`<lr-chat-message>` use. `preventDefault()` on either vetoes the transition.
  - **`<lr-popover>` gains `disabled`.** Both `<lr-tooltip>` and its own subclass `<lr-dropdown>` had
    it; the base did not. `<lr-dropdown>` now inherits it, with byte-identical behavior.
  - **`<lr-table>` emits `lr-selection-change`** when a `selectionMode` flip to `'single'` coerces a
    multi-row selection down to one key — previously a silent mutation a host mirroring the event could
    not see.
  - **`<lr-command-palette>` re-emits `focus`/`blur`** from its search input; native ones neither bubble
    nor cross the shadow boundary.
  - **`PptxViewerAdapter` and friends are importable.** `pptx-loader.js` had no `package.json#exports`
    entry despite the reference documenting the import, so it failed with
    `ERR_PACKAGE_PATH_NOT_EXPORTED`. A new check now requires every helper module to be classified
    public or internal, closing the same class that stranded `archive-viewer-register.js` in 9.0.0.
  - **`PlaceSync`** is re-exported from `dropdown.class.js`, and ~13 constituent types are re-exported
    from the composite components whose public properties use them.
  - **`<lr-knowledge-graph-explorer>` no longer announces on mount.** A preset `search-query` fired its
    live region before any user action.

### Minor Changes

- 357ee35: `<lr-chart>`: declarative reference lines and shaded bands via a new `annotations` property.

  Marking a threshold, an event year, a regime change or a highlighted period previously meant
  importing `chartjs-plugin-annotation` yourself and wiring it through the raw `config` passthrough —
  the point where a declarative component dropped the user into raw Chart.js, for one of the most
  common things anyone needs on a time series.

  - `annotations: readonly LyraChartAnnotation[]`, where `LyraChartAnnotation` is
    `{ axis?: 'x' | 'y'; value?: number; from?: number; to?: number; label?: string; tone?: 'neutral'
    | 'brand' | 'success' | 'warning' | 'danger' }`. A finite `value` renders a reference line on that
    axis; a finite `from`/`to` pair renders a band bounded on that axis and spanning the other. `axis`
    defaults to `'y'`.
  - Entries specifying neither a finite value nor a finite range are dropped rather than handed to
    Chart.js, where they render nothing at best; a reversed range is normalized rather than rejected.
  - Tones resolve through the same `getComputedStyle`-then-`resolveCanvasColor` path every other chart
    color takes, since canvas silently ignores an unparseable `strokeStyle`/`fillStyle`.
  - Labelled entries are included in the generated accessible description, mirroring `lr-heatmap`. The
    label is consumer-supplied text and so is not localized; an unlabelled line has no nameable
    meaning to announce beyond a coordinate.
  - The optional `chartjs-plugin-annotation` peer loads on first actual demand, so a page with no
    annotated charts never downloads it. Without it installed the chart still renders and a single
    console warning explains the no-op — the same fail-closed contract `data-labels` uses.

  On the filed concern about Chart.js's page-wide singleton registry: this plugin is registered
  globally, like `chartjs-plugin-zoom` and unlike `chartjs-plugin-datalabels`. The distinction is that
  datalabels draws on every dataset the moment it is globally registered, whereas annotation draws
  nothing at all unless a chart supplies annotation options — so the registration is unobservable to a
  chart that sets none, covered by an explicit test. It also *has* to be global: registration is what
  installs the plugin's own element types and defaults, and an inline `config.plugins` entry skips
  that, leaving the plugin to throw on missing `borderWidth`/`borderCapStyle` the moment it draws.
- 8e3f602: `<lr-chart>`: add a logarithmic value axis via a new `scaleType` property.

  The core loader registered `LinearScale`, `CategoryScale` and `RadialLinearScale` but never
  Chart.js's `LogarithmicScale`, so a logarithmic axis was unreachable — there was no property for it,
  and the raw `config` passthrough could not supply one either, because Chart.js rejects an
  unregistered scale type at construction. Any dataset spanning several orders of magnitude (prices,
  growth, population, latency percentiles, file sizes) could not be charted honestly, since a linear
  axis collapses everything below the maximum into the baseline.

  - `scaleType: 'linear' | 'logarithmic' = 'linear'` (attribute `scale-type`, type
    `LyraChartScaleType`, exported from the root barrel) targets the **value** axis; the categorical
    axis is never affected. Inherited by `lr-line-chart`, `lr-scatter-chart` and `lr-bar-chart`, and
    applied to the secondary `y2` axis when one is present.
  - `beginAtZero` is not forwarded on a logarithmic axis, since `log(0)` is `-Infinity` and Chart.js
    would otherwise be handed a bound it cannot place.
  - `LogarithmicScale` is registered with the core rather than behind the feature loader: unlike the
    zoom and datalabels plugins it is not a separate package, so it already ships inside the
    `chart.js` module namespace the loader imports and costs no extra download weight.

  Default is unchanged and covered by an explicit unset test.
- 744da58: Four consumer-filed defects, plus one the sweep for the same defect class turned up.

  **`<lr-checkbox-group>`: `value` is settable.** It was a getter with no setter. Reading was fine,
  but `.value=${...}` — the binding every other form control here accepts — compiles to a plain
  property assignment that `readonly` cannot catch at the binding site, so it threw
  "Cannot set property value ... which has only a getter" from inside lit-html during a *later*
  render, blaming framework internals rather than the offending line. Assigning now mirrors the array
  onto the owned checkboxes; it is controlled input, so it emits no `lr-change`, and an assignment
  made before the children exist is applied once they arrive.

  **`<lr-time-input>`: `valueAsNumber` and `valueAsDate` are settable.** Nobody filed this — sweeping
  the library for the same "public getter a consumer would naturally bind, with no setter" shape found
  it. `<lr-input>`, `<lr-date-picker>`, `<lr-slider>` and `<lr-known-date>` all ship both, and the
  native `<input type="time">` this mirrors accepts both; `<lr-time-input>` was the lone outlier.
  Out-of-range or non-finite figures clear the field rather than wrapping into a different time.

  **`<lr-map>`: `lr-map-click` resolves `feature` against `dataLayers`, not only the choropleth.**
  Clicking a shape painted through `dataLayers` reported `feature: undefined`, indistinguishable from
  clicking empty ocean — which broke the pattern the two properties invite: choropleth for features
  that have a value, a data layer for features that exist but have none. The detail gains `origin`
  (`'choropleth' | 'data-layer'`) and `sourceId` (the authored `dataLayers[].sourceId`) so a hit is
  attributable.

  **`<lr-map>`: an untileable numeric feature property is now named up front.** MapLibre GL tiles
  GeoJSON through a worker, where an oversized integer throws "Given varint doesn't fit into 10
  bytes" — uncatchable by the app, invisible except as an opaque message, and with the rest of the
  layer still painting. Sources are pre-scanned and any property beyond `Number.MAX_SAFE_INTEGER`
  draws a dev-mode warning naming the feature and property.

  **`<lr-heatmap>`: the matrix row-label gutter is configurable, and labels truncate.** It was a
  hardcoded 60px with no measurement, so a longer row label was clipped mid-word by whatever was
  painted beside it on the canvas — which reads as a rendering fault. `rowLabelWidth` now pins a
  width or takes `'auto'` to measure the widest label and size to fit (floored at 60, capped at 40%
  of the host so one label cannot squeeze out the cells it describes), `colLabelHeight` does the same
  for the column band, and a label too wide for the resolved gutter is truncated with an ellipsis
  instead of clipped. The default stays 60: auto-sizing every chart would silently reflow layouts
  whose labels already fit, which is a bigger change than the clipping it fixes.
- d92bfb2: Two dev-mode defects that shipped in 9.x, plus the per-point chart color cost behind them.

  **The unknown-attribute diagnostic no longer reports a component's own API as a mistake.**
  Components can now declare a `knownUnobservedAttributes` static for attributes they own without
  observing, and four do. Without it the diagnostic fired on correct markup and on state components
  set on themselves:

  - `<lr-page disable-sticky="header">` is documented public API read only by
    `:host([disable-sticky~="..."])` rules, so it has no reactive property — authoring it correctly
    drew a warning saying it was wrong.
  - `<lr-animated-image>` (`playing`), `<lr-menu-item>` (`submenu-open`) and `<lr-app-rail>`
    (`mode`, `dragging`) reflect read-only state onto their own host. Each reported its own output
    as an unknown attribute, in every consumer app, the moment that state turned on.

  **Per-point chart colors are resolved once per distinct color, not once per point.**
  `resolveCanvasColor` inserts a probe element and forces a synchronous style recalculation on every
  call, which `<lr-chart>` paid for each entry of a series' `color`, `segmentColors` and
  `pointColors` arrays — 2,000 probe insertions for a 2,000-point series, before drawing anything.
  The new `resolveCanvasColors` memoizes by color string across the batch, and authored ramps are
  typically a handful of distinct colors repeated across many points. The cache lives for one call,
  so a later draw still picks up live `--lr-*` theme changes.

  **`<lr-tooltip>` no longer schedules a wasted second render on close.** Its `anchorPositioned`
  reset moved from `updated()` to `willUpdate()`, where it belongs — nothing visible changes, since
  that render already hides the popup via `open`.
- 19d15f6: Five consumer-reported gaps, several of them follow-ups to the charts/timeline work in this release.

  **`<lr-chart>`: the formatter now receives the `export` and `spoken` surfaces.** `LyraChartFormatSurface`
  has always declared both and `<lr-lite-chart>` has always emitted them, but `lr-chart` only ever
  passed `visual` and `table` — so one formatter written against the documented contract behaved
  differently depending on which chart rendered it, silently, in exactly the places unit formatting
  matters most. CSV cells now route through `export` and the live announcement through `spoken`. With
  no formatter installed, CSV cells stay the raw machine-readable number (no locale grouping a
  spreadsheet would misparse) and announcements keep their locale format.

  **`<lr-map>`: choropleth interpolation is selectable.** The fill expression was hard-coded to
  `['interpolate', ['linear'], …]`, so a heavy-tailed quantity — price, population, income — put every
  value below the maximum into the first colour band. `LyraMapChoroplethLayer.interpolation`
  (`'linear' | 'logarithmic'`, default `'linear'`) emits maplibre's own
  `['interpolate', ['exponential', 0.25], …]`, exposing an existing capability rather than adding one.
  **`stops` stay in the data's own units**, so the legend keeps reading in real values instead of log
  units.

  **`<lr-heatmap>`: a dev-mode warning when `legendStops` and `colorSteps` disagree.** Both are
  deliberate and independent — that independence is what lets a `cellColor` consumer describe a ramp
  the grid no longer uses — but nothing checked they described the same thing, and a legend that
  confidently labels colours the cells never use is worse than no legend. Warning rather than deriving
  one from the other: deriving would silently change what an existing `colorSteps`-only consumer sees
  and would break that escape hatch. Caption-only stops (the `less ▢▢▢▢ more` shape) claim no colour
  and never warn.

  **`<lr-timeline>`: `collision="stack"` for dense `scale="time"` chronologies.** Coincident items
  overlapped, which is the common case rather than the exception at realistic density. `'stack'` steps
  each colliding item one lane along the cross axis (`--lr-timeline-collision-offset`); an isolated
  item returns to lane 0 rather than inheriting a preceding run's depth. No `'cluster'` mode: collapsing
  items into one expandable marker needs a selection model and click events this deliberately passive
  component does not have.

  **`<lr-sequence-strip>`: activation and a controlled selection.** The strip read as pickable but had
  no click handling and no event to hook. `lr-item-activate` (`detail: { index, id, item }`) fires on
  click and on Enter/Space at the roving-tabindex focus, and `selectedIndex` marks the current item
  with `aria-current` and `data-selected`. Controlled on purpose: activation does not move the
  selection itself, so the strip cannot drift from a playback index it does not own. The selection is
  drawn as a ring, not a tint — a cell's background is data (its category colour).

  All five are additive; unset, every component renders as before.
- b3b9d30: `<lr-flag>`: accept ISO 3166-1 alpha-3 country codes, and render a neutral fallback for codes that
  cannot resolve.

  Two related consumer reports.

  **Alpha-3.** `country` took alpha-2 only, while public statistical sources — World Bank, UN, IMF and
  most open-data portals — key country records on alpha-3, so every consumer plotting country-level
  data shipped and maintained its own ~249-row conversion table purely to satisfy this component.
  `country` now accepts either: length alone disambiguates the two code spaces, so no format hint or
  new API is needed. The 249 officially-assigned mappings are packed as a ~1.2 KB fixed-width string
  and expanded into a lookup lazily on the first alpha-3 use, so an alpha-2-only app never pays for
  them. Withdrawn and user-assigned codes deliberately do **not** map to a successor state — a
  dissolved federation has no current flag, so it takes the unresolved path below.

  **Unresolved ≠ error.** An unresolvable code rendered localized error text into `[part="error"]` and
  reflected `data-error`. That is right for a genuine mistake, but historical and longitudinal
  datasets legitimately contain states with no current ISO code, and in a table or card grid those
  rows want a neutral placeholder occupying the same footprint, not wording that reads to a user as a
  bug. Styling `[part="error"]` could not fix it, because the localized string is contained text
  rather than substitutable content.

  - A new `fallback` slot renders in place of the flag for an unresolvable code, and a `fallback`
    property takes a placeholder image URL (rendered as `[part="fallback-image"]`) when no slot
    content is supplied.
  - The host now reflects `data-unresolved` separately from `data-error`, so the two cases can be
    styled apart.

  Both additive: a resolvable code renders exactly as before, covered by an explicit inert-by-default
  test.
- 990f4d6: `<lr-heatmap>`: support signed data via new `domain` and `midpoint` properties, and stop dropping
  the negative half of a signed dataset.

  Two related reports. The ramp always spanned the data's own `min`…`max`, so two heatmaps of
  comparable data could not share a scale — each silently normalized to its own extremes — and a
  diverging palette could not be centred: with data running -4.93 to +28.8, the neutral colour landed
  at 15% of the range rather than on zero, painting "no change" onto a substantial decrease.
  Separately, the cell-fill guard was `value < 0 || !Number.isFinite(value)`, so *every* negative
  rendered as no-data, not just the documented `-1` sentinel — indistinguishable from a genuinely
  missing cell, and silent (32.7% of cells in the reporter's dataset).

  - `domain?: [number, number]` pins the ramp's input domain, so comparable charts can share a scale.
    A reversed pair is normalized; a degenerate or non-finite one falls back to the derived range.
  - `midpoint?: number` anchors a diverging ramp's neutral colour, scaling the two halves
    independently (`lo`→0, `midpoint`→0.5, `hi`→1). A midpoint outside the domain degrades to plain
    normalization rather than distorting the ramp.
  - Setting either one opts into **signed data**, where only a non-finite value is no-data. That
    gating is deliberate: `-1` is the long-documented sentinel and a matrix of counts has no
    meaningful negative, so declaring a domain or midpoint is what disambiguates the two. With
    neither set, behavior is byte-identical to before — covered by an explicit unset-regression test.
  - A structurally absent matrix cell now reads as `NaN` in signed mode (non-finite is no-data in
    both modes), so it stays a hole while a real `-1` beside it renders on the ramp. The default
    mode still resolves an absent cell to `-1`, keeping `valueAt()` and the `lr-cell-click` payload
    unchanged.
  - The accessible cell labels track the painted contract, so a rendered negative is announced with
    its value instead of "no data".
  - `scale="sqrt"` continues to reject negatives — a square root of a negative has no meaning — now
    explicitly rather than as a side effect of the shared guard.
- 17b540a: `<lr-lite-chart>`: add a base-10 `scale="logarithmic"` value axis.

  The dependency-free SVG chart does not extend `LyraChart`, so it did not inherit the `scaleType`
  support added for the Chart.js-backed charts, leaving no way to plot data spanning several orders of
  magnitude honestly — a linear axis collapses everything below the maximum into the baseline.

  - `scale` now accepts `'logarithmic'` alongside `'linear'` and `'sqrt'`, defaulting to `'linear'`.
  - Unlike `'sqrt'` (which compresses bars only, by long-standing design), the logarithmic axis
    applies to **bars, line points and gridlines alike** — a log axis whose gridlines stayed linear
    would misrepresent the plot. All three now resolve through one `valueFraction()` dispatcher so the
    scale can never apply to some marks and not others.
  - Its lower bound is the smallest *positive* datum, not the linear `lo`. `beginAtZero` defaults to
    true, so `lo` is normally `0`, which has no logarithm; deriving the floor from the data is what
    makes a 1…1000 series span three even decades instead of collapsing onto one. Measured: decade
    gaps of 80.7/80.6/80.7px versus linear's 2.2/21.8/217.8px on the same data.
  - Zero and negative values pin to the axis floor rather than reaching the SVG as `-Infinity`, which
    would blank the series — this renderer has no Chart.js-style "drop the point" fallback. A
    degenerate domain falls back to the linear fraction.

  `'linear'` and `'sqrt'` render exactly as before, covered by an explicit unchanged-default test.
- 1b0aa52: `<lr-map>`: render a continuous choropleth legend, via a new `legendGradient` property and a
  `legend` slot.

  `choropleth` builds an interpolated fill expression from `stops` — a continuous ramp — but `legend`
  accepted only `{ color, label, pattern }` rows rendered as discrete swatches, and the component
  exposed no slots. The standard key for a choropleth (a gradient bar with endpoint ticks) could not
  be rendered inside the component that produces the gradient, so a consumer had to draw a second,
  unaligned legend outside the map and keep its stops manually in sync with the layer's.

  - `legendGradient: readonly (readonly [number, string])[]` takes the same `[value, color]` shape as
    `choropleth.stops`, so the usual assignment is `map.legendGradient = myChoropleth.stops` and the
    key cannot drift from the layer it describes. Stops are sorted ascending, bounded to 64, and
    filtered to finite values with a CSS-parsable color; fewer than two usable stops render no bar,
    since a one-stop "gradient" is a flat block that describes nothing. Each stop sits at its true
    proportion of the value range rather than being evenly spaced.
  - `legendGradientLoLabel` / `legendGradientHiLabel` override the endpoint captions, which otherwise
    default to the lowest/highest stop value in the component's own locale-aware formatting.
  - New `legend-gradient`, `legend-lo` and `legend-hi` parts, named to mirror `lr-heatmap`'s gradient
    legend as the request asked, so one styling vocabulary covers both. The bar is `aria-hidden` and
    `inert`; the captions carry the meaning. It mirrors under RTL like the heatmap's does.
  - A new `legend` slot renders custom legend content inside the panel's own layout, so it stays
    positioned with the map. Supplying it opens the panel even when both legend inputs are empty.

  All additive: with none of them set the component renders exactly as before, covered by an explicit
  unset test.
- 1625356: Ship an optional `reservations.css` stylesheet that prevents layout shift from lazy-upgrading
  elements, and document the library's scope boundaries.

  An undefined custom element is an inline box with no intrinsic size, so every `lr-*` in the initial
  viewport contributes a reflow as its definition loads; components that additionally defer on an
  optional peer (`lr-chart`, `lr-map`, `lr-flag`, `lr-flow-canvas`, `lr-knowledge-graph-explorer`)
  can cost a second shift when the peer resolves. Each is individually well-behaved — the aggregate on
  a first paint is what costs a Cumulative Layout Shift score. Until now every consumer derived its own
  `:not(:defined)` sizing rules per component by measurement, and those rules rotted silently whenever
  a component's default dimensions changed.

  ```css
  @import "@aceshooting/lyra-ui/reservations.css";
  ```

  - Reserves each component's intrinsic footprint before upgrade, styling **only** `:not(:defined)`
    elements inside an `@layer lr-reservations`, so it is inert the moment a definition upgrades and
    can never fight a component's own layout. No colors, no `:root` rules.
  - Every reservation is expressed with the **same custom property and fallback token the component's
    own stylesheet uses** (`--lr-chart-height`/`--lr-size-280px`, `--lr-flag-aspect-ratio`,
    `--lr-form-control-height`, …). That is what makes it worth shipping rather than documenting
    measured pixels: the reservations track the components, and theming a component re-themes its
    reservation with it.
  - Reservations target each component's *final* default size rather than its skeleton's, so a
    skeleton-to-content swap stays stable too.

  `llms/shared.md` gains a matching CLS section with the hand-rolled equivalent for consumers who
  prefer their own rules, plus a new **Scope: what this library does not provide** section stating the
  boundaries explicitly — client-side routing (there is no router and no route outlet; the navigation
  components expose active state as ordinary properties to be driven by the application's own router),
  data fetching/state management, and form-submission orchestration.
- 870ed4f: `<lr-switch>` and `<lr-checkbox>`: expose the row wrapper as a new `row` CSS part.

  Both controls render the track/box owner and the rich label as *siblings* inside a wrapper element
  that carried no `part` at all, while `base` names the owner box rather than the row. A consumer
  laying out a column of switches therefore had no selector for "the row": `inline-size: 100%` on any
  part inside it resolved against a shrink-to-fit parent, and because the owner box centers its track
  and its width tracks the label's, a longer label shifted the track's x-position from row to row —
  visibly ragged.

  `row` names the real wrapper, so `::part(row)` can stretch or align it. `base`/`switch`/`wrapper`
  and `base`/`checkbox` keep their existing nodes and meaning — they are documented Web Awesome /
  Shoelace compatibility names, so repointing them would have broken shipped consumers. This is
  purely additive; an unstyled control renders identically.
- 8cb3545: Four more consumer-filed defects, two per component.

  **`<lr-table>`: cell links are themeable.** A column's `cell(row)` renders its TemplateResult inside
  the component's shadow root, so an anchor it returns is unreachable from page CSS — and `::part()`
  cannot select past the first compound selector to reach it either. It computed to the UA default
  link blue, the one colour on the page belonging to no design system. Cells now take
  `--lr-table-cell-color`, and a cell anchor takes `--lr-table-cell-link-color` (brand by default)
  plus `--lr-table-cell-link-hover-color`. `:where()` keeps specificity at zero so an inline style on
  the returned anchor still wins, and `revert` hands the UA default back.

  **`<lr-table>`: `scroll-mode="page"` makes an uncapped table's sticky header work.** `[part="base"]`
  was unconditionally `overflow: auto`, which makes it the sticky containing block for the header
  whether or not anything can scroll in it — so with no `--lr-table-max-height` the header scrolled
  away with the page, and an uncapped page-scrolling table and a pinned header were mutually
  exclusive. That is a real CSS constraint rather than an oversight: a scroll container clips *both*
  axes. The fix is therefore an explicit opt-in, not a changed default, since dropping the overflow
  unconditionally would cost every uncapped wide table its horizontal scrolling.

  **`<lr-map>`: a guarded `maxBounds`.** Calling `map.setMaxBounds()` through the `.map` escape hatch
  can wedge maplibre-gl at a sub-1 fractional zoom in a wide container: `getZoom()` returns `null`
  permanently, every frame throws from inside the peer's matrix math, and the canvas never paints
  again — a blank map, with nothing thrown at the call site. The property applies the same call, reads
  the camera back, and reverts if it did not survive, so the worst case is an unconstrained map plus a
  dev-mode warning.

  **`<lr-map>`: property-only choropleth updates no longer re-tile the whole source.** `setData()`
  re-tiles unconditionally, which is invisible on a static map and expensive on an animated one. When
  an update changes only feature properties, the component now emits maplibre-gl's incremental
  `updateData()`. The fast path requires the same feature count, an addressable `id` per feature, and
  geometry that is the *same object* as last time — a deep compare would cost about what the re-tile
  costs, and a false positive would paint stale geometry. Anything else falls back to `setData()`.
- 10b7d14: `<lr-timeline>`: position items along a real time axis with the new `scale="time"` mode.

  The timeline was an evenly-spaced sequence in which `timestamp` was rendered as text but never used
  for placement, so a chronology spanning a long period lost the main thing a timeline conveys — two
  events weeks apart and the next decades later all looked equidistant, and the shape of the history
  was invisible.

  - `scale: 'flow' | 'time' = 'flow'` (type `LyraTimelineScale`, exported from the root barrel).
    `'flow'` is today's layout, unchanged and still the default. `'time'` positions each item at its
    true proportion of the range.
  - `rangeStart` / `rangeEnd` pin the axis instead of deriving it from the earliest and latest items;
    a reversed or non-finite pair falls back to the derived range.
  - `--lr-timeline-time-extent` (default `var(--lr-size-20rem)`) sets the distance to distribute
    along — `block-size` when vertical, `inline-size` when horizontal. Items are absolutely
    positioned, and a percentage against an auto-sized track would resolve to zero.
  - An item with no parseable `timestamp` — including one supplied only through the `timestamp` slot,
    which carries no machine-readable instant — keeps document order and is spread evenly, so a
    partially-timestamped list degrades instead of stacking every unknown at the origin.
  - Positions are written to each child as a private `--_lr-timeline-item-offset` custom property and
    removed again when switching back to `'flow'`, so the component still never alters its children's
    content or structure.

  Scope note: this covers the request's preferred option. Items sharing an instant overlap rather than
  being fanned into lanes — the denser case (parallel lanes by category, a brushable/zoomable range,
  per-event click events, collision handling) would change this component's deliberately passive,
  zero-event contract, so it belongs in a sibling component with its own design, not here.

### Patch Changes

- c174d2d: Fix three defects found while auditing test coverage:

  - `<lr-chat-message>`: with `actions-position="outside"`, the slotted actions row is a sibling of
    the bubble rather than a flex item nested inside the footer, so the footer's role-conditional
    auto-margin alignment became a no-op (a box that already fills its container has no spare space
    for `auto` margins to distribute). A user turn's actions stayed pinned to the inline-start edge
    instead of aligning to the inline-end edge next to its own right-aligned bubble. Now aligned via
    `justify-content` on the actions row itself.
  - `<lr-file-input>`: the dropzone collapsed to its own intrinsic content height instead of filling
    a host given a definite block size (e.g. absolutely positioned with `inset: 0` over a sized
    panel) — none of `[part="form-control"]`, `.dropzone`, or `[part~="base"]` propagated the host's
    height down the chain.
  - `<lr-chart>`: a chart whose row count exceeded the 1,000-record rendering budget but whose series
    count did not got its shared `labels` array correctly sampled down, but each series' own
    `data`/`color`/`pointRadius`/`pointColors`/`segmentColors` arrays stayed at full source length —
    a length mismatch handed straight to Chart.js. Row sampling now applies to every series
    regardless of whether the series dimension itself also needed sampling.
- bf447ca: `<lr-tooltip>`: close a pointer-held tooltip when a re-render replaces its trigger.

  A list that re-renders — a chat transcript, a log view, anything virtualized — replaces the `for`
  target with a fresh node rather than moving the existing one. The outgoing element is detached
  before it can fire the `mouseleave` that normally closes a resting tooltip, and the incoming element
  is not necessarily under the pointer. `adoptTrigger()` correctly rebound its listeners to the new
  node but let the tooltip inherit the outgoing node's open state, so the tooltip hung open over a
  trigger nobody was pointing at. Reported live as several resting tooltips visible at once with the
  pointer over none of them, via `<lr-copy-button>`'s default `tooltip="full"`.

  A trigger swap now re-derives the open state from the incoming element: the tooltip stays open only
  while that element is genuinely held — the pointer resting over it (`:hover`) or focus inside it —
  and closes otherwise. Focus-, click- and `manual`-opened tooltips are untouched, and re-rendering a
  row the pointer still rests on leaves its tooltip alone. Verified on Chromium, Firefox and WebKit.

  The same report's secondary note about a tooltip being clipped inside a scroll container is existing
  behavior with existing API: pass `hoist` (`<lr-copy-button>` already forwards it to its tooltip) to
  render the popup in the top layer and escape the clipping ancestor.

## 9.1.1

### Patch Changes

- 3de3498: Document 18 additive public surface additions from 9.0.0 that had no changelog entry:

  - `<lr-chip>`: new `end` slot (trailing content, typically an icon, after the label).
  - `<lr-claim-evidence>`: new `compact` and `frame` properties.
  - `<lr-code-editor>`: new `size` property.
  - `<lr-ebook-viewer>`, `<lr-pptx-viewer>`, `<lr-spreadsheet-viewer>`: new `maxHeight` property on
    each.
  - `<lr-token-input>`: new `start` and `end` adornment slots.
  - New CSS custom-property indirection (a themeable `--lr-*` hook backing a previously
    hardcoded/token-only value) on `<lr-dock-panel>`, `<lr-retrieval-compare>`,
    `<lr-spreadsheet-viewer>`, `<lr-stream-status>`, `<lr-code-block>`/`<lr-code-block-core>`,
    `<lr-page-rail>`, and `<lr-pdf-viewer>`.

  All 18 are additive and backward-compatible — nothing removed or renamed, no behavior change when
  left unset — but none were individually called out in the 9.0.0 changelog entry, unlike the many
  other opt-in additions from the same release that are documented by exact component/property name.
- d04b07e: Document six cancelable pre-mutation events added in 9.0.0 with no changelog entry:
  `<lr-dock-panel>`'s `lr-collapse-request`, `<lr-widget>`'s `lr-collapse-request`,
  `lr-fullscreen-request`, and `lr-view-request`, `<lr-page>`'s `lr-nav-toggle`, and
  `<lr-split-panel>`'s `lr-reposition-request`.

  9.0.0 added a consistent propose-then-commit event pair to several components that previously
  only fired a single post-commit notification: a new cancelable `*-request` event fires first with
  the proposed next state, and a consumer's `preventDefault()` on it now vetoes the change before
  the existing non-cancelable `*-change`/completion event fires. `<lr-dock-panel>` gained
  `lr-collapse-request` alongside its existing `lr-collapse-change`; `<lr-widget>` gained its own,
  independent `lr-collapse-request` (alongside `lr-collapse-change`) plus `lr-fullscreen-request`
  and `lr-view-request` (alongside `lr-fullscreen-change`/`lr-view-change`); `<lr-page>` gained
  `lr-nav-toggle`, its first event of any kind; `<lr-split-panel>` gained `lr-reposition-request`
  alongside its existing `lr-reposition` post-commit event. All six are genuine new opt-in public
  API — a consumer can now veto a collapse, fullscreen, view, nav-open, or divider-reposition
  mutation before it commits — but none were called out in the 9.0.0 changelog entry, unlike the
  many other opt-in additions from the same release that are individually documented by name.
- 7bcef3e: Document further additive 9.0.0 public surface that had no changelog entry, found auditing the
  two largest 9.0.0 remediation commits:

  - `<lr-context-inspector>`: five new events — `lr-error`, `lr-copy-error`, `lr-export-error`,
    `lr-show`, `lr-hide` (all from its embedded copy/export controls).
  - `<lr-graph>`: eight categorical fallback CSS custom properties, `--lr-graph-cat-1` through
    `--lr-graph-cat-8`, backing the default node-type color palette.
  - `<lr-tag>`: new `lr-remove` event (non-cancelable notification that the remove button was
    activated).
  - `<lr-rating>`: new `focus`/`blur` native-passthrough events, `focus()`/`blur()`/`click()`
    methods, and `base`/`rating` csspart compatibility aliases (same node, two names).
  - A long tail of new, narrowly-scoped CSS custom properties (visual tokens only, no new
    interaction surface) on `<lr-activity-feed>`, `<lr-prompt-studio>`, `<lr-task-list>`,
    `<lr-tool-approval-dialog>`, `<lr-tool-param-form>`, `<lr-push-to-talk>`, `<lr-flow-controls>`,
    `<lr-menu-item>`, `<lr-chip-group>`, and further `<lr-rating>` properties; plus new slot aliases
    on `<lr-prompt-input>` (`start`/`leading`/`end`/`trailing`) and `<lr-push-to-talk>`
    (`microphone-icon`/`icon`), and new cssparts on `<lr-model-select>`, `<lr-push-to-talk>`, and
    `<lr-source-picker>`.

  All additive and backward-compatible — nothing removed or renamed, no behavior change when left
  unset.
- d59f8c5: Fix `<lr-button>`'s start/end adornments claiming a 40%-of-row flex-basis instead of just being
  capped at 40%.

  A 9.0.0 change gave `[part~="start"]`/`[part~="end"]` `flex: 0 1 40%`, which sets the flex
  *basis* to 40% of the button's own internal row -- a preferred size the flex algorithm tries to
  honor before shrinking -- not merely `max-inline-size: 40%`'s ceiling. Because the basis is
  self-referential (relative to the button's own internal row, unrelated to its position in the
  page), even a small icon claimed a 40% preferred share before shrinking, squeezing
  `[part="label"]`'s `flex: 1 1 auto` below what its text needed and ellipsizing labels that had
  room to spare, with visible unused space left in the row. Adornments now use `flex: 0 0 auto`
  (content-sized); `max-inline-size: 40%` remains as the actual cap for a genuinely oversized
  adornment.
- f7de4a5: Fix the same `overflow-wrap: anywhere` mid-word-break defect already fixed across seven other
  components (see the `overflow-wrap-anywhere-sibling-components` and `switch-label-break-word`
  changesets) in `<lr-card>` too — a straggler that remediation pass missed. Both `[part="body"]`
  and a slotted `[slot="header"]` collapsed their min-content contribution to near nothing while
  sitting as a flex item next to a non-shrinking sibling, splitting an ordinary short word mid-
  syllable instead of wrapping at the space before it. `overflow-wrap: break-word` gives the
  identical last-resort rescue for a genuinely unbreakable long token without that regression.
- e57c135: Fix `<lr-chat-message>`'s `[part='actions']` pinning its footer actions to the inline end
  regardless of `message-role`, detaching an assistant/system turn's copy/regenerate controls from
  their own start-aligned (and often transparent-background) bubble. `[part='actions']` now scopes
  its `margin-inline-start: auto` to `message-role="user"` and adds the mirrored
  `margin-inline-end: auto` for `assistant`/`system`, matching the role-conditional alignment
  `[part='bubble']` already uses. `actions-position="outside"` is unaffected for every role.
- f7de4a5: Fix `<lr-checkbox-group>` occasionally leaking a child `<lr-checkbox>`'s own raw `lr-change`
  (`{checked, value}`-shaped detail) to an ancestor listener, ahead of the group's own translated
  `lr-change` (`{value: string[]}`-shaped detail) — two events instead of one, the first the wrong
  shape. `onChildEvent`'s `stopImmediatePropagation()` only protects a listener that runs *after* it;
  the internal listener was registered on the default bubble phase in `connectedCallback()`, which
  only outraces a consumer's *own* bubble-phase listener when that listener happens to be registered
  later. A Lit `@lr-change=${...}` template binding — the common case — attaches its listener while
  the element is still a disconnected fragment, before `connectedCallback` ever runs, so it saw the
  unstopped child event first. The internal listener now runs in the capture phase instead, which
  always completes before any bubble-phase listener on the same node fires, regardless of
  registration order.
- 7d2ad99: Fix `<lr-dashboard-grid>`'s auto-created default `<lr-widget>` cell tripping the dev-mode
  unknown-attribute diagnostic. The component marked its own library-created default cell with a
  plain `cell-id` attribute — the same name used for the public, author-facing routing attribute a
  consumer writes on their own light-DOM children (`<div cell-id="a">`), but `cell-id` isn't (and
  shouldn't be) a real `<lr-widget>` property, since `lr-widget` is a general-purpose component with
  no concept of dashboard-grid cells. The auto-created default cell now carries `data-cell-id`
  instead — internal bookkeeping through the universally dev-mode-exempt `data-*` prefix, consistent
  with the existing `data-dashboard-grid-default-cell` marker on the same element — while
  author-authored content continues to use the public `cell-id` attribute unchanged.
- 7a03421: Add a dev-mode console warning when an `lr-*` element is connected with an attribute it doesn't
  observe.

  A typo'd or renamed attribute previously failed silently: the browser stores it inertly, the
  component keeps rendering its default, and nothing signals the mismatch, in any environment. In
  development only -- gated on Lit's own dev-mode signal (`globalThis.litIssuedWarnings`, already
  populated whenever a consumer's bundler resolves `lit`'s `development` build, exactly as it
  already does for Lit's own dev-mode warnings) -- each `lr-*` component now warns once per
  `(tag, attribute-name)` for an attribute outside its observed set, with a did-you-mean suggestion
  when a close match exists: `` `<lr-lite-chart>: unknown attribute 'hide-axis' — did you mean
  'without-value-axis'?` ``. Global HTML attributes (`class`, `id`, `style`, `hidden`, `slot`,
  `part`, ...), `data-*`, and `aria-*` are always exempt. No production behavior change -- the
  check is fully inert when Lit's own dev-mode signal isn't present.

  Scoped to attributes only; an unrecognized `.property =` write is not detected (there is no safe
  way to intercept it generically without either enumerating instance properties -- which floods
  false positives against this codebase's extensive use of TypeScript's `private` keyword for
  internal state -- or wrapping every instance in a Proxy, which cannot intercept parser-driven
  custom-element upgrades).
- e57c135: Fix `<lr-heatmap>` silently substituting the built-in fallback ramp color whenever
  `--lr-heatmap-scale-lo`/`-hi` (or a `colorSteps` entry) was set to a modern CSS color function --
  `color-mix()`, `oklch()`, `lab()`, `color(display-p3 ...)`, etc. -- with no warning. `resolveRgb()`
  previously re-parsed the canvas's `ctx.fillStyle` read-back as a string (hex or `rgb()`/`rgba()`
  only), which neither recognizes the `color(srgb r g b [/ a])` form Chromium normalizes
  `color-mix()` to, nor the literal `oklch()`/`lab()`/`color(display-p3 ...)` syntax canvas
  round-trips as-is for those functions. It now falls back to reading the actual rendered pixel back
  via `getImageData(0, 0, 1, 1)` -- the same idiom already used in `theme.ts`/`shiki-dark-theme.ts`/
  `color-core.ts` -- resolving any CSS color syntax the canvas accepts instead of only the forms a
  hand-written parser recognizes. A genuinely invalid color string is unaffected: it still triggers
  `warnInvalidColor()` and falls back.
- 3a9ae9d: Fix `<lr-knowledge-graph-explorer>`'s composed legend starving the graph pane when `nodeTypes` is long.

  The explorer's flex column gives `[part='graph']` `flex: 1 1 auto; min-block-size: 0` so it's the
  one part designed to shrink, but `[part='legend']` had no size cap — browser-default flex-item
  sizing floors it at its full content height, so a `nodeTypes` list long enough to exceed the
  host's allocated height pushed 100% of the shrinkage onto the graph pane instead, silently
  ignoring the documented `height` property. `[part='legend']` now caps at `var(--lr-size-12rem)`
  and scrolls internally past that, matching the existing `[part='search-results']` pattern in the
  same stylesheet.
- fa7b8a1: Fix `<lr-lightbox>`'s caption starving the stage when it's unusually long.

  `[part='stage']` is `flex: 1 1 auto; min-block-size: 0` — the one part designed to shrink — but
  `[part='caption']` had no size cap, so an unusually long caller-supplied caption could floor at
  its full multi-line content height and squeeze the stage's allocation. `[part='caption']` now
  caps at `var(--lr-size-8rem)` and scrolls internally past that. Same mechanism, same fix shape, as
  `<lr-knowledge-graph-explorer>`'s composed-legend fix in this same release.
- 53ad948: Raise the optional `marked` peer dependency's lower bound to `^18.0.10` (was `^18.0.9`), picking
  up an upstream patch release. Affects every Markdown-rendering component that declares `marked` as
  an optional peer: `lr-agent-workspace`, `lr-dashboard-grid`, `lr-eval-run`, `lr-markdown`,
  `lr-markdown-core`, `lr-message-parts`, `lr-notebook-viewer`, `lr-rag-answer`,
  `lr-streaming-text`, and `lr-widget-renderer`.
- f7de4a5: Fix `<lr-multi-split>`'s `'floating'` collapse state requiring `!important` to override its
  drawer's `position`/`inset-block`/`inset-inline-start`/`inset-inline-end`. All four were applied
  as owned *inline* styles — always higher cascade priority than any external stylesheet rule,
  regardless of specificity — even though their floating-state value is always the same fixed
  literal (`absolute`, `0`), never per-render computed data. They're ordinary (overridable)
  stylesheet rules now, keyed off the already-reflected `collapse` host attribute and the panel's
  existing `data-collapse-state="floating"` marker, so a consumer's own CSS wins at normal
  specificity. `flex`/`order`/`inline-size` are unaffected and stay inline: `inline-size` in
  particular is intentionally live, mirroring the panel's own draggable `sizes` percentage so there's
  no visual jump un-floating — a consumer wanting a different floating *width* should set `.sizes`
  rather than override the stylesheet rule.
- 961987b: Document `<lr-multi-split>`'s 9.0.0 behavior change: leaving a non-floating collapse state now
  actually clears `open`, a change that shipped without a changelog entry.

  Before 8.2.3, the component reference already promised: "Leaving 'floating' while `open` is still
  `true` also closes it, the same way `<lr-app-rail>` closes its mobile overlay when leaving
  'mobile' while open." 8.2.3's compiled class never implemented it — there was no assignment
  clearing `open` anywhere in the collapse path; `this.open = false` appeared only as the property
  initializer.

  9.0.0 implemented it, in `applyEffectiveCollapseTransition`: for any transition to a state other
  than `'floating'`, `open` is now cleared. The direction of the fix was correct — the code now
  matches what was always documented — but it shipped silently, and the reference read identically
  in both versions since it described the intended behavior all along, giving no changelog signal
  to grep for.

  The ordering matters to any `lr-multi-split-collapse-change` handler that reads `open`: the clear
  happens **after** the event fires, not before, so a listener reading `this.open` synchronously
  inside its own handler still sees the pre-clear value.

  This is the same omission class already retro-documented twice in 9.1.0 (the heatmap
  flat-property-to-`data` collapse, and the tab group's removed `slot`/`label` child model).
- 22056b1: Fix the same `overflow-wrap: anywhere` mid-word-break defect just fixed in `<lr-switch>` (see the
  sibling `switch-label-break-word` changeset) in six more components, found by auditing the rest of
  the library for the same `overflow-wrap: anywhere` + `min-inline-size: 0` fingerprint on
  natural-language text: `<lr-agent-eval-dashboard>` (heading and run-label text),
  `<lr-realtime-session>` (status text), `<lr-spinner>` (the after-placement label),
  `<lr-schema-viewer>` (name/description/issue text), `<lr-subagent-panel>` (label/task/model text),
  and `<lr-callout>` (content/message text). Same root cause and fix in every case:
  `overflow-wrap: break-word` gives the identical last-resort rescue for a genuinely unbreakable
  long token without collapsing normal min-content sizing, so ordinary text now only wraps when it
  truly cannot fit, and wraps at a word boundary when it does.
- efb4b9b: Fix `<lr-popover>` and `<lr-tooltip>` getting stuck visible and interactive after closing.

  Both components drove their popup's `data-hidden` attribute through a Lit declarative template
  binding *and* an imperative direct DOM write to the same attribute, keyed off a plain
  non-reactive private field (`anchorPositioned`). The imperative write silently desynced Lit's own
  dirty-check cache for that attribute part; because neither component's `updated()` lifecycle hook
  repositions on close (only while `open`), a later close transition could evaluate the same
  boolean expression to a value matching Lit's stale cache and skip the DOM write entirely — leaving
  the popup visually and interactively present (`pointer-events: auto`) after every dismissal route
  (trigger click, outside click, Escape, `.hide()`) once it had opened once. `anchorPositioned` is
  now a real reactive `@state()` property in both classes, and the redundant imperative writes are
  removed, making Lit's own render cycle the single source of truth for the attribute.
- 34c12fa: Harden `<lr-popup>` against the same imperative/declarative attribute-write desync just fixed in
  `<lr-popover>`/`<lr-tooltip>`.

  `<lr-popup>` shared the identical pattern (`anchorPositioned` as a plain non-reactive field, an
  imperative `toggleAttribute` write alongside a declarative template binding for the same
  `data-active`/`data-awaits-position` attributes) but never exhibited the observable bug, because
  its `updated()` lifecycle hook unconditionally repositions on every update cycle regardless of
  which property changed -- masking any stale-cache skip with a redundant imperative correction on
  the same cycle. `anchorPositioned` is now a real reactive `@state()` property here too, removing
  the fragile reliance on that masking behavior.
- 03fd04f: Fix `<lr-switch>`'s label/hint/error text breaking mid-syllable, and possibly wrapping, well
  before it runs out of room.

  The shared `[part="form-control"], [part="label"], [part~="hint"], [part="error"]` rule used
  `overflow-wrap: anywhere`, which -- unlike `overflow-wrap: break-word` -- also collapses the
  element's min-content contribution to essentially a single character. Combined with the same
  rule's `min-inline-size: 0`, an ordinary short label could be squeezed far below its longest
  word's width and forced to split it mid-syllable, even when there was ample room to sit on one
  line or wrap cleanly at a space.

  Switching to `overflow-wrap: break-word` alone regressed the pre-existing 320px unbreakable-token
  test: without a width propagated down to it, `.switch-layout` (an `inline-flex` box with no
  explicit size) falls back to shrink-to-fit sizing, which can never size narrower than its own
  min-content -- and `break-word` (correctly) keeps that min-content at the token's full width, so
  the layout overflowed its ancestor instead of shrinking into it. Adding `max-inline-size: 100%` to
  both `:host` and `.switch-layout` propagates an ancestor's real constraint all the way down to the
  flex layout, fixing the overflow. `min-inline-size: 0` was deliberately *not* added to either of
  those two rules: leaving their automatic minimum size content-based means an outer flex/grid
  ancestor (e.g. a settings-panel row with another sibling control) won't disproportionately squeeze
  the switch below its longest word's width the way `overflow-wrap: anywhere`'s near-zero min-content
  let it -- at the cost of the row overflowing slightly rather than breaking a word, which is the
  tradeoff `break-word` intends.
- f7de4a5: Fix `<lr-thread-list>`'s exported `row-start`/`row-actions` parts sitting on the row's inline text
  baseline (adding descender strut height above and below) instead of vertically centering their
  `renderStart`/`renderActions` adornment content. Both parts are plain `<span>`s and default to
  `display: inline`; they are now `display: inline-flex; align-items: center`, matching every other
  adornment slot in the library. `row-content`/`row-meta`, which hold real text, are unaffected.

## 9.1.0

### Minor Changes

- b027f44: Re-export `LyraNodeTypeStyle` from every component module whose public API types a property
  against it (`lr-graph`, `lr-knowledge-graph-explorer`, `lr-drilldown-panel`, `lr-agent-trace`,
  `lr-entity-dossier`, `lr-entity-card`, `lr-memory-panel`, `lr-provenance-panel`,
  `lr-graph-legend`). The type was previously only reachable from the package root barrel
  (`@aceshooting/lyra-ui`'s `LyraNodeTypeStyle` export); a consumer importing one of these
  components from its own granular subpath, as this library's own examples do, had no local type
  to import against and had to either duplicate the shape by hand or reach into the disallowed
  `internal/` path.
- d8fe77e: Restore `./components/viewers/archive-viewer/archive-viewer-register.js` and
  `./components/viewers/ebook-viewer/ebook-viewer-register.js` as importable package subpaths. Both
  files register a `<lr-document-viewer>` renderer (`application/zip`/`.zip` and
  `application/epub+zip`/`.epub` respectively) and are genuinely opt-in for a granular consumer not
  using the `all.js` compatibility bundle. Neither had an entry in `package.json`'s `exports` map, so
  the documented import pattern (matching `flag-peer.js`'s precedent) hit
  `ERR_PACKAGE_PATH_NOT_EXPORTED` even though both files ship in `dist/` and are correctly declared in
  `sideEffects` — the same defect class as the historical `flag-peer.js` `sideEffects` omission, this
  time in the exports map instead.

### Patch Changes

- c9a9303: Document `<lr-heatmap>`'s flat-property-to-`data` replacement, a 9.0.0 breaking change that shipped
  without a changelog entry.

  9.0.0 replaced ten independent top-level `<lr-heatmap>` members with a single discriminated-union
  `data` property. The removed members are `mode`, `days`, `rowLabels`, `colLabels`, `values`,
  `firstDayOfWeek`, `columnX`, `rowY`, `weekdayLabelText`, and `monthLabelText`. They are now fields on
  one of the two `data` branches — `HeatmapMatrixData` (`{ kind: 'matrix', rowLabels, colLabels,
  values }`) or `HeatmapCalendarData` (`{ kind: 'calendar', days, firstDayOfWeek?, columnX?, rowY?,
  weekdayLabelText?, monthLabelText? }`) — united as `HeatmapData` and exported from the package root.

  There are no runtime aliases, and assigning a removed member is silent: Lit accepts it as an
  unobserved instance property, so the component keeps rendering its default empty grid instead of
  erroring. That silence is why this entry exists — the 9.0.0 notes omitted the change entirely, so a
  consumer grepping the changelog for `HeatmapMatrixData`, `HeatmapCalendarData`, `HeatmapData`, or any
  of the removed member names found nothing and had no way to learn the API had moved.

  The `data` shape itself is unchanged and intentional; only the changelog record was missing.
  `llms/data.md`'s "9.0 migration" note already carries the full recipe, including the related removal
  of the magic `value-label="value"` localization sentinel:

  ```js
  // removed in 9.0.0
  el.mode = 'matrix';
  el.rowLabels = ['Mon', 'Tue'];
  el.colLabels = ['00h', '06h'];
  el.values = [
    [1, 2],
    [3, 4],
  ];

  // 9.0.0 and later
  el.data = {
    kind: 'matrix',
    rowLabels: ['Mon', 'Tue'],
    colLabels: ['00h', '06h'],
    values: [
      [1, 2],
      [3, 4],
    ],
  };
  ```
- 3f294c1: Fix `<lr-lite-chart>`'s first category label colliding with the bottom y-axis tick.

  A line chart centres its first category label on `plotX`, so that label always reaches left into the
  y-axis tick column — a measured 5.7px horizontal overlap on both Chromium and Firefox. The only thing
  holding the two apart is the vertical gap between the label row and the bottom tick, which is
  `dominant-baseline="middle"` on the plot floor and therefore hangs half its line box below that floor
  into the label row.

  That gap was 1.3px on Chromium and **-0.7px on Firefox**, whose line box for the same 10px
  `system-ui` font is 16px against Chromium's 14px. Firefox therefore painted the first x-axis label
  overlapping the `0` tick. Raising `CATEGORY_LABEL_OFFSET` 18 → 24 and `PAD_BOTTOM` 24 → 30 together
  leaves ~5px clear on both engines, comfortably past that 2px cross-engine variation.

  Because both constants moved by the same amount, the category-label row does not shift: the plot
  floor rises instead, so a chart's labels stay where they were and its plot area is 6px shorter. Charts
  with an `x-label` axis title are unaffected beyond that, since `AXIS_TITLE_SPACE` is measured from
  `padBottom`.

  Note the truncation width model is unchanged: `displayCategoryLabel()` still estimates fit from
  `APPROX_LABEL_CHARACTER_WIDTH`, so a label's *horizontal* extent remains an approximation rather than
  a measurement. This change makes the label row robust to that approximation being wrong rather than
  making the approximation exact.
- 7ae8930: Document `<lr-tab-group>`'s removed `slot`/`label` child model, a 9.0.0 breaking change that shipped
  without a changelog entry.

  9.0.0 removed the pre-9.0 attribute child model, in which a direct `<div slot="x" label="…">` child
  became a tab captioned by its `label` with its own content as the panel, and a sibling
  `slot="x-icon"` child supplied that tab's leading icon. `<lr-tab-group>` now builds its tab list
  only from `<lr-tab panel="x">` descriptors paired with `<lr-tab-panel name="x">` panels; any other
  child element is skipped regardless of its `slot`/`label` attributes, so markup still written in the
  old shape renders an empty tab strip with no console warning.

  The removal itself is unchanged and intentional — this entry only records it, because the 9.0.0 notes
  omitted it while `README.md` continued to state that the `slot`/`label` shape "still works
  unchanged". Both README claims are corrected (the 7.x → 8.0.0 rename table and the component/mirror
  table), which also clears the same stale claim from three generated `llms/migration.md` rows
  (`<wa-tab>`, `<wa-tab-panel>`, `<sl-tab-panel>`) and the packaged skill reference. `llms/layout.md`
  already described the removal correctly and is unchanged.

  To migrate, rewrite each former child as one descriptor plus one panel, folding any former
  `slot="x-icon"` sibling's content into the `<lr-tab>`'s own default slot:

  ```html
  <!-- removed in 9.0.0 -->
  <lr-tab-group>
    <div slot="general" label="General">General settings</div>
  </lr-tab-group>

  <!-- 9.0.0 and later -->
  <lr-tab-group>
    <lr-tab panel="general">General</lr-tab>
    <lr-tab-panel name="general">General settings</lr-tab-panel>
  </lr-tab-group>
  ```

  A regression test now asserts a plain `slot`/`label` child produces no tab and no rendered panel, so
  the behavior cannot drift back into being documented as supported.

## 9.0.0

### Major Changes

- 000b9e3: This major version finalizes lyra-ui's 9.0 public-contract cleanup: it closes the deprecation and
  rename windows opened in 8.0.0, aligns the shared vocabulary with Web Awesome and Shoelace, and adds
  cross-component security, accessibility, internationalization, theming, and performance work. The
  generated migration reference (`llms/migration.md`) records the exact replacement for every renamed
  or removed member; sections below group every change by what it means for a consumer evaluating this
  upgrade.

  ### Breaking changes

  - make `lr-button` submit/reset behavior a true post-click default action, so
    `click.preventDefault()` vetoes it while propagation control alone does not.
  - `lr-tool-call-chip` / `lr-message-parts`: `lr-tool-chip-select` → `lr-tool-call-chip-select`
    (identical detail). `lr-tool-timeline` bound both, so a host listener fired **twice** per click;
    it now fires once.
  - `lr-flow-canvas`: `--lr-flow-canvas-node-current-outline-color` →
    `--lr-flow-canvas-node-selected-outline-color`
  - `lr-usage-badge`: `compact` → `abbreviate`. It selected `Intl` compact *notation* while 20 other
    components use `compact` for visual density.
  - `lr-chart` (and every typed subclass): `horizontal` → `index-axis="y"`
  - `lr-rag-answer`, `lr-retrieval-results`: `error` → `errorText` / `error-text`
  - `lr-document-preview`: `errorMessage` → `errorText` / `error-text`
  - `lr-ingestion-queue`, `lr-activity-feed`: `virtualizeThreshold` → `virtualizeAt`, and the bound is
    now exclusive to match the other four components — `virtualize-threshold="N"` becomes
    `virtualize-at="N-1"` for an identical switchover point.
  - `lr-knowledge-base`: `lr-kb-create`/`-sync`/`-pause`/`-delete` → `lr-source-*` (identical details;
    hosts listening on `lr-knowledge-base-admin` are unaffected).
  - `lr-data-grid`: option fields `columns` → `columnIds`, `filename` → `fileName` (upstream's own
    spellings — this moves *toward* the mirror).
  - `lr-test-results`: legacy `detail-{suiteId}-{testId}` and `detail-{testId}` slots → the canonical
    `testResultDetailSlotName(suiteId, testId)`.
  - `lr-confirm-bar`: `compact` is density only; use `compact frame="plain"` for the old flat
    presentation. `--lr-confirm-bar-compact-{border,background,radius}` removed.
  - `lr-accordion`: direct `<lr-details>` children are no longer coordinated — use
    `<lr-accordion-item>` (which still accepts `summary`/`open`/`show()`/`hide()`).
  - `lr-ebook-viewer`: `accessibleLabel` is private (the property never had any effect; set the
    `aria-label` attribute), and the permanently-empty `announcer` part is gone.
  - `lr-sequence-strip`: `orientation` removed — a single-member union that nothing read or styled.
  - `lr-split-panel`: `SplitPanelSnapFunctionOptions` / `SplitPanelSnapFunctionParams` removed; both
    aliased `SnapFunctionParams`.
  - Compatibility type aliases `ActivityEntryTone`, `ConfirmBarTone`, and `ChipTone` removed; use
    `LyraVariant`, `ConfirmBarVariant`, and `ChipVariant` respectively. The corresponding public
    properties already use `variant`, so these aliases duplicated the canonical type vocabulary.
  - Ten orphaned localization keys removed — they shipped translated into all ten locales while no
    component rendered them: `trendIncreased`/`trendDecreased`/`trendGoodSuffix`/`trendBadSuffix` (use
    `statTrend*`), `subagentPanelCancel` (use `subagentPanelCancelRun`), plus
    `contactViewerOrganizationLabel`, `evaluationDashboardMetricLabel`, `heatmapCellSelectedSuffix`,
    `liteChartMarkPosition`, `spanTokens`. A generator gate now fails on any future orphan.
  - `srOnly` now hides via `clip-path: inset(50%)` instead of the deprecated `clip: rect(0 0 0 0)`. If
    you reveal an `.sr-only` element on focus, replace `clip: auto` with `clip-path: none`. Note
    `clip-path` establishes a containing block for absolutely-positioned descendants.
  - **Migration coverage improves measurably**: `warning-required` mappings drop from 13 to 9,
    `exact` rises 54 → 56 and `rewritten` 78 → 80. `<wa-button>`, `<sl-button>`, `<wa-breadcrumb-item>`
    and `<sl-breadcrumb-item>` now migrate mechanically — previously the codemod refused *every* one of
    them, including buttons with no `href` at all, because the warning is emitted per tag rather than
    per member.
  - complete the v9 identity migrations from `lr-split`, `lr-query-builder`, `lr-playback`,
    `lr-generation-status`, and `lr-flow-run-overlay` to `lr-multi-split`,
    `lr-condition-builder`, `lr-sequence-playback`, `lr-generation-metrics`, and
    `lr-flow-run-status`, including their class, type, event, CSS-hook, storage-key, registration,
    and framework surfaces; the pre-v9 `lr-geojson-view` tag remains the documented compatibility
    alias for canonical `lr-geojson-viewer`;
  - replace ambiguous generic public type aliases with the curated `Lyra*` vocabulary and remove
    superseded root and granular aliases; the generated migration reference records each exact
    replacement and every surface that now requires manual review;
  - use `CustomEvent<null>` for no-payload library events, frozen named detail objects for scalar
    state, and cancelable before/invalid events only where vetoing changes the originating action;
  - unify model and voice catalogs on readonly `LyraCatalog<T>` and move their shared filtering,
    popup, selection, form, and native-event behavior into one catalog-picker contract;
  - treat omitted localized labels as the only request for a default string: an explicit empty
    string or the former English default is now caller-owned data and is never silently translated.
  - make public collection inputs and collection-bearing event details bounded readonly snapshots,
    with clone/freeze admission for structured records and copy-on-write updates instead of retained
    mutable aliases;
  - require stable, non-empty domain identifiers across collection-driven components, use
    deterministic first-valid-wins duplicate handling, and carry those identifiers through state,
    persistence, and event details rather than relying on array position or display text;

  ### Security

  - strip authored inline CSS from sanitized Markdown while preserving only strict Shiki palette
    colors, and paint-contain the explicit unsanitized escape hatch;
  - reject excessive ZIP entry and declared-expansion metadata before `lr-archive-viewer` asks JSZip
    to materialize its entry graph.
  - isolate fetched `lr-svg-viewer` content from author styles, SVG animation, and external resource
    references while retaining local paint servers.
  - reject XML document type declarations before browser entity expansion and preserve mixed XML
    child-node source order in `lr-xml-viewer`.
  - place `lr-mcp-app`'s inline CSP before every app-controlled token so head decoys cannot bypass it.
  - bound streamed ANSI CSI/OSC carry and recover `lr-terminal` after overlong unterminated sequences.
  - namespace `lr-thread-list` group and thread identities independently so unrestricted raw thread
    ids cannot steal active ownership from or collide with virtual group headers.
  - scope `lr-widget-renderer` warning dedupe to the current root/registry generation so streamed
    attacker-controlled type and prop names cannot accumulate for the instance lifetime.
  - reject malformed reachable `lr-widget-renderer` nodes before dereference, clear stale content,
    and report exactly one render error instead of throwing from a streamed update.
  - fail closed on duplicate `lr-tree` data ids so one public identity cannot own multiple focus,
    selection, expansion, or reorder targets.
  - widen the CSV formula-injection guard to fullwidth sigils and leading whitespace, and share one
    definition between `lr-data-grid` and the export helper instead of two drifted copies;
  - drop an `lr-mcp-app` tool result whose originating frame has been replaced, via an additive
    `frameGeneration` correlation on the event detail and an optional `postToolResult()` argument;
  - reject or truncate hostile oversized string, tree, registry, schema, and traversal inputs at
    documented ceilings while keeping valid siblings usable and async generation ownership intact.

  ### Accessibility

  - preserve keyboard focus when retrieval paths, chips, and source collections change;
  - preserve `lr-emoji-picker` option focus by identity or nearest survivor when controlled groups
    reorder or shrink, without stealing search or external focus, and materialize off-window roving
    targets before transferring focus;
  - move `lr-time-input` focus to a surviving segment when a controlled pattern change removes the
    focused segment, without reclaiming focus from another control;
  - restore `lr-export-button` to sequential keyboard navigation after loading or disablement ends;
  - report and announce `lr-diff-view` clipboard failures without falsely confirming stale or failed writes;
  - fully suppress the visible `lr-toast-item` progress animation under reduced motion;
  - reconcile and announce retained `lr-xml-viewer` search state after XML reloads.
  - preserve `lr-code-block` and `lr-code-block-core` roving focus when controlled code shrinks.
  - keep disabled, hidden, and inert custom controls out of `lr-message-actions` roving navigation.
  - skip unavailable `lr-thread-list` rows locally and across virtual-window keyboard boundaries.
  - preserve `lr-prompt-queue` focus when a controlled removal is accepted.
  - enforce `lr-push-to-talk`'s hit floor and keep custom trigger glyphs decorative.
  - preserve `lr-sequence-strip` and `lr-heatmap` roving focus through controlled refreshes, and honor the strip's host name.
  - preserve `lr-graph-query-builder` focus when filter chips or saved queries are removed.
  - transfer `lr-realtime-session` focus when its public capture surface is hidden.
  - preserve `lr-selection-toolbar` focus when its controlled action set changes.
  - keep `lr-stat` slotted controls outside its whole-card link and forward its host accessible name.
  - normalize `lr-combobox`'s active descendant after local or async option-set changes.
  - preserve `lr-table` row and header focus when controlled collections shrink or reorder.
  - retain a non-color visual state and part hook for hidden chart legend series.
  - honor host `aria-label` presence on `lr-filter-bar`, including an explicitly empty value, before
    falling back to the component's `label` property.
  - preserve host `aria-label` precedence by attribute presence, including explicitly empty values,
    across archive, calendar, contact, CSV, dataset, comparison, document, and DOCX viewers and the
    nested dialog path used by `lr-document-viewer`.
  - preserve `lr-select`'s keyboard-active option by identity across live reorders and rehome
    removed or disabled active rows to the nearest navigable survivor.
  - make `lr-thread-list` Home/End navigation resolve the complete virtual model, skipping group
    records and unavailable endpoint rows instead of stopping at the mounted window.
  - remove disabled `lr-token-input` edit triggers from focus, expose their disabled state, retire
    internal focus, and suppress enabled hover feedback until the control is re-enabled.
  - retain the shared icon-button hit-area floor for every emoji-picker option while compact size
    tiers continue to scale the glyph independently.
  - retain the shared 40px compact-target floor for circle/icon-only buttons and label-less
    checkboxes across every size tier while keeping their visible glyph and box tier-sized.
  - integrate combobox listboxes with the shared nonmodal overlay stack so z-order, Escape,
    outside-pointer dismissal, and focus handoff remain topmost-only beside color-picker popups.
  - announce `lr-tree` reorder success only after the host-owned sibling order confirms the exact
    request, keeping ignored or rejected requests silent.
  - realign canceled `lr-carousel` mouse drags instantly under reduced motion while preserving
    smooth full-motion recovery, including the pointer-capture-loss path.
  - cancel stale `lr-sequence-strip` arrow/Home/End focus continuations across controlled item
    replacement and disconnect/reconnect instead of focusing a new model by an old numeric index.
  - give custom `lr-page` navigation toggles a resolvable host bridge for their private drawer and
    restore only component-owned ARIA across replacement, removal, and reconnect.
  - rehome `lr-locale-picker`'s active option when a live locale catalog shrinks, keeping
    `aria-activedescendant` and the next keyboard command valid.
  - make `lr-time-input.focus()` honor direct and fieldset disablement, matching its click and tab-stop
    contract without emitting synthetic host focus events.
  - preserve `lr-av-player`'s named region when an unsafe media source renders its error branch.
  - announce `lr-flow-minimap` viewport changes for map clicks, wheel zoom, keyboard commands, and
    completed drags while keeping canceled drags silent.
  - make `lr-token-input.focus()` synchronously honor direct and fieldset-cascaded disablement before
    the internal draft input has re-rendered disabled.
  - preserve `lr-swatch-picker` keyboard focus across live palette reorders and focused-option
    removal without changing its controlled value or emitting a user-change event.
  - isolate all seven `lr-video` icon slots in inert decorative sibling layers so accidentally
    interactive glyph markup cannot nest actions or add keyboard stops.
  - `lr-xml-viewer` search now scrolls the active match into view, honoring reduced motion.
  - `lr-breadcrumb.accessibleLabel` is consumed instead of ignored.
  - move `lr-transcript-feed` announcements out of its shadow `role="log"` and onto the shared
    light-DOM polite sink, leaving the shadow region non-live;
  - give `lr-thinking-panel`'s always-tabbable scroll region a real focus ring and a distinct hover
    preview, matching `lr-code-block` and `lr-virtual-list`;
  - draw `lr-mind-map`'s focus ring as soon as the widget takes focus, rather than only after the
    first arrow key;
  - hand `lr-confirm-bar` focus to its status region when a host defers the decision, instead of
    dropping it to `<body>` while the just-activated button becomes `disabled`;
  - name an icon-only toggleable `lr-chip`'s real control, and give `lr-phone-input` an accessible-name
    fallback;
  - carry a persistent region landmark on `lr-dataset-viewer` in every fetch state;
  - announce timeline and step position on `lr-video` and `lr-sequence-playback` through localized
    `aria-valuetext`;
  - floor the `lr-flow-minimap` viewport rect and `lr-data-grid` hit targets to a real pointer size.
  - give `lr-box-plot` and `lr-lite-chart` the chart family's forced-colors series encodings;

  ### New capabilities

  - add standard error text, rich error-slot chrome, and handle descriptions to `lr-slider`;
  - add standard error text, rich error-slot chrome, SSR presence hints, and semantic descriptions
    to `lr-file-input`;
  - export public component property and configuration types from the registration-free package root;
  - complete `lr-voice-picker`'s standard form-control frame and slotted-label contract.
  - add owned error chrome and the standard form-control frame to `lr-checkbox`.
  - accept canonical `start`/`end` adornment slots on `lr-chat-composer` while retaining legacy aliases.
  - accept canonical `start`/`end` adornment slots on `lr-radio-button` while retaining Shoelace
    `prefix`/`suffix` aliases.
  - accept canonical `start` adornment content on `lr-conversation-item` while retaining `leading`.
  - complete `lr-model-select`'s standard slotted-label and `form-control` frame contract.
  - expose the shared configurable Markdown parser and public refresh method on `lr-markdown-core`.
  - relay `lr-data-grid` search and filter focus transitions through its host as native events.
  - expose `lr-model-select`'s free-text input, selection, and range-editing facade.
  - separate chart legend, data-table, and reset-zoom hover/pressed theme hooks.
  - expose `lr-lite-chart`'s selected-mark outline width alongside its color.
  - let `lr-calendar` themes independently inherit selected, outside-month, and today paint hooks.
  - keep every `lr-flow-canvas` edge tone and its referenced arrowhead independently themeable.
  - expose peer-neutral Markdown, message-feedback, and model-select types from the registration-free
    package root and the relevant granular Markdown entries.
  - forward semantic `lr-filter-bar` control parts so consumers can theme its composed select,
    combobox, text, and date fields through the bar's shadow boundary.
  - expose independently inheritable progress, action, current-page, hover, and pressed color hooks
    for `lr-flow-node`, `lr-graph-query-builder`, and `lr-pagination`, preserving the existing palette
    as exact fallbacks.
  - split `lr-pagination` summary, control-group, and numbered-page spacing into independent
    inheritable layout hooks that retain the existing spacing defaults.
  - let `lr-message-feedback` hosts hold a submission for async persistence, then explicitly finalize
    or revert it without premature success UI.
  - let `lr-table`, `lr-timeline-item`, and `lr-word-cloud` inherit their public theme hooks from
    ancestor theme wrappers while preserving direct-host precedence and existing defaults.
  - expose independent `lr-stat` emphasis edge/value hooks and checked/indeterminate `lr-tree-item`
    checkbox border, background, and glyph hooks without changing their semantic-token fallbacks.
  - expose `lr-otp-input` compact-string selection and range-editing APIs, with sanitized silent
    replacements kept synchronized across visual cells, form value, and validity.
  - define `lr-realtime-session.errorCode` as host-readable diagnostic metadata while retaining one
    safe localized generic failure for all provider codes.
  - publish normalized effective zoom bounds in flow-canvas companion snapshots and use them for
    `lr-flow-controls` button availability.
  - add the canonical `start` leading-icon slot to `lr-stat`, with the shipped unnamed slot retained
    as its deterministic fallback.
  - add the purpose-named `marker-icon` slot to `lr-timeline-item`, retaining `icon` as its
    deterministic fallback.
  - keep form-control theme inputs inheritable through ancestor wrappers across size, appearance,
    and pill fallbacks for emoji picker, icon button, input subclasses, locale picker, OTP input,
    and phone input.
  - expose inheritable component-scoped gap and radius hooks for segmented time input, preserving
    shared size and pill tokens as fallbacks.
  - expose independent, inheritable state-paint hooks for input actions/focus, segmented-time
    surfaces and options, locale selection weight, and OTP active/invalid segments.
  - expose independent component-scoped state paint for checkbox/group/editor invalid and
    interaction states, color-picker selection, option current/selection, and the full date-picker
    title/navigation/day/range/selection-view state family.
  - add the shared reflected size ladder to `lr-voice-picker` in both picker modes while retaining
    the preview action's compact hit-area floor.
  - expose independent component-scoped hover and pressed paint hooks for `lr-time-range` presets
    and handles and for `lr-token-input` edit/remove actions, preserving the prior shared-token and
    aggregate-action hooks as backwards-compatible fallbacks.
  - expose independent component-scoped hover, pressed, checked, open, action, thumb, and field paint
    hooks across radio, radio-button, rubric-form, select, slider, switch, and textarea while
    preserving their existing shared-token fallbacks.
  - expose instance-scoped content and row gap hooks for radio-button chrome, slider, and switch,
    preserving the shared spacing tokens as defaults.
  - expose independently inheritable appearance, hover, and pressed paint hooks for app rail,
    app-rail item, breadcrumb item, card, and carousel states without changing their existing defaults.
  - expose independently inheritable appearance, border, hover, and pressed paint hooks across
    details, accordion, and accordion-item while retaining every existing shared-token fallback.
  - expose inheritable `lr-details` summary gap and surface radius hooks independently of its shared
    density ladder, preserving the existing spacing and radius tokens as defaults.
  - expose `lr-token-input`'s native draft selection and event-silent range-editing facade, keeping
    programmatic range edits synchronized with the next token commit.
  - expose every composed push-to-talk event through `lr-realtime-session`'s TypeScript, CEM,
    framework, Storybook, and authored-reference contracts without re-emitting the runtime events.
  - add optional peer-neutral literal icons to `lr-suggestion-chips`, rendered through a stable
    decorative part while the chip button retains focus and selection ownership.
  - accept Web Awesome's exact `currentSlide` carousel markup spelling after HTML normalization,
    retaining `current-slide` as the reflected canonical attribute and initial-conflict winner.
  - New `bridgeLyraLocale()` and `subscribeLyraLocale` (`@aceshooting/lyra-ui/utilities/localization.js`)
    mirror the active locale onto a host element's `lang`/`dir` and re-render non-`LyraElement` hosts on
    locale change.
  - `lr-combobox` exposes `part="group-label"`, matching `lr-select`.
  - `lr-stack-trace` gains `compact`; `lr-confirm-bar` gains `frame`.
  - `rel` is now settable on `lr-button` and `lr-breadcrumb-item`. Both mirror upstream's `rel`, so
    `nofollow`, `me`, `license` and `external` survive a `wa-`/`sl-` → `lr-` rename instead of being
    silently dropped. The security guarantee is unchanged and not removable: `opener` is always
    stripped, and `noopener noreferrer` is force-added whenever `target` is set. A same-tab link (no
    `target`) renders the author's tokens verbatim, because it opens no new browsing context.
    `lr-breadcrumb-item` defaults to `'noreferrer noopener'`, matching both upstreams; `lr-button` keeps
    no default, matching `wa-button` — defaulting it would start suppressing the `Referer` header on
    every same-tab link.
  - `lr-terminal` gains `compact` and `frame`, matching its agent-tools siblings;
  - `lr-swatch-picker` gains `disabled`; `lr-context-meter` gains `showLegend` and legend parts;
  - `lr-node-palette` gains `reorderable`; `lr-retrieval-results` gains custom grouping;
  - `lr-knowledge-graph-explorer` gains a presettable `searchQuery`;
  - `lr-xml-viewer` gains host-supplied highlights and attribute-path precision;
  - `lr-box-plot` gains per-box keyboard and pointer interactivity;
  - `lr-time-range` gains click-to-seek; `lr-filter-bar` options accept an icon;
  - `lr-document-viewer` gains an immutable discriminated renderer payload and typed registry
    adapters, letting the AV renderer receive bounded cues/tracks and advertise search only when its
    retained transcript is searchable without widening legacy `DocumentFile` callbacks;
  - host `focus()`/`blur()`/`click()` forwarding and re-emitted focus/blur on `lr-av-player`,
    `lr-pan-zoom`, `lr-video`, `lr-video-playlist`, and `lr-zoomable-frame`.
  - export the chart, graph, map, and geojson classes from the registration-free package root.
  - Expose `lr-provenance-panel`'s entity-chip row as the `entity-row` CSS part and make its line
    packing themeable through `--lr-provenance-panel-entity-justify` (default `flex-start`, so nothing
    changes when unset). The row wraps N entity chips but carried only a class, and it fills
    `::part(body)`'s inline size, so justifying the body could not move the wrapped lines — the same
    unreachable-packing gap fixed for `lr-suggestion-chips` in this release.
  - Expose `lr-suggestion-chips`' chip row as the `row` CSS part and make its line packing themeable
    through `--lr-suggestion-chips-justify` (default `flex-start`, so nothing changes when unset).
    Centering the chips under centered empty-state text previously had no reachable hook: the row is
    rendered in both the wrapping and the scrolling layout, carried only a class, and styling
    `::part(base)` as a centered flex container centered the chips only while they fit a single line —
    once they wrapped, the row filled the inline size and every line, the short final one included,
    packed to the start edge.
  - Add theming, accessibility, responsive-layout, and controlled-interaction improvements across the
    component library, including new component CSS hooks, prompt-input aliases, model option icons, and
    cancelable pre-commit layout events.

  ### Fixes

  - notify every same-variant Markdown instance when a shared in-flight KaTeX load settles, without
    duplicating completion work across repeated renders;
  - preserve retrieval evidence locators through `lr-retrieval-trace`;
  - honor slot-only answer and source content in `lr-rag-answer`;
  - normalize invalid `lr-knowledge-base-admin` tab state to its Sources fallback;
  - remove unintended nested card chrome from generated RAG sources and evaluation metrics;
  - isolate registered renderer dialog events so inner dialogs cannot close `lr-document-viewer`;
  - surface nonfatal `lr-dataset-viewer` parser diagnostics while preserving recoverable rows;
  - restore `lr-tool-param-form`'s cloned initial value and pristine interaction state on native form reset.
  - validate every `lr-box-plot` canvas theme color and fall back from invalid CSS expressions.
  - materialize `lr-audio-visualizer` canvas colors, including `currentColor`, in the live theme scope.
  - preserve author-supplied `lr-tree-item` names across data-model refreshes.
  - preserve nested `lr-evaluation-run` approval dialogs when a host vetoes the correlated decision event.
  - keep `lr-markdown-core` leading-tab parsing and `tab-size` behavior aligned with `lr-markdown`.
  - honor documented `lr-data-grid` copy/export/scroll option interfaces, including an explicit copy
    delimiter overriding the format default.
  - theme `lr-data-grid`'s search placeholder through the shared quiet-text token.
  - align `lr-code-block-core`'s reflected `copyable` states with the full code block.
  - export a total `lr-test-results` detail-slot-name helper for malformed UTF-16 ids.
  - stop `lr-flow-canvas`'s numeric grid fallback from shadowing its public theme hook.
  - wrap `lr-flow-canvas` corner companions so controls and minimaps do not overlap at 320px.
  - keep obsolete `lr-code-block-core` language-map loads from clearing the current loading state.
  - share full/core Markdown parsing, fallback, anchor, highlight, and rendering behavior while
    retaining their intentionally distinct Shiki grammar loaders.
  - keep the contained menu's standalone `lr-menu-select` alias inside `lr-dropdown`, leaving one
    documented cancelable `lr-select` event for direct, nested, and consumer-supplied menu shapes.
  - contain composed controls' `lr-input`/`lr-change` aliases inside `lr-filter-bar` while preserving
    one bar-owned full-value `lr-input` and the controls' native-style event path.
  - enforce `lr-dropdown[disabled]` as an opening invariant for initial markup, pre-upgrade property
    replay, and later imperative opening, independent of assignment order.
  - contain 200- and 500-item `lr-sequence-strip` datasets at 320px through a documented dense-collapse
    policy while preserving every semantic cell, tooltip, and roving keyboard stop.
  - keep `lr-push-to-talk` timer, level sampling, and maximum-duration deadline synchronized with
    option changes during an active recording.
  - prevent a shared Markdown dependency settlement from parsing twice when `lr-markdown` or
    `lr-markdown-core` reconnects before its promise callback runs.
  - keep `lr-document-library` search, tag-filter, and checkbox implementation events inside the
    component while emitting one documented filter or selection event per interaction.
  - roll back `lr-data-grid` pointer resize state on cancellation or lost capture without emitting a
    committed resize; retain pointerup as the commit path.
  - make a live `lr-flow-canvas[locked]` transition cancel and roll back active gestures, retire
    global pointer listeners, and block imperative viewport mutation.
  - retire `lr-flow-canvas` node-drag and connect gestures when the controlled node model is
    replaced so stale ids cannot move or connect after refresh.
  - keep `lr-flow-minimap` click-to-center available after a canceled viewport drag while consuming
    only the browser-synthesized click after a completed drag.
  - constrain emoji-grid and segmented-time-picker scrollports to their intended block axis under
    undersized allocations, without introducing phantom horizontal scrollbars.
  - reconcile action-bearing input, number, segmented-time, and phone rows on one shared rendered
    size ladder while preserving compact plain-field tiers.
  - keep `lr-tree-item` constructible when optional `ElementInternals` custom-state support is
    absent or only partially implemented.
  - restore `lr-graph-query-builder`'s normalized initial query on form reset instead of always
    erasing it, while retaining pristine-state cleanup and native custom-validity persistence.
  - normalize non-finite `lr-query-builder` number conditions at controlled-model, late-field, and
    user-input boundaries so blank number controls cannot retain JSON-null-producing infinities.
  - strictly round-trip-validate `lr-filter-bar` ISO date chips, preserving impossible values raw and
    retaining literal four-digit years below 0100 instead of JavaScript's 1900 remapping.
  - make `lr-token-input` overflow ownership explicit: uncapped rows grow, while exact-height rows
    clip inline overflow and scroll in the block axis so wrapped tokens and actions remain reachable.
  - make a vetoed `lr-combobox` close atomic, preserving its filter query, active option, async rows,
    reflected open state, and overlay ownership until a close is accepted.
  - roll canceled or capture-lost `lr-color-picker` pointer previews back to their pre-gesture
    visible and submitted value without emitting a commit, while preserving authoritative consumer
    assignments.
  - bind `lr-otp-input` auto-submit tasks to the exact completed code generation so a stale task
    cannot submit a later full value after replacement, reset, restoration, or reconnection.
  - keep `lr-filter-bar`'s reset action on the same default `m` height tier as its adjacent built-in
    fields instead of hardcoding the shorter `s` tier.
  - keep short resizable dashboard-grid cells at the shared interactive-action height floor so their
    absolute resize handles cannot overlap the preceding stacked cell or gap.
  - commit a pending `lr-time-range` keyboard gesture exactly once when its handle loses focus before
    key release.
  - invalidate active `lr-time-range` keyboard and pointer gestures on direct/fieldset disablement or
    form reset so later physical releases cannot commit stale values.
  - co-tokenize `lr-date-picker`'s mirrored `date-picker` part and deprecated `base` alias on
    the same visible shell so either consumer selector reaches identical chrome.
  - contain `lr-model-settings-panel`'s internal live slider event while preserving its mirrored
    temperature readout and consolidated committed-change contract.
  - Nine form-associated components silently never published their validity custom states in
    environments without `ElementInternals`; their fallback lacked a `states` set.
  - `lr-combobox.validators` and `lr-file-input.validators` type-checked and assigned but **never
    ran**. Both are now wired to the same contract `lr-date-input` uses.
  - `lr-flow-node.nodeId` now reflects, so a JS property write is visible to `lr-flow-canvas`, which
    adopts children by attribute.
  - `lr-spreadsheet-viewer.jumpToCell()` no longer reports a phantom `found: true` when nothing loaded.
  - keep a cancelled `lr-animation` cancelled when the play state is later synced;
  - report an honest failure when a concurrent `src` reassignment lands mid-anchor-resolution in the
    archive, CSV, and dataset viewers;
  - guard `lr-code-block`'s async highlight continuations on `isConnected`;
  - ignore a non-primary pointer button when starting an `lr-image-viewer` annotation.
  - bound and cache text-quote indexing, search, DOM-range painting, and host highlight resolution;
    `lr-search-change` now includes `matchCountExact`, so capped or partially loaded viewers report
    a truthful lower bound instead of presenting it as a complete count;
  - wire Shiki's dark palette through `lr-markdown` and `lr-markdown-core`;
  - move documented per-component custom-property defaults to private fallback tokens and apply the
    public hook at each use site, so values inherited from an ancestor are no longer shadowed by a
    `:host` declaration.
  - Forward `lr-tree-item`'s complete public CSS-part surface through recursively rendered data-model
    children. A single `::part()` selector on the outer item can now theme matching rows, labels,
    states, badges, checkboxes, and disclosure controls at every rendered depth.

  ### Performance

  - cap explicit and page-derived `lr-table` skeleton row counts before allocating placeholder cells.
  - forward text and reasoning part streaming state from `lr-message-parts` into its composed Markdown
    renderers so parse and highlighting work coalesces until completion.
  - compute `lr-message-parts` citation ranks in one linear pass instead of rescanning every prefix.
  - suspend full `lr-heatmap` canvas work while offscreen, coalesce hidden invalidations into one
    visibility-entry redraw, and repaint locale-derived canvas labels when locale changes.
  - resolve `lr-terminal` highlight ownership and search-match state in one pass per render instead of
    rescanning per line;
  - cache tree ordering in `lr-subagent-panel`, status counts in `lr-test-results`, the filter/categorize/roving chain in `lr-node-palette`, the dedupe/sort/group pipeline in
    `lr-retrieval-results`, the folded-quote transform in `lr-email-viewer`, and the text index in
    `lr-docx-viewer` (now binary-searched);
  - coalesce `lr-scroller`'s `lr-scroll` to one emission per animation frame.

  ### Internationalization and RTL

  - harden rendered safe-area coverage for every `lr-toast` placement in LTR and RTL;
  - replace English `lr-test-results` status initials with language-neutral decorative marks.
  - localize generated matrix and category counts in `lr-heatmap` and `lr-sequence-strip`.
  - let `lr-select` triggers and overlaid tag rows shrink so long placeholders, selected labels, and
    multi-select chips remain inside constrained LTR and RTL rows.
  - localize complete highlighted-cell names in CSV, dataset, and spreadsheet viewers with separate
    value and annotation placeholders, allowing each locale to control their order and punctuation.
  - format compact `lr-rubric-form` score labels with the effective locale, matching its slider
    branch while preserving stable raw item and submission values.
  - contain long RTL input adornments and segmented-time chrome, with exact-320px stories for input,
    number, segmented-time, and OTP action/fixed-cell compositions.
  - contain button, checkbox, option, and date-input labels/adornments in narrow LTR and RTL rows
    while preserving fixed interactive and glyph geometry.
  - contain standalone `lr-radio`, `lr-radio-button`, and `lr-slider` label, adornment, reference,
    and hint content within narrow LTR and RTL allocations without shrinking fixed controls.
  - correct the `lr-heatmap` cell-text contract to describe its localized matrix/calendar templates
    and reserve `cellText` for application-specific wording rather than ordinary translation.
  - contain unbroken consumer-authored dashboard-grid cell content within narrow LTR/RTL stacks while
    preserving explicitly child-owned horizontal scrollports.
  - contain unbroken active-filter chip values within narrow LTR/RTL filter bars by zeroing nested
    flex minima while retaining the chip's own ellipsis and removal behavior.
  - localize `lr-callout`'s complete labeled live-announcement template so locales can reorder its
    context/content fields and choose their own punctuation.
  - `setLyraLocale()` was **inert on any page with `<html lang>`** — i.e. essentially every
    well-formed page — because the document element won the precedence walk. An explicit call now
    wins; element and ancestor `lang`/`locale` still override it, per a documented order.
  - add `playbackStepPosition`, `phoneInputLabel`, and `emojiPickerLoadError`, translated into all ten
    shipped locale catalogs; `lr-phone-input` and `lr-emoji-picker` no longer borrow another
    component's message key;
  - format `lr-grounding-summary` evidence offsets with the effective locale;
  - fix an RTL double-mirror in `lr-chart`'s DOM legend placement by removing a redundant mirror
    rather than adding a third;
  - stop re-mirroring MapLibre's physically-assigned popup anchors under `dir="rtl"`.
  - `lr-data-grid` accepts `'start'`/`'end'` as spelling aliases for the existing RTL-relative
    `'left'`/`'right'` pin sides, and renders its pager glyphs as mirroring icons;

  ### Internal quality and coverage (informational)

  - complete standalone theme inputs and semantic contrast coverage;
  - publish every `lr-typing-indicator` geometry hook through CEM/editor metadata and gate its
    legacy public token namespace against future omissions;
  - cover `lr-spinner`'s populated, forwarded visible-label accessibility state.
  - make authored CSS-part references exact and complete, and correct `lr-model-select` accessible-name precedence prose.
  - verify conversation placeholder theming, typing motion, and native search-decoration suppression
    through rendered browser behavior, including WebKit's still-clickable search cancel control.
  - keep Web Test Runner failure payloads primitive-only and gate component tests against live DOM-node Chai assertions.
  - verify `lr-agent-run` spinner motion through live full-motion and reduced-motion computed styles.
  - verify `lr-prompt-studio` native option palettes through live light- and dark-theme computed styles.
  - harden agent-tool motion, native-control, placeholder, and footer layout contracts with rendered checks.
  - verify `lr-code-block-core` Shiki token palettes through rendered light and dark computed colors.
  - clarify that `lr-prompt-input` submits its composite state through typed events, not native form data.
  - cover every concrete typed chart controller in a shared 320px RTL long-content story fixture.
  - cover `lr-calendar` in an exact 320px RTL fixture with long localized event content.
  - keep lean code-block and Markdown bundles free of Shiki's full grammar table, enforced through
    peer-inclusive dependency-graph checks.
  - correct the documented Markdown heading-outline return type and guard its keys against the
    exported `MarkdownHeadingItem` interface.
  - enroll standalone theme inputs in the canonical design-token artifacts and keep stripped
    internal Shiki test seams out of published declarations.
  - recognize constant-backed `exportparts` vocabularies in the manifest contract and document the
    chart subclasses' inherited hidden-legend and control-state theming surfaces.
  - cover `lr-query-builder` at exactly 320px with long public and localized select labels in both
    LTR and RTL, guarding the composed `lr-select` min-content repair.
  - add paired exact-320px LTR/RTL Storybook baselines for long stat, table priority/action, vertical
    and horizontal timeline, and word-cloud legend compositions.
  - add exact-320px RTL Storybook and rendered baselines for checkbox-group wrapping and code-editor
    form-chrome/source-overflow ownership.
  - correct transcript-feed documentation for plain `scrollToBottom()` behavior and conditional
    interim-area rendering, with a focused regression for follow-state preservation.
  - align thread-list documentation with the shipped conditional `row-wrapper` part and clarify its
    nesting around built-in and custom row actions.
  - document and verify a genuinely lean widget-renderer manual-definition route whose real bundle
    graph excludes the eager default registry and all eight default mapped classes.
  - add exact-320px LTR/RTL long-content Storybook and rendered baselines for horizontal radio-button
    groups, switch labels/hints, and rubric fields/actions.
  - add exact-320px RTL long-content Storybook and rendered baselines for app rail, breadcrumb,
    button-group, and carousel compositions, including open mobile chrome and populated controls.
  - add exact-320px RTL long-summary, expanded-content, and action Storybook/rendered baselines for
    details and the composed accordion/accordion-item family.
  - add an exact-320px RTL long-content Storybook/rendered baseline for expanded and collapsed
    `lr-dock-panel` layouts beside independently scrolling main content.
  - correct the npm README and internal rationale for `lr-sequence-strip` to describe its named
    roving list/listitem inspection model instead of the retired aggregate-image model.
  - publish `lr-flow-canvas` zoom controls as real prototype methods in CEM and remove them from
    generated framework assignable-prop unions.
  - correct `lr-flow-canvas` running-edge documentation and metadata to use the time-only ambient
    duration token, with rendered coverage that distinguishes it from the transition shorthand.
  - nest `lr-flow-controls` and `lr-flow-minimap` inside the canonical `lr-flow-canvas` example so
    their documented corner slots are actually assigned.
  - remove `lr-stream-status` real-timer test races by installing stall listeners before connection
    or recovery can arm and fire their timers.
  - verify `lr-streaming-text` cursor theming and reduced-motion behavior through live rendered
    geometry, animation, and opacity instead of stylesheet-source assertions.
  - follow static, dynamic, side-effect, single-quoted, and double-quoted component imports when
    attributing optional peers, and document `lr-streaming-text`'s exact Markdown fallback matrix.
  - eagerly wire the `lr-stream-status` LiveDemo so its first Connect click works, and invalidate a
    pending connection completion when the demo is stopped.
  - replace source-only CSS assertions with rendered submit-color, listbox-overflow, and
    currentColor-glyph coverage for rubric form, select, and swatch picker.
  - stop the default-string slice generator from treating an incidental string literal in a helper
    module as a reachable message key, which had been pulling unused messages into component bundles;
  - forward a README mirror row's migration note into the generated migration disposition;

## 8.2.3

### Patch Changes

- 7d76af5: Fix horizontally scrolled Shiki code and diff backgrounds, provide shadow-local MapLibre canvas,
  marker, popup, and control layout, and prevent inline-size query containers from collapsing in
  shrink-to-fit layouts.
- db49718: Raise the optional `postal-mime` peer range to `^2.7.6`.

## 8.2.2

### Patch Changes

- f0a41be: Fix: `<lr-input>` now forwards `name` and a host-supplied `id` to its internal native `<input>`, restoring password-manager autofill/save detection for shadow-DOM-aware password managers that key field detection off the actual control's `name`/`id` rather than `autocomplete` alone. The internal `<label for>` tracks whichever id is in use. Leaving `id` unset keeps the internal input at `id="input"`, unchanged from before.

## 8.2.1

### Patch Changes

- 6c00bbc: Fix `<lr-chat-composer>` marking a field touched from a blur the platform forces when the control becomes disabled while focused, which could trip a Lit dev-mode reentrancy warning.
- 3a942eb: Fix `<lr-checkbox>` marking a field touched/interacted from a blur the platform forces when the control (or an ancestor `<fieldset>`) becomes disabled while it is focused, which could leave the control primed to show as invalid immediately on re-enable, or trip a Lit dev-mode reentrancy warning.
- 842484a: Fix `<lr-model-select>` marking a field touched from a blur the platform forces when the trigger button or combobox input becomes disabled while focused, which could trip a Lit dev-mode reentrancy warning.
- 9c69ed7: Fix `<lr-textarea>` marking a field touched from a blur the platform forces when the control becomes disabled while focused, which could trip a Lit dev-mode reentrancy warning.
- 9476967: Fix `<lr-time-input>` marking a segment touched from a blur the platform forces when the focused segment becomes disabled (its tabindex drops below zero while it still holds focus), which could trip a Lit dev-mode reentrancy warning.
- 823b395: `@aceshooting/lyra-ui/testing`'s `installHappyDomFormAssociatedShims()` now resolves the stub `ElementInternals.form` live via `host.closest('form')` instead of always `null` — a form-associated component that calls `attachInternals()` from its constructor (before it's inserted anywhere) previously got a permanently-`null` form owner even after being placed inside a real `<form>`, silently breaking anything (like `<lr-button>`) that resolves its submit target through `internals.form`.
- de626e7: Fix `<lr-input>` marking a field touched from a blur the platform forces when the control becomes disabled while focused, and stop `formDisabledCallback()` redoing validity/render work that a same-tick `disabled` write already performed — together these could trip Lit's dev-mode "scheduled an update after an update completed" warning inside a real `<lr-dialog>` for a re-render nothing observable needed.
- 340d39b: Fix `<lr-known-date>` marking a field touched from a blur the platform forces when the field becomes disabled while focused, which could trip a Lit dev-mode reentrancy warning.
- db19f3e: `LyraElement`'s ancestor `class`/`style` observer (kept for CSS-only direction/locale context changes) now only calls `requestUpdate()` when the resolved direction or locale actually changes, instead of on every ancestor `class`/`style` mutation regardless of relevance — an unrelated ancestor style write (e.g. an overlay's own stacking-index custom property) could otherwise schedule a spurious re-render.
- 6ef43b1: Fix `LyraElement`'s ancestor `class`/`style` observer (introduced in the previous release to stop spurious re-renders) forcing a `getComputedStyle()` read on _every_ ancestor `class`/`style`/`locale`/`lang` mutation, even ones with nothing to do with direction. That forced read — from a sibling's own unrelated `MutationObserver` reacting to an ancestor's inline-style write — could permanently break a completely unrelated host's own shadow-DOM CSS custom-property resolution in Chromium (observed with `<lr-chip-group>`'s `--lr-chip-group-overflow-expanded-color`), and forced an extra synchronous style read on every reconnect/adoption regardless of whether anything direction-relevant changed. The observer now only calls `getComputedStyle()` when the mutation could plausibly affect direction (an explicit `dir`/`class` change, or a `style` change that actually declares `direction`), and seeds its baseline from whatever the host's own render already resolved instead of forcing an extra read at connect time.
- 0eb1de0: Fix `<lr-token-input>` marking a field touched (and committing a pending draft) from a blur the platform forces when the control becomes disabled while focused, which could trip a Lit dev-mode reentrancy warning.
- e849075: Fix `<lr-code-editor>` marking a field touched from a blur the platform forces when the control becomes disabled while focused, which could trip a Lit dev-mode reentrancy warning.
- f4b8304: Fix `<lr-phone-input>` marking a field touched from a blur the platform forces when the control becomes disabled while focused, which could trip a Lit dev-mode reentrancy warning.
- a9cd82e: Fix `<lr-file-input>` marking its dropzone touched from a blur the platform forces when the focused `[part="base"]` button becomes disabled, which could trip a Lit dev-mode reentrancy warning.
- 9c35472: Fix `<lr-emoji-picker>` marking a field touched from a blur the platform forces when the control becomes disabled while focused, which could trip a Lit dev-mode reentrancy warning.
- 5b96432: Fix `<lr-checkbox-group>` marking the group touched from a blur the platform forces when a focused child `<lr-checkbox>` becomes disabled -- either directly or via an ancestor `<fieldset disabled>` cascading down -- which could trip a Lit dev-mode reentrancy warning.
- 3d5b695: Fix `<lr-date-input>` marking the field touched from a blur the platform forces when the internal date text input becomes disabled while focused, which could trip a Lit dev-mode reentrancy warning.
- 8e1eb54: Fix `<lr-otp-input>` marking a field touched from a blur the platform forces when the control becomes disabled while focused, which could trip a Lit dev-mode reentrancy warning.
- 530c30b: Fix `<lr-voice-picker>` marking a field touched from a blur the platform forces when the trigger button or free-text combobox input becomes disabled while focused, which could trip a Lit dev-mode reentrancy warning.
- 614ee6a: Fix `<lr-switch>` marking a field touched from a blur the browser forces when the control (a form-associated custom element) becomes disabled while focused, which could trip a Lit dev-mode reentrancy warning and could flash `user-invalid` styling on a later re-enable for an interaction the user never had.
- 6149517: Fix `<lr-select>` marking a field touched from a blur the platform forces when the trigger becomes disabled while focused, which could trip a Lit dev-mode reentrancy warning.
- 1c58af5: Fix `<lr-combobox>` marking its field touched from a blur the platform forces when the internal input becomes disabled while focused, which could trip a Lit dev-mode reentrancy warning.
- aa345d8: Fix `<lr-locale-picker>` marking a field touched from a blur the platform forces when the control becomes disabled while focused, which could trip a Lit dev-mode reentrancy warning.

## 8.2.0

### Minor Changes

- 5944ba7: Add a `click()` override to five multi-control form wrapper elements —
  `<lr-radio-group>`, `<lr-checkbox-group>`, `<lr-rubric-form>`, `<lr-graph-query-builder>`, and
  `<lr-tool-param-form>` — so a host click (whether from a `<label for>` association or a
  programmatic `.click()`) reaches the first relevant internal control instead of being a no-op.
  `<lr-radio-group>` activates its selected (or first enabled) radio, matching its own `focus()`
  override; the other four move focus to their first field.

## 8.1.0

### Minor Changes

- 968d39c: Fix `<lr-locale-picker>`'s trigger button never rendering a flag for the currently selected
  locale — `showFlags` only ever affected the open listbox's rows, so a consumer relying on the
  default `show-flags` still saw a text-only trigger (e.g. "English") with no flag until the
  dropdown was opened. The trigger now renders the same `<lr-flag>` (new `trigger-flag` part,
  honoring a `country` catalog override exactly like the row does) that the matching row shows.

### Patch Changes

- b8028f8: Fix `DocumentAnchorTarget` (the shared mixin behind every viewer's `.scrollToAnchor()`) so a
  throwing `applyAnchor()` reliably degrades to a resolved `false` and still emits
  `lr-anchor-result:{found:false}`, instead of leaving the promise rejected and the documented
  "always reports a definite result" contract broken. A previous attempt at this (a blanket
  try/catch) was reverted because it made `lr-ebook-viewer`'s own override's localized
  rendition-failure alert unreachable; `scrollToAnchor()` is now split into a thin public wrapper
  carrying the safety net and a `performScrollToAnchor()` the mixin's own subclasses (currently only
  `lr-ebook-viewer`) can call directly to bypass it and keep full control of their own catch.
- a260188: `lr-archive-viewer` no longer binds an untrusted ZIP entry name as a DOM `id` (`renderEntry()` set
  `id=${entry.name}`, a classic DOM-clobbering primitive — a crafted archive entry named e.g.
  `"body"` or `"documentElement"` could shadow a global DOM property lookup for code elsewhere in the
  page). Fragment-anchor resolution (`scrollToAnchor()`'s `'fragment'` kind) now matches the target
  row by its rendered `textContent` instead of by `id`, and no longer delegates to the shared
  `TextViewerTarget` base's generic `id`-based fragment resolution for this component.
- 21ff77f: Harden three more components against untrusted values reaching a CSS sink unvalidated (same class
  of fix as the earlier ANSI-color/`align`/`open-link` hardening):

  - `lr-selection-toolbar` computed its floating position directly from a caller-supplied `rect`
    (`DOMRectReadOnly | null`, but nothing enforces that shape at runtime) into a `styleMap()`-bound
    custom property. A non-finite or non-numeric `left`/`top`/`width`/`bottom` could produce `NaNpx`
    or, since `styleMap()`'s first commit serializes the whole `style` value as one string, break out
    of the declaration. Both `coordinates()` and `updateToolbarPosition()` now coerce `rect` through a
    shared `safeRect()` helper before use.
  - `lr-data-grid`'s `columnStyle()` wrote a column's `width` into a `--column-authored-width` custom
    property with no numeric guard, unlike the sibling `gridTemplate` getter's own `Number.isFinite`
    check for the same field — inconsistent, and reachable by the same first-commit `styleMap()`
    string-injection class of bug above.
  - `lr-entity-card`'s data-driven type-badge color only rejected `;`/`{`/`}` structural characters,
    not `url(...)`, unlike every other color sink in this library. Now routed through the shared
    `sanitizeCssColor()` helper.

- 38d4511: Fix `<lr-flow-minimap>` not respecting a paired `<lr-flow-canvas>`'s `locked` state. Click-to-center,
  wheel-zoom, viewport-rectangle drag, and the viewport rectangle's keyboard controls now check the
  linked canvas's `locked` property before calling `setViewport()`/`zoomIn()`/`zoomOut()`/`fit()`,
  mirroring the same guard `<lr-flow-canvas>` already applies to each of its own gesture handlers.
  Previously the minimap relied entirely on the paired canvas separately gating those calls itself;
  a locked canvas now stays locked even if a `FlowCanvasLike` companion does not also guard its own
  methods. The `FlowCanvasLike` structural interface gained a read-only `locked` accessor to support
  this.
- fbcf0ef: Fix `installHappyDomFormAssociatedShims()`'s stub `ElementInternals` missing a `states`
  (`CustomStateSet`) property. Any form-associated component that calls
  `this.internals.states.add()`/`.delete()`/`.has()` (added in 8.0's custom-state work, e.g.
  `lr-input`'s `blank` state) threw on its very first update under the documented happy-dom test
  setup.
- 867f68c: `lr-icon` and `lr-icon-button` clone slotted custom SVG content into a real SVG namespace so it
  paints reliably (Chromium doesn't reliably paint slotted SVG geometry). That clone copied every
  source attribute verbatim, including event handlers (`onload`, `onclick`, ...) and `href`/
  `xlink:href`, with no sanitizer in the loop — unlike a fetched `src` document, which is already
  sanitized through DOMPurify. Both clone paths now drop event-handler and `href`/`xlink:href`
  attributes (a new shared `isUnsafeSvgCloneAttribute()` helper); every other presentational
  attribute (`d`, `fill`, `stroke`, `viewBox`, `transform`, gradient stops, ...) is unaffected.
- 21e6f07: Fix `<lr-knowledge-graph-explorer>`'s `[part="search-empty"]` "no matches" message rendering as a
  direct child of the `role="list"` `[part="search-results"]` container without `role="listitem"` --
  invalid ARIA, since every child of a list role must itself be `listitem` (or one of a small allowed
  set), unlike the real `[part="search-result"]` match rows which already carry it. It now carries
  `role="listitem"` too.
- 2e0d525: Fix `custom-elements.json` under-reporting `cssParts` for components that extend another
  component's class (e.g. `<lr-number-input>` extending `<lr-input>`, `<lr-dropdown>` extending
  `<lr-popover>`). The manifest-compaction step pruned any inherited-and-resolvable entry — including
  CSS parts — off a subclass's own declaration, on the assumption that a consumer would walk the JS
  `extends` chain to see the full contract, the same way it does for members/attributes. Unlike those,
  `::part()` has no such chain for its consumers (docs generators, editor tooling, `::part()` usage
  checks), which read a tag's `cssParts` list directly, per tag — exactly how `cssStates` already
  behaved. `<lr-number-input>` now declares `form-control`, `form-control-label`, `input-wrapper`, and
  `input` (inherited from `<lr-input>`) in addition to its own parts, and `<lr-dropdown>` now declares
  `trigger`, `popup`, `dialog`, `popup__popup`, `content`, `body`, `arrow`, and `popup__arrow`
  (inherited from `<lr-popover>`). 15 other components with the same inheritance shape (the icon
  charts, `<lr-native-time-input>`, `<lr-radio-button>`, `<lr-accordion-item>`, `<lr-dropdown-item>`,
  `<lr-tag>`, `<lr-drawer>`) gained the same correction. Generated docs (`llms/components/*.md`) and
  other manifest consumers were already unaffected, since they already read parts through
  `expandManifestInheritance()`; only the checked-in compact manifest itself was missing them.
- 6313c1b: `lr-avatar`'s `image`, `lr-attachment-chip`'s `thumbnail-src` and file-object preview URL, and
  `lr-flag`'s pre-resolved `src` are now validated through the shared `safeMediaSrc()` helper before
  reaching an `<img src>` sink, rejecting `javascript:`/other unsafe schemes. Each falls back to its
  existing placeholder state (initials, the generic file glyph, or an empty render) instead of
  rendering an unsafe URL.
- ad5a464: Fix partial child-event stopping in three components whose nested `<lr-virtual-list>`/child
  controls only had some of their bubbling events stopped at the host boundary:

  - `<lr-notebook-viewer>` and `<lr-page-rail>` each already stopped the nested `<lr-virtual-list>`'s
    `lr-visible-range-changed` event from leaking past the host, but left its `lr-scroll` event (and,
    for `<lr-page-rail>`, its `lr-load-more` event too) undocumented and free to bubble straight
    through. Both are now stopped the same way, mirroring the existing `lr-visible-range-changed`
    handling.
  - `<lr-query-builder>`'s add/remove condition buttons called `addCondition()`/`removeCondition()`
    directly from their `@click` handlers, bypassing the `consumeChildEvent()` helper every other
    handler in the component consistently uses to stop the raw composed child event before emitting
    the component's own wrapper event. The two buttons now route through `consumeChildEvent()` like
    the rest of the file.

  `retrieval-results.class.ts` and `tool-select-dialog.class.ts` already stop every child event
  consistently, so neither needed a change.

- d53cec6: `lr-spreadsheet-viewer` now validates that the optional `xlsx` peer's parsed `workbook.SheetNames`
  is actually an array of strings before using it, instead of trusting an unchecked type assertion.
  A malformed shape (a real risk here, since the workbook is parsed from consumer-supplied,
  untrusted `src` content) now surfaces the standard localized load-failure state instead of silently
  producing corrupted sheet tabs.
- d84fca9: Harden three components against untrusted values reaching CSS/URL sinks unvalidated:

  - `lr-terminal` and `lr-notebook-viewer`'s shared ANSI-segment styling (`segmentStyle()`) wrote a
    parsed stream's `fg`/`bg` color tokens directly into an inline `style` declaration; a
    crafted ANSI color escape could inject CSS syntax. Both now validate through
    `sanitizeCssColor()` before the value reaches `styleMap()`.
  - `lr-widget-renderer`'s agent-authored widget tree wrote an arbitrary `align` prop string
    directly into `align-items` with no allowlist. Now normalized through a bounded value map;
    an unrecognized value renders as unset rather than reaching the declaration list.
  - `lr-mcp-app`'s `open-link` message handler forwarded a `postMessage`-supplied `href` to
    consumers verbatim as long as it was a string, with no scheme validation. Now validated through
    `safeLinkHref()` (rejects `javascript:`/other unsafe schemes) before the `lr-mcp-open-link`
    event fires.

- 0e6d53e: Fix `lr-switch`'s thumb miscentering when a consumer styles the `track` part with a `border`.
  `box-sizing: border-box` (the library-wide default) let an added border eat into the padding box
  the thumb is absolutely positioned against, while the thumb's own size/travel math stayed derived
  from the track's declared (border-box) dimensions -- breaking symmetric clearance on the far edge
  in both the unchecked and checked states. The track part now uses `box-sizing: content-box`, so an
  added border grows the track's outer footprint instead of shrinking the space the thumb positions
  within, keeping clearance symmetric regardless of border width.
- 7f37a42: `<lr-table>`'s `TableColumn.cellStyle` hook now sanitizes every property/value pair before it
  reaches `styleMap()`: the property name must match a safe CSS-identifier shape, the value must
  contain no `;`/`{`/`}` structural characters or a `url(...)` function, and the browser must accept
  the property/value pair via `CSS.supports()` (falling back to a permissive regex where
  `CSS.supports` is unavailable). A custom property (`--foo`) is exempted from the `CSS.supports`
  check, since arbitrary custom-property values are always valid CSS.
- 7987719: Fix `lr-table` rows and `lr-tree-item` where hovering an already-selected row/item had no visible
  effect. The `:hover` rule and the selected-state resting rule both resolved to the same
  `--lr-color-brand-quiet` fallback at equal CSS specificity (and, for `lr-tree-item`, the
  `:host([aria-selected='true'])`-scoped selected rule outranked a bare `[part='row']:hover`
  outright), so the selected rule always won and hovering produced no change. Mirrors the fix already
  applied to these same files' `:active`-while-selected rules: the hover rule now also matches through
  the same specificity-matching selector arm (source order deciding the tie), and its resting fill is
  a distinct `color-mix()` step (using `--lr-color-mix-hover`) instead of the plain `brand-quiet`
  fallback, so hovering a selected row/item is visually distinguishable from its resting state.
- 4392216: Fix `<lr-tree>`'s `expandAll()` bypassing lazy-loading. It used to set `expanded = true` directly
  on every node, skipping `<lr-tree-item>`'s own `expand()` -- the only code path that emits
  `lr-lazy-load` and calls `beginLazyLoad()` for a `lazy` node whose children have not been fetched
  yet. A tree containing lazy nodes would render them visually expanded but empty after
  `expandAll()`, with their content never actually requested. `expandAll()` now calls each node's
  `expand()` directly, so a lazy node triggers the same load request whether it was expanded by a
  click or by `expandAll()`.

## 8.0.1

### Patch Changes

- Fix three bugs surfaced by the full Chromium/Firefox/WebKit browser-engine suite:

  - `lr-page-rail`: a `thumbWidth` change scheduled its thumbnail-state invalidation from a
    post-render hook, forcing an extra Lit update cycle instead of reflecting the new width in the
    same pass.
  - `lr-graph`: a pointer-capture call on canvas pointerdown was unguarded against browsers
    rejecting capture for a synthetic (non-driver) pointer id, unlike its sibling release call.
  - `lr-map`: a GeoJSON-view WebGL2 load-failure flag could stay set across a re-entrant load
    triggered while the real availability check was still racing a test override.

## 8.0.0

### Major Changes

- aa9f6ff: 8.0.0 — one styling vocabulary, a colour system that works in the dark, and a migration promise that is actually true.

  This release is mostly about removing accidents: names that meant two things, values that were only ever solved for light mode, and a mirror table that claimed more than the components could honour. Almost every break below is a rename with a mechanical fix.

  ### The migration promise

  All 145 pinned Web Awesome and Shoelace tags now have a checked migration classification. `scripts/check-migration-coverage.mjs` fails when the inventory, README relationship, or registered target drifts. The codemod applies only `exact` and fully specified `rewritten` mappings; `warning-required`, `conceptual-only`, and `unsupported` uses stay unchanged and are reported with their source location.

  Lyra 7 overlay defaults have their own explicit, opt-in codemod profile. Run
  `migrate-wa.mjs --origin=lyra-v7 --dry-run …` to preserve the old popup positioning and
  popover/tooltip arrow behavior before upgrading. It inserts only absent attributes, emits true
  booleans as presence (`flip`, `shift`, `without-arrow`), never rewrites an `lr-*` tag/import, and
  blocks on opaque spreads or DOM aliases rather than guessing.

  - **Both upstream spellings of the clear button are now accepted** on `lr-input`, `lr-select` and `lr-combobox`. Shoelace spells it `clearable`, Web Awesome spells it `with-clear`, and each control previously honoured only one — so half of all migrations silently lost the control. Neither spelling is deprecated: deprecating Web Awesome's own name would work against the promise.

  ### Complete mapped components without losing Lyra behavior

  Public names now describe one coherent contract. Where an existing Lyra component meant
  something different, its behavior remains available under a truthful tag instead of being
  deleted or silently mixed with the mapped API:

  - `lr-time-input` is now the locale-aware segmented time field; the former browser-native field
    is `lr-native-time-input`.
  - `lr-zoomable-frame` is now the sandboxed iframe preview; the former slotted/image inspection
    surface is `lr-pan-zoom`.
  - New `lr-split-panel` supplies the exact two-pane separator contract while the richer multipanel
    `lr-split` remains unchanged.
  - Accordion/Item, Tree/Item, Dropdown/Menu, Carousel, Popup/Popover/Tooltip, Dialog/Drawer,
    form controls, formatters, Include, Icon, Chart/Sparkline, Date Picker/Input, File Input and the
    remaining mapped helpers now carry their complete attributes, defaults, slots, events, parts,
    CSS properties, methods, native relays and form behavior.
  - New `lr-page` is an allocation-responsive semantic application shell with per-instance skip
    targets and shared-overlay mobile navigation.
  - New `lr-video` and `lr-video-playlist` use a shared generation-safe native-media controller,
    preserve platform media promises, cap remote thumbnail input, and keep inactive players
    unloaded so a playlist cannot overlap audio.
  - Experimental `lr-data-grid` returns with the complete mapped data-grid contract and full semver
    coverage; `lr-table` remains its smaller, independent table component.
  - Mapped writable IDLs are writable in both TypeScript and runtime behavior:
    `lr-select.selectedOptions` accepts exact live option occurrences without emitting user events,
    while `lr-combobox.validationTarget`, `lr-date-input.validationTarget`, and
    `lr-file-input.validationTarget` accept validity anchors with `undefined` restoring each
    component's internal default. Assigning `lr-popup.popup` is source-compatible but deliberately
    leaves the shadow-owned positioning/animation node authoritative.
  - `lr-option.defaultSelected` now maps the `selected` attribute (with non-reflecting property
    writes) to the parent combobox/select reset default, while `lr-option.selected` is property-only
    live state. User selection no longer rewrites the declarative default; changing
    `defaultSelected` after mount updates what `form.reset()` restores without clobbering a dirty
    live selection.
  - Mapped string setters accept upstream `null` writes without making reads nullable: `name` and
    `for` clear to `''`, ordinary string values clear to `''`, and checkbox/switch values restore
    their native absent-attribute `'on'` default.
  - Additional upstream write compatibility keeps canonical reads stable: breadcrumb `href`, icon
    `name`/`src`, icon-button `name`, and split-panel `snap` accept `undefined`; animation names
    accept arbitrary registered strings; badge/tag/rating/toast size and variant aliases normalize
    to Lyra's canonical values; and date-input accepts Web Awesome object validators, including
    their observed-attribute revalidation contract. The deprecated
    `lr-known-date::part(label)` alias remains on the `form-control-label` node and will not be
    removed before 10.0.0.

  Registration examples use granular component modules. The principal new and compatibility
  surfaces register from these exact paths:

  ```js
  import "@aceshooting/lyra-ui/components/lr-page.js";
  import "@aceshooting/lyra-ui/components/lr-video.js";
  import "@aceshooting/lyra-ui/components/lr-video-playlist.js";
  import "@aceshooting/lyra-ui/components/lr-native-time-input.js";
  import "@aceshooting/lyra-ui/components/lr-pan-zoom.js";
  import "@aceshooting/lyra-ui/components/lr-split-panel.js";
  import "@aceshooting/lyra-ui/components/lr-alert.js";
  ```

  The component-family references contain the exact surface and the 7.x migration notes for
  changed defaults and event timing.

  ### Security-preserving differences

  Migration never trades away Lyra's stronger defaults. `lr-include` still sanitizes every
  fragment, has no script-executing mode, and defaults to same-origin fetches; independently
  authored link `rel` values are not copied onto controls that derive
  `rel="noopener noreferrer"` from `target`; iframe/media URLs and remote thumbnail input remain
  validated and bounded. Those uses receive location-aware warnings instead of silent rewrites.

  ### Platform and distribution

  - Root and granular imports are server-safe. `ssr-loader.js` publishes an exhaustive
    render-and-hydrate/client-render matrix, installs Lit hydration support in the browser, and
    exposes diagnostics; CI verifies server rendering and real-browser node reuse.
  - Generated opt-in React JSX, Vue and Svelte declarations come from the same Custom Elements
    Manifest as editor data. The manifest has its own package export and packed consumers exercise
    each framework surface without runtime wrappers.
  - The side-effect-free manual autoloader and guarded CDN entry discover only inventory-known
    rendered tags; optional peers remain opt-in. `allDefined()` is also available as a readiness
    barrier after explicit or automatic registration.
  - `native.css` and `utilities.css` are independent opt-in, layered, tokenized assets. Neither is
    imported by the root entry or installs a page-wide reset.
  - A public animation registry supports page and per-element overrides, logical RTL keyframes and
    reduced motion without changing component lifecycle events.
  - Overlay consumers share rendered-state suspension, and third-party modal systems can use the
    scoped, nestable `suspendLyraModalsFor()` handle from
    `@aceshooting/lyra-ui/utilities/overlay-manager.js` without abandoning Lyra's focus/inert stack.
  - Canvas renderers watch theme/style/link/CSSOM/adopted-sheet/media-query changes. A host theme
    engine with an otherwise unobservable mutation can call `invalidateLyraTheme()` from
    `@aceshooting/lyra-ui/utilities/theme.js` for the owning document realm.
  - Component status, history-derived `since`, and actionable deprecation metadata flow through
    CEM, Storybook, editor data and the agent reference. Stable and experimental published APIs both
    receive semver protection; a deprecation names its replacement and cannot be removed before the
    major after the complete following major release line.
  - Registration imports, allowlists, package side effects, migration classifications and the
    component scaffold are inventory-driven and freshness-checked.

  ### Design-system operations and integrations

  - Every public tag has a stable tag-shaped registration entry point such as
    `@aceshooting/lyra-ui/components/lr-input.js`. Existing family registration paths remain
    supported, while class-only modules stay available at their family paths.
  - Public optional-peer APIs now use Lyra-owned structural interfaces instead of leaking peer
    implementation types or `any`. They remain compatible with the supported Chart.js, Marked,
    Shiki, MapLibre, D3, DOMPurify and document-viewer peers without making those packages required
    dependencies.
  - Event contract checks now compare concrete detail schemas across event maps, class JSDoc and
    the Custom Elements Manifest, reject `any`, and preserve native `Event`/`InputEvent`
    constructors for native relays.
  - The theme runtime distinguishes system-following `auto` from cascade-owned `unset`, supports a
    validated brand accent, and provides reusable built-in or application-defined presets through
    the same persistence and change-event path.
  - An authored canonical token inventory generates DTCG-compatible interchange, fixture CSS,
    Storybook token data and stable documentation/editor inputs, with freshness checks against the
    production token styles.
  - Synthetic `en-XA` and mirrored RTL `ar-XB` pseudo-locales exercise string expansion,
    interpolation and direction without presenting themselves as human translations.
  - Runnable React 19, Vue and Svelte examples verify granular registration, typed properties and
    typed custom events against the packed package.
  - Generated component quality and integration references expose recorded accessibility,
    browser, SSR, optional-peer, dependency and bundle evidence without converting missing human
    review into a pass.
  - Governance, support and RFC documents define the contribution and compatibility process, and
    the bundled composition skill teaches consumers to assemble Lyra interfaces from stable public
    entry points.

  ### Attribute renames (breaking)

  Every one of these is a find-and-replace. Where the default flips, the behaviour is the upstream one.

  | 7.x                                                            | 8.0.0                   | Note                                                                                                                                                                                |
  | -------------------------------------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `no-light-dismiss`                                             | `light-dismiss`         | polarity un-inverted; default flips to off                                                                                                                                          |
  | `hide-summary`                                                 | `with-summary`          | polarity un-inverted; default flips to off                                                                                                                                          |
  | `total-items`                                                  | `total`                 |                                                                                                                                                                                     |
  | `<lr-avatar src>`                                              | `<lr-avatar image>`     |                                                                                                                                                                                     |
  | `<lr-drawer>` default `placement`                              | `start` → `end`         | matches the upstream default                                                                                                                                                        |
  | `<lr-tabs>`                                                    | `<lr-tab-group>`        | plus `lr-tabs-change` → `lr-tab-show`/`lr-tab-hide`, and `--lr-tabs-*` → `--lr-tab-group-*`                                                                                         |
  | `<lr-tree-node>`                                               | `<lr-tree-item>`        |                                                                                                                                                                                     |
  | `<lr-slider>` `fill` part                                      | `indicator` part        |                                                                                                                                                                                     |
  | `<lr-callout variant="danger">` `[part="base"]` `role="alert"` | shared live-region sink | announcements now flow through the shared light-DOM live-region sink instead; `[part="base"]` carries `role="group"` (only when the host has an accessible label) or no role at all |
  | `<lr-spinner>` `part="base"`                                   | `part="base spinner"`   | an exact-match `[part="base"]` attribute selector no longer matches; `::part(base)` is unaffected. Also gained `role="progressbar"`                                                 |

  The JavaScript-only string property `.autoCorrect` on `lr-input`, `lr-textarea`, and
  `lr-combobox` is now the upstream-compatible `.autocorrect` IDL, which always reads as boolean.
  Web Awesome boolean writes remain canonical. For a prefix-only Shoelace migration, `lr-input`
  also accepts `'off'`/`'on'` writes and `lr-textarea` accepts its complete string write surface;
  both normalize the result back to a boolean. The HTML attribute name remains `autocorrect`; use
  `autocorrect="on"` / `autocorrect="off"` in markup.

  ### Colour, in both modes

  The palette is generated from a numeric ramp and a 45-slot semantic grid, and `scripts/check-contrast.mjs` proves 367 pairs across light and dark on every run. Colours shifted where the generated ramp put them — the brand seed moved `#0969da` → `#035ec6`. Restoring the old hexes would reintroduce the failures the ramp exists to prevent.

  - **Dark mode had no elevation.** `--lr-shadow` was a single black step at 12–22% alpha, which against a near-black surface is not a luminance difference at all. There are now five steps (`--lr-shadow-xs` … `-xl`), declared per mode with roughly tripled dark alphas, and every call site is tiered by role. A theme that set `--lr-theme-shadow-color` keeps working; one that overrode `--lr-shadow` directly should move to the step it meant.
  - **Modal panels no longer share the page surface token.** New `--lr-color-surface-overlay`: the page surface in light mode, lighter than the page in dark. Before this, an open dialog in dark mode was a scrim with text floating on it and no visible panel. Both scrims (`--lr-color-overlay`, `--lr-color-overlay-strong`) also gained dark values.
  - **The terminal's ANSI palette is now two token sets.** `--lr-terminal-color-*` remains the foreground set; SGR 40-47/100-107 now read a new `--lr-terminal-bg-*` set solved against the panel's default text. One set could not serve both roles: once the foregrounds were solved to be legible on a light panel they were all dark, so `ESC[41m` painted a near-black red behind near-black default text. `white` and `black` also no longer resolve to the same hex, which had made `ESC[30;47m` invisible text on its own colour.

  ### Interaction states

  `filter: brightness()` is gone as the hover mechanism. It multiplies every channel — so it lightened a dark control and darkened a light one only by coincidence, did nothing whatsoever to a pure white or black fill, and shifted the control's text and icons along with its background. Hover and press are now a colour mix toward `--lr-color-mix-partner`, tunable library-wide through `--lr-color-mix-hover` / `--lr-color-mix-active`. `--lr-hover-brightness` still resolves but no component reads it.

  Every interactive part that responds to `:hover` now also responds to `:active`, enforced by `scripts/check-interaction-states.mjs`.

  ### One styling vocabulary

  `variant`, `tone` and `kind` meant the same thing on different components; `appearance` meant two unrelated things; twenty-two size unions covered four different ladders. The shared vocabulary now lives in one place.

  - **`tone` → `variant`** on `lr-avatar`, `lr-avatar-group`, `lr-chip` and `lr-confirm-bar`, and on the activity-feed / tree-badge data fields. No alias.
  - **`appearance="card|plain"` → `frame="card|plain"`** on the thirteen container components that had it. `appearance` now means the fill vocabulary only: `accent | filled | outlined | filled-outlined | plain`.
  - **`lr-button`'s default `appearance` is now `accent`**, and `filled` is a genuinely different, quieter tier — the two used to render identically for every variant except neutral, where `filled` resolved to the page background.
  - **`size` accepts both ladders everywhere**: the library's `2xs|xs|s|m|l|xl` and Web Awesome's `small|medium|large`.
  - **Numeric properties that were misnamed `size` are renamed**: `lr-attachment-chip` and `lr-file-icon` take `bytes`; `lr-dock-panel` takes `extent` (with `min-extent`/`max-extent`).
  - **`lr-badge`, `lr-tag` and `lr-chip` are no longer unconditionally pill-shaped.** They default to a rounded rectangle; add `pill` for the old shape.

  ### Tokens and theming

  - **CSS cascade layers.** `theme.css` declares `@layer lr-base, lr-theme, lr-utilities, lr-overrides` and its tokens sit in `lr-theme`. Any _unlayered_ consumer declaration now beats every Lyra one regardless of specificity or load order. If you previously wrapped your overrides in your own `@layer`, they now sort relative to `lr-theme` instead of losing to an unlayered `:root`.
  - **Compound motion tokens are split** into duration and easing.
  - **`--lr-font-size-md` is removed**; use `--lr-font-size-m`. The two were the same value under two names, which is why `lr-button` rendered `size="m"` and `size="l"` at identical text sizes.
  - **New `--lr-form-control-*` tier** (height, font-size, padding, gap, radius), one ladder shared by every control.
  - **The required-field marker is themeable.** The `*` every labelled control appends to its label reads `--lr-form-control-required-content` (default `' *'`), `--lr-form-control-required-color` (default `--lr-color-danger`) and `--lr-form-control-required-offset` (default `0`) — so a locale that wants `' (required)'`, a design that wants the marker in the label's own colour, or a form that wants no marker at all is one declaration, not a per-component override. Deliberately undeclared rather than given a `--lr-theme-*` input: an undeclared custom property inherits, so a single `:root` rule retunes every marker in the application; declaring them on `:host` would have made the host's own value win and cut off exactly that route.

  ### Localization

  - **Pluralized messages are now CLDR category objects**, selected through `Intl.PluralRules`, replacing the paired `<key>` + `<key>Plural` convention. A catalog registered through `registerLyraLocale()` or a per-instance `.strings` that used the old pair must be rewritten as `{ one: '…', other: '…' }` — with that locale's real categories, which for Russian is four and for Arabic six.
  - **Ten complete translation catalogs ship**, as side-effect-only modules: `import '@aceshooting/lyra-ui/translations/de.js';` and so on for `ar`, `es`, `fa`, `fr`, `he`, `ja`, `pt-BR`, `ru`, `zh-CN`. Persian and Hebrew add regional fallback and RTL coverage; set `dir="rtl"` explicitly because locale selection does not change writing direction.

  ### Form validation

  - **`lr-invalid` is now `cancelable`.** It is the alias for the platform's own `invalid` event, which is cancelable, and cancelling a copy of an event can only honestly mean cancelling the original — so `event.preventDefault()` on `lr-invalid` now forwards to the native event and suppresses the browser's validation bubble and `reportValidity()`'s focus/scroll. An app that wired `lr-invalid` to its own error banner previously had no way to stop the native UI appearing alongside it. Nothing breaks by adding cancelability, but a listener that already called `preventDefault()` speculatively now actually vetoes something.
  - **A control barred from constraint validation no longer publishes `:state(invalid)` / `:state(user-invalid)`** — nor `valid`/`user-valid`. Disabled, fieldset-disabled and readonly controls match neither `:valid` nor `:invalid` natively (verified against real `<input required disabled>` and `<input required readonly>`), and publishing `invalid` from one is what made the documented `lr-input:state(user-invalid) { border-color: red }` rule paint every disabled required field red. `:state(required)` / `:state(optional)` describe the attribute rather than the outcome, so they keep publishing exactly like native `:required`/`:optional`. A stylesheet that relied on the old behaviour to style disabled fields must select `:disabled` instead. The `readonly` bar had also been copy-pasted per component and missed one — `<lr-rating required readonly>` reported `valueMissing` while `<lr-otp-input required readonly>` did not; both now route through one shared predicate.

  ### Registration is no longer a side effect of the package root

  **`import '@aceshooting/lyra-ui'` registers nothing. Rewrite it as `import '@aceshooting/lyra-ui/all.js'`.**

  The package root is now a pure export surface. In 7.x it was both: importing it for a type also defined all 268 root-included tags, which meant a consumer could not name a class or an event-map type without conceding the whole library to their bundle. Those 268 registration side effects now live in `all.js`, and `package.json#sideEffects` moved with them, so the root is genuinely free to import.

  - Every named and type export stays on the root, under the same specifier, with the same name. `import { LyraSelect, type LyraSelectEventMap } from '@aceshooting/lyra-ui'` is unchanged — it just no longer registers anything. Only the side effect moved.
  - `all.js` registers the same 268 tags the root used to, and still excludes the same 15 optional-peer-family tags (`lr-chart` and its 8 typed subclasses, `lr-box-plot`, `lr-histogram`, `lr-map`, `lr-graph`, `lr-knowledge-graph-explorer`, `lr-geojson-view`), which keep requiring their own granular import. Granular per-component imports remain the recommendation; `all.js` exists so an application can upgrade in one line, not because 268 elements is a sensible bundle.
  - **A missed migration does not throw.** The import still resolves, the build still succeeds, and the tags simply never upgrade — an unknown inert `<lr-select>` with its light DOM showing through and nothing in the console. That silence is what makes this the nastiest item in the release: every rename above fails loudly, this one does not.
  - `ssr-loader.js` is unaffected — it still installs Lit's hydration hook and then pulls the full `all.js` closure, exactly as before. A server integration that wants granular registration can import the new `@aceshooting/lyra-ui/hydration.js` first instead, then only the components it renders; `@aceshooting/lyra-ui/ssr/all.js` is the server-side convenience that registers the complete 283-tag inventory.

  ### Packaging

  - **`@aceshooting/lyra-ui/internal/*` is no longer a published subpath.** The supported helpers live under `@aceshooting/lyra-ui/utilities/*`, which now also carries `FormAssociated` and `groupByRecency` — previously reachable only through the side-effectful root barrel.
  - **`lr-flag`'s deprecated `detailed` boolean is removed**; use `variant="detailed"`.
  - **`lr-combobox`'s `withClear` alias is removed** in favour of the two upstream spellings above.
  - **Published tarballs no longer contain source maps.** `package.json#files` ships `dist` and not `src`, so every `.js.map` / `.d.ts.map` pointed at a `../../../../src/**/*.ts` path that does not exist in an install and carried no `sourcesContent` — dead weight, and actively worse for `declarationMap`, which routed an editor's Go-to-Definition at that missing `.ts` and failed there instead of falling back to the readable `.d.ts`. Maps stay on for local type-checking and docs; only the published artifact drops them.
  - **`emit()` is type-checked against each component's event map.** `LyraElement<Events>` now checks both the event name and the detail shape against the same map that types `addEventListener`, so a misspelled name or a detail that disagrees with the JSDoc, manifest and docs is a compile error rather than an event nobody listens for. This is a `protected` member, so it reaches consumers only through subclassing: a subclass that emits an event outside its base class's map must declare its own map. A component that declares no map keeps the permissive default.

## 7.8.1

### Patch Changes

- d699c7c: Stop letting a throwing `ShadowRoot.activeElement` getter escape a component. `ShadowRoot.activeElement`
  is not universally safe to read: under happy-dom 20.11.1 — the DOM a large share of consumers get by
  default from Vitest — that getter _itself_ throws `TypeError: Cannot read properties of undefined
(reading 'getRootNode')` whenever the document has no active element. Optional chaining was no
  defence, because `root?.activeElement` only guards `root` being nullish and the throw happens
  _inside_ the getter, after `?.` has already decided to proceed.

  Because these reads live in `willUpdate()` and in keydown handlers, the symptom was not a failed
  assertion but an _unhandled rejection_: one downstream suite reported 120 in a single run, all the
  same stack, from an `<lr-segmented>` re-rendering after its items changed. The suite still passed
  while the runner exited non-zero, and the stack pointed at library internals rather than anything
  the consumer wrote.

  Reported against `<lr-segmented>`, but a sweep found the same read at **every** focus-rehoming,
  roving-tabindex and focus-restoration site in the library — 30 modules across 11 families, including
  `<lr-tabs>`, `<lr-stepper>`, `<lr-table>`, `<lr-tree>`, `<lr-graph>`, `<lr-combobox>`'s siblings and
  the shared overlay manager. All of them now read through a new internal helper that returns `null`
  instead of throwing; `<lr-tree>`'s nested-shadow-root walk was the worst case, reading the raw
  getter in its _loop condition_ where a guard on the assignment alone would not have helped.

  Returning `null` is the honest answer: a DOM that cannot say what is focused is indistinguishable,
  for these call sites, from one where nothing is — and every one of them already handles that as the
  ordinary state, so the guard degrades to skipping focus restoration. Real browsers never take the
  catch, so behavior there is unchanged.

- 21b1051: Declare `flag-peer.js` in `package.json#sideEffects`, so `<lr-flag>` still resolves images in a
  production build. `sideEffects` is an explicit allowlist, and every entry in it was derived from a
  `*.class.ts` file's sibling registration module. `flag-peer.ts` has no `*.class.ts` of its own, so
  neither the generator nor the completeness check ever visited it and it shipped undeclared. It is a
  side-effect-only module — a consumer writes a bare `import '.../flag-peer.js'` and reads no export
  — so any bundler honoring `sideEffects` dropped it outright. `setFlagUrlResolver()` then never ran,
  `loadFlagUrlResolver()` cached `Promise.resolve(null)`, and every `<lr-flag>` given a
  `country`/`language` rendered the localized "flag unavailable" alert instead of an image. Silently:
  that null-resolver path logs nothing, and dev servers don't tree-shake, so it only ever appeared in
  a built artifact.

  Both scripts now derive `*-peer.ts` and `*-register.ts` modules, plus the per-family
  `components/<family>/index.ts` barrels, straight from the file tree rather than carrying them over
  from the previous `package.json` — so a rename or a family move can't strand an entry again. The
  completeness check fails on the missing `flag-peer` entries before the fix and passes after.

- 899543f: Centre content that a hit-area floor makes narrower than its own box, in `<lr-calendar>`,
  `<lr-citation-badge>`, `<lr-entity-chip>`, `<lr-rating>` and the `<lr-chart>` / box-plot legend
  items. These carry the same defect reported against `<lr-widget>`'s view toggle: a flex part with a
  `min-inline-size: var(--lr-icon-button-size)` floor (a WCAG 2.5.8 tappable-size requirement, not a
  layout intent) but no `justify-content`, so whenever the content is narrower than that floor the
  default `justify-content: normal` — resolving to `flex-start` — dumps every pixel of slack on the
  trailing side.

  `<lr-calendar>` was the most visible: its month-nav buttons hold a single chevron glyph and rendered
  **8.8px** off centre, sitting right next to a symmetric month title. `<lr-citation-badge>` left its
  one- or two-digit number hugging the badge's leading edge, and `<lr-entity-chip>` did the same to a
  short entity label inside an otherwise symmetric pill.

  Adding `justify-content: center` only changes rendering in precisely the buggy case: once content
  already fills or exceeds the floor there is no slack left to redistribute and the declaration is a
  no-op, so every component whose content was already wide enough renders exactly as before. The chart
  legends were checked for the overflow case specifically — long series names wrap rather than
  overflow, and both legends are wrapping horizontal rows, so per-item centring cannot make a column
  of items ragged.

  The sweep also cleared roughly ten other parts carrying the same floor where `flex-start` is
  correct — full-width header and list rows, whose content should start-align regardless.

  Rendered-geometry regression tests (measuring the glyph's centre against its button's) cover
  `<lr-widget>` and `<lr-calendar>`; both reproduce the offset without the fix, the widget one at the
  same 4.5px the report cited.

- 8a67993: Stop driving mask alpha from `--lr-color-shadow` in `<lr-segmented>`, `<lr-tabs>`, `<lr-stepper>`,
  `<lr-timeline>` and `<lr-document-preview>`. All five used `var(--lr-color-shadow)` for the
  _opaque_ stops of a `mask-image` gradient — 22 references across 12 declarations. A mask reads
  alpha only, but that token is a documented consumer theming input (`--lr-theme-color-shadow`) whose
  job is coloring shadows: setting it to something translucent such as `rgb(0 0 0 / 0.25)`, entirely
  reasonable for a shadow color, silently dropped the mask alpha across the _entire_ element rather
  than just its edges. Every affected component then rendered uniformly washed out — indistinguishable
  from a broken disabled state, with nothing pointing back at the shadow token as the cause. It worked
  only because that token's default happens to be opaque black.

  The opaque stops now use a new `--lr-mask-opaque`, declared in the internal tokens sheet.
  Deliberately **not** themeable and deliberately not a second alias of the shadow token: "opaque" is
  not a design decision a consumer tunes — a mask's opaque stop must be opaque by definition — so
  giving it its own `--lr-theme-*` hook would just reintroduce the same footgun under a new name.

  `<lr-document-preview>`'s determinate progress ring was the least obvious casualty: its mask punches
  the ring's centre out, so a translucent shadow theme faded the whole ring rather than cutting a hole
  in it.

  Regression-tested in all five components by rendering under `--lr-theme-color-shadow: rgb(0 0 0 /
0.25)` and asserting the resolved computed mask, which reproduces `rgba(0, 0, 0, 0.25)` at the
  opaque stops without the fix.

- 8a67993: Paint the horizontal edge fade only while the track actually overflows, in `<lr-segmented>`,
  `<lr-tabs>`, `<lr-stepper>`, and `<lr-timeline>`. All four applied their `--lr-scroll-fade-size`
  `mask-image` unconditionally, described in-code as an intentionally static, observer-free
  affordance. That is only harmless when there _is_ overflow. On a track that fits, the fade is pure
  damage: at the `2rem`-per-edge default a two-option `<lr-segmented>` (`Overall | Daily`) is
  narrower than its own two fades, so both labels rendered half-transparent and the control read as
  permanently disabled; a short `<lr-tabs>` row dimmed its first and last tab for no reason.

  A new internal `ScrollOverflowController` measures `scrollWidth` vs `clientWidth` and toggles a
  `data-scroll-overflow` attribute on the track (inside the shadow root — not consumer-visible DOM),
  which now gates each mask rule. It re-measures from two sources, because they catch different
  changes: a `ResizeObserver` on the track for container resizes, and the host's own update cycle for
  content changes, which need not alter the track's border box at all. Overflowing tracks keep
  exactly their previous rendering.

  Note for anyone spying on `ResizeObserver` construction: `<lr-stepper>` now arms one of its own
  regardless of the `orientationBreakpoint` feature.

- 899543f: Center the glyph in an icon-only `<lr-widget>` view toggle. `[part="view-toggle"]` set
  `align-items: center` but no `justify-content`, unlike the sibling `collapse-button` /
  `fullscreen-button` rules, which set both. `min-inline-size` floors the pill at the square
  icon-button size, so a 13px glyph inside a 40px pill has slack that the default
  `justify-content: normal` (→ `flex-start`) dumps entirely on the trailing side — measured 4.5px off
  true center once the asymmetric inline padding is counted, and plainly visible as an off-center
  icon in a round toggle. A labeled toggle was never affected: its content already fills a pill that
  sizes to fit.

## 7.8.0

### Minor Changes

- 2c3934b: `<lr-embedding-explorer>`'s `height` property now actually sizes the plot. It was rendered as an
  SVG `height` presentation attribute while the component's own stylesheet declared
  `[part='plot'] { block-size: auto }` — and any stylesheet declaration outranks a presentation
  attribute, so the property was inert at every value, including its documented `360px` default: the
  plot always sized itself from the `viewBox` aspect ratio instead.

  `height` is now published on the host as the new `--lr-embedding-explorer-height` custom property,
  which `[part='plot']`'s `block-size` reads. Consequences worth knowing before upgrading:

  - The default `height="360px"` now takes effect, so a plot wider than 640px is no longer as tall as
    its allocation implies. Set `height="auto"` to keep the previous aspect-ratio-preserved sizing.
  - A value the browser cannot parse as a `block-size` is dropped rather than applied, leaving the
    `auto` behavior instead of collapsing the plot.
  - A consumer's own `::part(plot) { block-size: ... }` rule still overrides `height`, and the
    narrow-allocation `min-block-size` floor still raises it.

- 2c3934b: `<lr-memory-panel>` no longer strands keyboard focus when a row action opens its confirmation step.
  Activating "Add to long-term memory", "Remove", or "Forget all" destroys the button that had focus,
  and nothing moved focus into the `lr-confirm-bar` that replaces it, so focus fell back to `<body>`:
  a keyboard user was dumped at the top of the page with nothing announced, and had to re-tab through
  the whole document to reach the confirmation they had just opened. Focus now moves into the
  confirmation (its Deny control -- the safe action -- falling back to the bar's status element), and
  is handed back to the row (or to the "Forget all" control) once the decision resolves. Pressing
  Escape while the confirmation holds focus now cancels it exactly like pressing Deny: no event is
  emitted, focus returns the same way, and the key does not propagate past the panel.
- 2c3934b: `<lr-notebook-viewer>`: `searchNext()` and `searchPrevious()` now resolve `true` when the active
  match moved and `false` when there was nothing to move to, matching the shared viewer search
  contract (`LyraTextViewerTarget`) that every other searchable viewer already honors. They
  previously returned nothing, so a find-in-page host driving several viewers polymorphically —
  `if (await viewer.searchNext()) { ... }`, or awaiting the call before reading its own match
  counter — got `undefined` from the notebook viewer alone and took its falsy "no more matches"
  branch on every press, even mid-notebook.

  This is an additive widening: the methods return a resolved promise instead of nothing, and callers
  that ignored the return value are unaffected. `search()` already resolved the match count and is
  unchanged.

- 80e0ef1: Repair four regressions left by earlier fixes:

  - `<lr-radio-group>`: arrow-key selection now emits `input` and `change` alongside `lr-change`, as
    click and Space already did and as native `<input type=radio>` does. The earlier fix for a
    duplicate `lr-change` had left the keyboard path emitting only the group event, so a consumer
    bound to the native-mirroring events silently missed every keyboard selection.
  - `<lr-progress-ring>`: an unslotted ring is named from the localized fallback again. Its slot's
    fallback content is the formatted percent, and `assignedNodes({flatten:true})` returns fallback
    children when nothing is assigned, so the control had been naming itself "40%" and no
    `registerLyraLocale()` override could reach it.
  - `<lr-tour>`: opening a detached tour no longer locks scroll on the document or installs a global
    Escape handler with nothing visible, matching the guard `<lr-dialog>` already had.
  - `text-quote` anchors and highlights now case-fold with the component's locale in
    `<lr-docx-viewer>`, `<lr-pdf-viewer>`, `<lr-markdown>` and `<lr-markdown-core>`. Under `lang="tr"`
    a quote of "istanbul" silently failed to match "İSTANBUL" in these four while resolving correctly
    in every viewer built on the shared text-viewer mixin.

- d8a026d: Give `<lr-table>`'s sorted column header an opaque default fill. The header is `position: sticky`
  and the sorted-state rule defaulted to `transparent`, so in any height-capped table the body rows
  scrolled visibly through the sorted column's header cell.

  Give `<lr-pdf-viewer>`'s toolbar buttons a hover fill that differs from the toolbar behind them —
  the rule existed but resolved to the toolbar's own opaque token, so hovering produced no visual
  change at all. Retunable via the new `--lr-pdf-viewer-toolbar-button-hover-bg`.

  Correct three `<lr-chat-message>` snippets in the authored reference that used `role="user"`. The
  property reflects to `data-role`, so `role` was never observed: consumers copying those examples
  got a message rendered as the default `assistant`, plus an invalid ARIA role token in the DOM.

- 2c3934b: `<lr-xml-viewer>`, `<lr-av-player>`, and `<lr-terminal>` now resolve a boolean from `searchNext()`
  and `searchPrevious()`, matching the shared `LyraTextViewerTarget` search contract that
  `search()` already followed on all three. They returned `void`, so a host driving several
  searchable components through that one typed surface — `if (await viewer.searchNext())` — read
  `undefined` and took the "nothing to move to" branch on every press.

### Patch Changes

- ed7f463: Stop retaining one live `Range` per search match in every text viewer. `<lr-archive-viewer>`,
  `<lr-calendar-viewer>`, `<lr-contact-viewer>`, `<lr-email-viewer>`, `<lr-geojson-view>`,
  `<lr-html-viewer>`, `<lr-include>`, and `<lr-pptx-viewer>` share a search mixin that held a live
  `Range` for every match. The engine revalidates each retained `Range` on every DOM mutation in its
  document, so a short query over a long document made every later mutation dramatically slower.
  Matches are now kept as inert offsets, and only a bounded window around the active match is
  materialized and painted. `matchCount`, `searchNext()`, and `searchPrevious()` still cover every
  match.
- 2d149dc: `<lr-dashboard-grid>` no longer starts a cell drag when the pointer lands on a button, link, or
  input inside the cell. The guard compared a slotted light-DOM control against a shadow-root wrapper
  with `contains()`, which never crosses the slot boundary, so it could never fire and every control
  click inside a draggable cell dragged the cell instead of activating the control.

  `<lr-tree>` no longer throws on the first arrow key when a `<lr-tree-node>` is written declaratively
  into its documented slot. `item` is `attribute: false`, so such a node has none until a host assigns
  one, and the keyboard handler read `item.id` unguarded.

- 9083c9b: `<lr-document-library>` now sorts its Updated column chronologically. It ordered rows correctly by
  timestamp itself, then handed the composed `<lr-table>` both those rows and a `sortKey` without
  `sort-mode="server"`, so the table sorted them a second time in client mode — from the column's
  rendered output, which is a _formatted_ date. The result was alphabetical by month name.
- 2c3934b: `<lr-env-list>` no longer paints its screen-reader-only "Value hidden" announcement as visible text
  beside the mask. The template emitted `class="sr-only"` but the component never adopted the shared
  stylesheet that defines that class, and no rule in `LyraElement.styles` supplies it.
- 2c3934b: Hide the anchor-announcement live region in `<lr-include>` and `<lr-pptx-viewer>`.

  Both viewers render the shared anchor-target mixin's `role="status"` live region, which the mixin
  marks up with `class="sr-only"`, but neither component's shadow stylesheet defined that class. The
  region therefore laid out as an ordinary block, so the first anchor jump (or a failed one) painted
  its localized announcement — "Jumped to highlighted passage." / "Passage not found in this
  document." — as visible body text: beside the transcluded fragment for `<lr-include>`, and as an
  extra row under the fidelity notice for `<lr-pptx-viewer>`. The announcement is now visually hidden
  and screen-reader-only, matching every other viewer that adopts the same mixin.

- 0e2dbf3: `LyraElement`'s internal `scheduleAfterUpdate()` now coalesces per key instead of collapsing every
  caller in an update cycle onto one slot. It tracked pending work in a single boolean, so the second
  caller in a cycle early-returned and its callback was dropped and never replayed — a component that
  scheduled two genuinely different pieces of after-update work silently lost one of them. Repeated
  schedules under the same key still collapse to one run, so the load path keeps producing one fetch
  per cycle rather than one per property write.
- 2cd5fb5: `<lr-artifact-panel>`, `<lr-commit-card>`, `<lr-heatmap>`, `<lr-query-builder>`, `<lr-tree>`, and
  `<lr-word-cloud>` now format the numbers they interpolate into localized strings with the effective
  locale. `localize()` substitutes values with a bare `String(value)` and does no number formatting,
  so these rendered Western digits inside otherwise fully-translated sentences — under a locale using
  its own numbering system (`ar-u-nu-arab`, `hi-u-nu-deva`, …) a single announcement mixed two digit
  sets.

  `<lr-attachment-chip>` also no longer falls back to an empty `src` on its thumbnail `<img>`; an
  empty `src` is a valid URL that resolves against the document, so it would make the browser
  re-request the page as an image.

- df4dac8: Reject non-integer index segments in anchor resolution. A range-only guard (`i < 0 || i >= len`)
  does not reject `NaN` (both comparisons are false) or a fractional index, so `<lr-xml-viewer>`
  reported `lr-anchor-result { found: true }` and announced "Jumped to…" for a `node-path` that
  matched nothing, and a non-trailing bad segment threw — rejecting `scrollToAnchor()` so
  `lr-anchor-result` never fired at all, and surfacing as an unhandled rejection on the declarative
  `anchor` path. `<lr-notebook-viewer>` had the same false-positive shape, and
  `<lr-virtual-list>.scrollToIndex(NaN)` silently scrolled the list to the top.

  `DocumentAnchorTarget` now also degrades a throwing `applyAnchor()` to "not resolved" instead of
  letting it reject, so the mixin keeps its documented promise of always reporting a definite result.

- c76ebf8: Fix `<lr-mcp-app>`'s remote `src` mode, which never loaded: binding `srcdoc` to an empty string
  still produced a _present_ `srcdoc=""` attribute, and the HTML spec's iframe processing branches on
  that attribute's presence, so the frame navigated to `about:srcdoc` and ignored `src` entirely
  (while still firing `lr-mcp-ready`). The same empty-string-vs-absent shape is fixed in
  `<lr-av-player>` (a bare player painted a "Failed to load the media" alert before a `src` was set)
  and `<lr-zoomable-frame>` (a rejected `src` rendered a broken-image glyph).

  Validate consumer-supplied CSS lengths before they reach an inline style declaration list, so a
  crafted value can no longer inject extra declarations: `<lr-stack-trace>`'s `max-height`,
  `<lr-code-block>`/`<lr-code-block-core>`'s `max-height`, `<lr-table>`'s column
  `width`/`minWidth`/`maxWidth`, and `<lr-browser-frame>`'s agent-supplied ping coordinates (which are
  now also clamped to the documented 0-100 range instead of serializing `NaN`).

- ed7f463: `<lr-virtual-list>` no longer rescans the whole `items` array on every scroll frame to resolve
  `active-id`. The lookup is now memoized on the `items`/`active-id`/`keyFunction` identities, so
  scrolling a large list stops calling `keyFunction` once per item per frame.

## 7.7.0

### Minor Changes

- 3ccd0ed: Improve component accessibility, localization, responsive behavior, interaction cleanup, and
  remote-content safety across the library. Add the new documented component APIs and strengthen
  event, packaging, and public-contract validation.

### Patch Changes

- 7bcac52: Raise the optional `pdfjs-dist` peer range to `^6.2.108` (from `^6.1.200`), alongside routine
  development-dependency upgrades. Only consumers of `<lr-pdf-viewer>` are affected, and only if they
  pin `pdfjs-dist` below `6.2.108`.

## 7.6.0

### Minor Changes

- b91bd2c: Add `type: 'custom'` filters to `lr-filter-bar`, letting a filter definition supply its own renderer and value adapter so any existing Lyra control (`lr-checkbox`, `lr-time-range`, an async `lr-combobox`, ...) can participate in the same controlled `value`, active-chip, reset, disabled, and validation contract as the built-in filter types.

## 7.5.0

### Minor Changes

- 33352e4: Add readonly support to `lr-textarea`, themeable `lr-thread-list` excerpt highlights, durable virtual-row stacking for open action menus, and cross-browser adjacent-row keyboard navigation.

## 7.4.0

### Minor Changes

- 3e6ab4c: Add public surface, accessibility, i18n, and documentation improvements across
  the library. No breaking change — every item below is additive, and every new custom property is an
  inline `var()` fallback carrying today's value, so rendering is unchanged when it is left unset.

  New public API:

  - `<lr-svg-viewer>` adopts the shared `DocumentAnchorTarget` contract: it gains the reactive `anchor`
    property alongside the existing `highlights`/`activeHighlightId`, a retrying
    `scrollToAnchor(target): Promise<boolean>`, the `anchor-live-region` part, and the
    `lr-anchor-result` event (`detail: { found: boolean }`). Its registry entry declares
    `capabilities: { anchors: ['region'], search: false, textSelect: false }`, so opening an SVG
    through `<lr-document-viewer>` no longer drops deep-link anchors and highlights. The same
    registry wiring and `capabilities` declaration was added for `<lr-html-viewer>`,
    `<lr-pptx-viewer>` and `<lr-archive-viewer>`.
  - `<lr-table>`'s `lr-column-resize` becomes cancelable **at the commit only**: a pointer drag still
    streams non-cancelable per-pixel feedback, then fires exactly one `{ cancelable: true }` event for
    the width committed at drag-end, and a keyboard step (Arrow/Shift+Arrow/Home/End) fires that single
    cancelable commit directly. `preventDefault()` reverts the column to its pre-gesture width.
  - `<lr-pdf-viewer>` gains the `previous-button`, `next-button`, `zoom-out-button` and
    `zoom-in-button` CSS parts, so the toolbar controls are reachable from a consumer stylesheet (a
    descendant combinator after `::part()` never matched).
  - `<lr-tool-param-form>` gains the `control` CSS part on its `string`/`number`/`integer` native
    inputs.
  - `<lr-commit-card>` gains the `file-status` CSS part: the one-letter git-status badge now carries a
    localized expansion as its accessible name, reusing `<lr-file-tree>`'s shared `gitStatus*` message
    keys.

  New component-scoped custom properties, each defaulting to the value its rule already used:
  `--lr-chart-canvas-hover-outline-width`, `--lr-chip-group-overflow-expanded-color`,
  `--lr-confirm-bar-approved-color`, `--lr-confirm-bar-denied-color`, `--lr-date-picker-nav-hover-bg`,
  `--lr-knowledge-base-admin-tab-selected-border`, `--lr-knowledge-base-admin-tab-selected-color`,
  `--lr-pagination-invalid-border`, `--lr-radio-checked-border-color`, `--lr-radio-checked-dot-color`,
  `--lr-result-card-compact-header-gap`, `--lr-result-card-compact-body-gap`,
  `--lr-schema-viewer-info-border`, `--lr-schema-viewer-info-bg`, `--lr-split-divider-hit-slop`,
  `--lr-task-list-compact-header-gap`, `--lr-widget-view-toggle-hover-bg`,
  `--lr-widget-view-toggle-hover-color`. `--lr-tree-selected-bg`/`--lr-tree-selected-color` were
  already honored by the stylesheet and are now declared, so editor tooling and the manifest see them.

  New `DEFAULT_STRINGS` message keys (additive; the English defaults match what shipped before, except
  `flagLoadError`, which previously rendered its own key name):

  - `emojiPickerGroupSmileysEmotion`, `emojiPickerGroupPeopleBody`, `emojiPickerGroupComponent`,
    `emojiPickerGroupAnimalsNature`, `emojiPickerGroupFoodDrink`, `emojiPickerGroupTravelPlaces`,
    `emojiPickerGroupActivities`, `emojiPickerGroupObjects`, `emojiPickerGroupSymbols`,
    `emojiPickerGroupFlags` — `<lr-emoji-picker>`'s auto-loaded group headings now follow
    `registerLyraLocale()`. `EmojiPickerGroup` gains an optional `labelKey`, set only by the built-in
    `emoji-picker-element-data` adapter; a consumer-supplied group is still rendered verbatim.
  - `flagLoadError` — `<lr-flag>`'s fail-closed alert had no default entry, so it rendered the literal
    string `flagLoadError` unless a locale was registered.

  Accessibility, correctness and lifecycle fixes:

  - Container components no longer leak an internal child's composed event past their own host under
    an undocumented name (`lr-activity-feed`, `lr-document-library`, `lr-filter-bar`,
    `lr-claim-evidence`, `lr-knowledge-graph-explorer`, `lr-message-feedback`).
  - A host `aria-label` now wins over the computed internal name on `<lr-document-viewer>` and
    `<lr-flow-canvas>`.
  - `<lr-split>`'s divider and `<lr-image-viewer>`'s fit/rotate/annotate controls meet the shared
    minimum hit-area floor on both axes.
  - `<lr-slider>`, `<lr-time-range>` and `<lr-swatch-picker>` forward host `focus()`/`blur()`/`click()`
    to their real internal control; `<lr-prompt-studio>` bridges its native `focus`/`blur` to the host
    and pairs its role select with a visible chevron affordance.
  - `<lr-subagent-panel>` implements the full roving-tabindex tree keyboard model
    (ArrowUp/ArrowDown/Home/End) its `role="tree"` already advertised.
  - `<lr-compare-panel>` no longer clobbers a new pair's vote when a listener re-entrantly changes
    `itemId`; `<lr-approval-queue>` marks the selected row with `aria-current`; `<lr-schema-viewer>`
    renders `info`-severity issues distinctly from `error`; `<lr-heatmap>` drops a focused cell that a
    `values` refresh made non-interactive; `<lr-test-results>` and `<lr-notebook-viewer>` key their
    expand state by stable identity instead of position; `<lr-box-plot>` preserves legend-toggled
    visibility across a redraw; `<lr-selection-toolbar>` re-arms positioning after a reconnect;
    `<lr-graph>`'s canvas renderer draws the expand badges and honors `--lr-graph-hull-opacity`;
    `<lr-diff-view>` locks code content to LTR under an RTL ancestor.
  - `<lr-chart>` redraws on a live `dir`/`lang` change, and its zoom-plugin loader reads
    `mod.default ?? mod`.
  - `<lr-voice-picker>` no longer throws when constructed in a DOM without `attachInternals()`; that
    guard now shares one implementation with the `FormAssociated` mixin and also covers an
    `attachInternals()` that throws.
  - `<lr-locale-picker>`, `<lr-flag>` and `<lr-chart>` call `super.willUpdate()`/`super.updated()`.
  - `<lr-button-group>` no longer collapses to zero width when it has no definite inline size.

  Documentation: corrected the drifted default values, anchor kinds, sanitizer, ARIA model and
  object-URL lifecycle claims across `llms/`, switched every copy-paste helper example from the root
  barrel to its side-effect-free granular subpath, and documented all of the surface above.

## 7.3.0

### Minor Changes

- 2cff771: Add the opt-in `wrap-labels` property to `<lr-stepper>` for wrapping long labels on the vertical axis.

## 7.2.0

### Minor Changes

- 9670255: Add accessible-table totals and a knowledge-graph selection event:

  - `<lr-chart>`: `stackTotals` now also feeds the generated accessible data table with a per-axis
    total column (localized `chartTotal`/`chartAxisTotal` header), and only activates when `stacked`
    is set (previously it could draw totals on unstacked bar/line charts). `valueFormatter` gains a
    `'table'` context so callers can format the new total cells and existing value cells consistently.
  - `<lr-lite-chart>`: new `tableCellFormatter` property formats the built-in multi-series accessible
    table's numeric cells (including its new opt-in `tableTotals` total column for stacked bar
    charts), via the new `LyraLiteChartTableCellFormatter`/`LyraLiteChartTableCellContext`/
    `LyraLiteChartTableCellKind` types.
  - `<lr-knowledge-graph-explorer>`: emits a new `lr-selection-change` event whenever its
    self-managed `selectedNodeId` changes from search, graph, neighbor, path, entity-card,
    invalidation, or popover-close interactions (direct host assignment stays silent).

## 7.1.0

### Minor Changes

- bf9ac61: Resolve public integration gaps:

  - add the side-effect-free `@aceshooting/lyra-ui/localization.js` runtime entry;
  - add `--lr-icon-button-border-hover`, falling back to the base border;
  - document and test the reflected `ariaControlsElements` contract for menu triggers;
  - make popover/dropdown `aria-controls` target their public host so native and Lyra triggers can
    resolve it;
  - make tooltip and checkbox descriptions resolvable across their trigger/control shadow
    boundaries; and
  - accept MapLibre GL JS v5 or v6, with version-specific worker guidance.

## 7.0.0

### Major Changes

- fb14fa1: Make the approved breaking contract corrections for the next major release.

  - `<lr-checkbox-group>` now consumes each child's native-style events and emits exactly one
    group-owned `input`, `change`, and `lr-change` sequence with `{ value: string[] }`. It also scopes
    selection to checkboxes owned by that group and silently resynchronizes its value and form data
    after programmatic child changes. Consumers that listened for leaked child events on the group
    must instead listen on the child checkbox itself.
  - `<lr-diff-view>` now defaults `maxLines` to `5000` and renders a localized fallback above that
    ceiling. Set `maxLines` to `Infinity` to preserve uncapped behavior; the line diff now uses a
    linear-space algorithm, but exceptionally large diffs can still be expensive.
  - `<lr-popover>` now returns focus to its trigger consistently after light dismiss and
    programmatic close, matching its Escape behavior. Use `hide({ focusTrigger: false })` when a
    programmatic close must leave focus elsewhere.
  - Remove the never-emitted `lr-highlight-activate` event declarations from `<lr-code-block>`,
    `<lr-code-block-core>`, and `<lr-notebook-viewer>`. Listen to the viewer's documented anchor and
    text-selection events instead.
  - `<lr-stepper>` now exposes list/progress-navigation semantics with
    `aria-current="step"` instead of incomplete tab semantics without associated tab panels.
    Selected-step state and keyboard activation remain available.
  - Toggleable `<lr-chip>` instances now put their toggle semantics on a separate native control;
    default-slot content is an inert label rather than an unrestricted interactive subtree. Move
    links and buttons outside a toggleable chip.

### Minor Changes

- fb14fa1: Expand the public component contracts needed by advanced consumers.

  - Export the document-viewer anchor, highlight, search, selection, and target types through the
    owning granular component entry, with type tests covering those imports.
  - Complete native-style `input`/`change`, focus, selection, and editing contracts across the
    affected form and conversation controls, including emoji picker and token input.
  - Add the documented viewer navigation, search, highlight, comparison, preview, and theme hooks,
    plus the corresponding component parts and custom properties.
  - Complete the typed agent-evaluation, evaluation-dataset, retrieval, and data-view surfaces that
    previously required consumers to infer internal shapes.

- 19022c7: Resolve the current feature-request backlog:

  - add configurable no-flash theme bootstraps and a Lit-free gemstone palette entry;
  - expose a pre-mount chart `seriesPalette()` helper and document its theme-token indirection;
  - let `lr-app-rail` select persisted fields, including `preferredMode`, without restoring transient
    mobile-open state;
  - add `lr-icon-button` border and hover-foreground tokens;
  - size gemstone swatches from their fill token and keep `lr-table`'s unnamed-grid warning out of
    production; and
  - document the supported SheetJS CDN install path and the unsafe npm-audit downgrade suggestion.

### Patch Changes

- fb14fa1: Correct accessibility, localization, lifecycle, security, responsive-layout, and rendering defects
  across the component families.

  - Reconcile accessible names, stateful ARIA, roving focus, focus return, wrapped-child event
    suppression, live regions, disabled behavior, and native control forwarding.
  - Make inherited locale and direction reactive through composed trees, use locale-aware text
    folding and sparse highlight offsets, and localize the remaining viewer and status messages.
  - Harden remote viewer loading, sanitization, generation ownership, size/resource guards, and
    reconnect behavior while preserving empty and error states.
  - Fix container-responsive layouts, hover/focus parity, reduced-motion behavior, theme-token
    resolution, and viewer allocation/geometry updates.
  - Avoid quadratic DOM walks when painting capped DOCX search matches.

## 6.2.0

### Minor Changes

- 7af01bf: Add `href`, `target`, and `download` to `<lr-button>`, giving it a real `<a>` anchor mode instead of
  a `<button>` that a consumer has to wrap or intercept.

  `rel` is derived from `target` rather than being independently settable, so a `target="_blank"`
  button cannot ship without `rel="noopener noreferrer"`. Hrefs are validated through the internal
  link allowlist; a `download` paired with a `mailto:` href falls back to the native `<button>`, since
  `mailto:` names no retrievable bytes.

- 7af01bf: Round out the chart components so app code stops reaching into raw `config` passthrough.

  - `data-labels` and `stack-totals` boolean attributes on `<lr-chart>` and its subclasses render
    value and stacked-total labels using themed tick colors and `--lr-font-*`, replacing hand-rolled
    `afterDatasetsDraw` painters with hardcoded colors. These require the new optional peer
    `chartjs-plugin-datalabels` (see the separate peer-dependency note); the plugin registers
    per chart instance, never globally.
  - `Series.pointRadius` accepts an array for per-point sizing, and `Series.segmentColors` maps to
    Chart.js segment coloring.
  - `seriesPalette()` is now public, so app code can read the resolved, dark-aware chart ramp instead
    of re-resolving `--lr-theme-color-chart-N` through `getComputedStyle` itself.
  - Charts re-theme automatically via a shared `ThemeWatcher` controller when the ambient theme
    changes.
  - `<lr-lite-chart>` renders a real `<table part="data-table">` screen-reader alternative when there
    is more than one series; the previous flat `<ul>` degenerated for multi-series data.

- 7af01bf: Add `chartjs-plugin-datalabels` (`^2.2.0`) as a new **optional** peer dependency, backing the new
  `data-labels`/`stack-totals` chart attributes.

  It is optional in the same sense as the other chart peers: install it only if you use those two
  attributes. Without it, charts render exactly as before and the label layer fails closed rather than
  throwing.

- 8e0540a: Add five agentic AI / RAG roadmap components: `lr-agent-eval-dashboard`, `lr-approval-queue`, `lr-embedding-explorer`, `lr-knowledge-base-admin`, and `lr-rag-answer`.
- 7af01bf: Fill several gaps in the form-control surface that were pushing logic onto consumers.

  - `<lr-input>` and `<lr-textarea>` gain `minlength`/`maxlength` constraints wired into the validity
    bridge, so length violations participate in constraint validation instead of being advisory.
    Length is counted in code points, so astral characters count as one.
  - `<lr-select>` and `<lr-combobox>` now emit value-carrying `lr-change` events, and their `input`/
    `change` events carry a typed detail — no more `as unknown as { value }` at every call site.
  - `<lr-card>` gains `target` for anchor-mode cards, with `rel="noopener noreferrer"` derived from it
    rather than settable on its own.
  - `<lr-combobox>` accepts an `AbortSignal` for `source` and a configurable `source-delay`
    (default 200ms), so a fast typist no longer races stale in-flight results.

- 7af01bf: Assorted layout and accessibility improvements.

  - `<lr-split>`'s `defaultSizes` accepts CSS length strings (`['200px', 50]`) alongside percentages,
    resolved against the measured container and renormalized — a fixed-width sidebar no longer needs a
    `firstUpdated()` measure-and-convert dance.
  - `<lr-table>` gains typed `accessible-label`/`caption` properties and warns in development when a
    grid ships with no accessible name.
  - `<lr-popover>` gains `hide({ focusTrigger })` for explicit focus return on programmatic close.
  - `<lr-segmented>` auto-reveals the selection when `value` is set programmatically, and exposes
    `scrollToValue()`.
  - `<lr-heatmap>` warns when no 2D canvas context is available instead of silently rendering nothing.
  - `<lr-file-input>` shows visible, localized rejection feedback per reason, replacing the sr-only
    count-only message.
  - `<lr-tool-result-view>` renderers can signal failure via a reflected `status`.
  - `gemstoneGlyph()` defaults its fill to `currentColor` and carries an intrinsic `1em` box.

- ad9165a: Add `mode="gemstone"` to `<lr-swatch-picker>`, shared gemstone accent data and glyph helpers, and
  apply the selected shine animation to custom icon swatches as well as plain color fills.
- 7af01bf: Add opt-in `storage-key` persistence to `<lr-table>`, `<lr-widget>`, and `<lr-app-rail>`, so layout
  state survives a reload without every application rebuilding the same `localStorage` plumbing.

  Set `storage-key` to persist `<lr-table>`'s `showAllColumns`, `<lr-widget>`'s `collapsed` state, and
  `<lr-app-rail>`'s open state and width. The attribute is unset by default — behavior without it is
  unchanged. All three share one internal helper with the `try`/`catch` handling needed for
  environments where `localStorage` throws (private mode, disabled storage, cross-origin frames).

- 7af01bf: Add theming tokens for surfaces that previously forced consumers through `::part()` overrides.

  - `<lr-icon-button>`: `--lr-icon-button-background`, `--lr-icon-button-background-hover`, and
    `--lr-icon-button-color`, so a bordered or tinted icon button no longer needs `::part(button)`.
  - `<lr-button>`: `--lr-button-shadow` (default `none`) for themed elevation.
  - `<lr-table>`: sorted-header theming tokens, plus a specificity fix so consumer `::part()` rules can
    actually win against the internal sort-state rule.
  - `<lr-select>` and `<lr-model-select>`: selected-state tokens; `<lr-combobox>` gains the matching
    `option-selected` token indirection.
  - `<lr-empty>`: `--lr-empty-compact-font-size` for compact heading typography.
  - `<lr-typing-indicator>`: `--lr-typing-duration`, so its speed is no longer keyed off the shared
    `--lr-transition-ambient`.
  - `<lr-conversation-item>`: tokenized active-row indicator part.

  Every token's `var()` fallback is the value it replaces, so unset rendering is unchanged.

- 7af01bf: Add `@aceshooting/lyra-ui/theme.js` — a zero-dependency theme runtime and no-flash bootstrap, so
  applications stop rebuilding mode/accent persistence by hand.

  The new subpath exports `setLyraTheme(theme)`, `getLyraTheme()`, the `LyraTheme`/`LyraThemeMode`
  (`'light' | 'dark' | 'auto'`) types, and `lyraThemeBootstrap` — a string of head-script source you
  inline before first paint to apply the persisted theme without a flash of the wrong mode. Theme
  changes persist to `localStorage` and announce themselves with an `lr-theme-change` event.

  The runtime deliberately does not include WCAG contrast math: deriving an accessible palette from a
  single brand color is application product logic, not a library concern.

### Patch Changes

- 7af01bf: Fix several rendering and correctness bugs.

  - `<lr-chart>`: a `valueFormatter` no longer corrupts the **category** axis. Formatted indices
    (`"0"`, `"1"`, `"2"`) were rendering in place of the category labels, because the tick callback was
    wired to every axis rather than only the value axes.
  - `<lr-diff-view>`: normalize CRLF and lone-CR line endings, so a Windows-authored file no longer
    diffs as entirely changed.
  - `<lr-app-rail>`: anchor the resizer to `:host`, pin `overflow-x`, and free fixed-position popups
    that were being clipped by the rail.
  - `<lr-button>`: collapse empty start/end adornment wrappers, which were reserving visible space for
    slots with nothing in them.
  - `<lr-swatch-picker>`: keep the selected glow on gemstone swatches.

- a656a10: Add a tokenized active-row indicator part to `lr-conversation-item` and export it through data-mode `lr-thread-list` rows.
- 203cbce: Forward host `aria-describedby` values to `lr-checkbox`'s internal checkbox role.
- 983dd04: Validate download anchors against a stricter URL allowlist than navigation anchors. A `mailto:` URL
  is a legitimate navigation destination but names no retrievable bytes, so pairing it with a
  `download` attribute produced an affordance that could never download anything.

  `safeDownloadHref()` (internal) is now `safeLinkHref()` minus `mailto:`, and the download sinks use
  it: `<lr-document-viewer>` and `<lr-document-preview>` omit their download link for a `mailto:`
  `src`, `<lr-media-card>` falls back to its inert file chip, and `<lr-button>` falls back to the
  native `<button>` when `download` is set alongside a `mailto:` `href` (a `mailto:` href _without_
  `download` still renders the anchor, unchanged).

  Behavior change: the `safeLinkHref()` re-exported from the package root is `<lr-media-card>`'s
  download-sink wrapper, so it now returns `null` for `mailto:` where it previously returned the URL.
  The general-purpose navigation validator is unchanged.

- 88dfe78: Add `LyraTreeEventMap` so `<lr-tree>`'s `lr-node-toggle`, `lr-node-select`, and `lr-reorder` events are typed on listeners (`addEventListener`), matching every other component with public events.
- a595ec7: Give `<lr-widget>`'s collapse-button `aria-label` its own dedicated locale keys,
  `widgetCollapse`/`widgetExpand`, instead of borrowing `<lr-dock-panel>`'s `dockPanelCollapse`/
  `dockPanelExpand`. Default English strings are unchanged ("Collapse panel"/"Expand panel"). If you
  had registered a locale under the old borrowed `dockPanel*` keys specifically to translate
  `<lr-widget>`'s collapse button, move that override to `widgetCollapse`/`widgetExpand` —
  `<lr-dock-panel>`'s own keys and behavior are unaffected.

## 6.1.0

### Minor Changes

- f9e1e18: Make `lr-app-rail`'s `lr-toggle` event cancelable: a listener calling `preventDefault()` keeps the
  rail open/closed as it was. The one exception is the forced close that fires when `mode` leaves
  `'mobile'` while open, which stays unconditional since it's a consistency fix-up rather than a
  user dismissal.
- f9e1e18: Add themeable `--*-gap`/`--*-radius` CSS custom properties to `lr-input`, `lr-select`, and
  `lr-combobox` (gap + radius), `lr-chip`/`lr-badge`/`lr-tag` (radius), and `lr-icon-button`
  (radius) — extending the pattern `lr-button` already shipped, so these values can be retuned
  without a `::part()` override. Every default is unchanged.
- f6d966e: Add an optional `country` field to `lr-locale-picker`'s `locales` catalog entries, letting a
  consumer override a row's derived flag (e.g. showing Lebanon's flag for an `ar` row instead of
  the library's default Saudi Arabia mapping).
- 3c9f478: Make `lr-reorder-list`'s `lr-reorder` event cancelable: a listener calling `preventDefault()`
  holds the move (reflecting `pending` on the affected `lr-reorder-item`) until the host calls the
  new `finalizePendingMove()`/`revertPendingMove()` methods — mirroring `lr-confirm-bar`'s cancelable
  approve/deny pattern, for hosts that persist the new order asynchronously.
- f9e1e18: Make `lr-token-input`'s `lr-remove` event cancelable: a listener calling `preventDefault()` (for
  example to run async removal validation, or to protect a token) keeps the token in place instead
  of it being removed unconditionally. Scoped to direct removal; multi-candidate paste/edit flows
  are unaffected.

### Patch Changes

- a4c4825: Fix `lr-agent-workspace` never emitting its documented `lr-retrieval-select` event when a row is
  selected in the built-in retrieval results, and leaking the internal `lr-retrieval-results`'s raw
  `lr-select` event through under the wrong name instead.
- f9e1e18: Fix `lr-date-input`'s `selectionDirection` getter returning `undefined` instead of `null` before
  the internal input has rendered, despite its declared `LyraDateInputSelectionDirection | null`
  return type.
- 2dabe8e: Fix `lr-icon-button`'s bare-SVG-geometry fallback rendering slotted stroke-style icon path data
  (no fill/stroke of its own) as a solid black shape instead of an outline, by giving
  `[part="fallback"]` the same `fill`/`stroke`/`stroke-width`/`stroke-linecap`/`stroke-linejoin`
  defaults `lr-icon`'s own wrapper svg already has.
- f9e1e18: Fix `lr-retrieval-results` and `lr-menu` leaking a wrapped child's own event under the wrong
  name alongside the documented, consolidated one: `lr-retrieval-results` leaked `lr-virtual-list`'s
  `lr-load-more` and `lr-chunk-inspector`'s `lr-chunk-open` (the latter also carrying an
  undocumented extra `anchor` field); `lr-menu` leaked `lr-menu-item`'s raw `lr-menu-item-select`
  alongside the documented `lr-menu-select`.
- cc90b3a: Fix two `lr-thread-list` bugs: a row click fired `lr-select` twice (the correct re-emit plus the
  original bare event leaking through unstopped), and content slotted into `slot="empty"` rendered
  unconditionally instead of only when the list has zero visible threads.

## 6.0.0

### Major Changes

- 5c93d1b: Remove `<lr-data-grid>`. It was a strict functional subset of `<lr-table>` (same `role="grid"` +
  roving-tabindex + sort/select/loading pattern, with none of `<lr-table>`'s filtering, pagination,
  inline editing, resize, grouping, expansion, heat-tint, sticky columns, or footers), implemented
  independently with no shared code. Use `<lr-table>` instead:

  - `DataGridColumn<T>`'s optional `value(row)` becomes `TableColumn<T>`'s required `cell(row)`.
  - `<lr-data-grid>`'s `emptyText` string becomes `<lr-table>`'s `emptyHeading`/`emptyDescription`
    pair (rendered via an internal `<lr-empty>`, not a plain text cell).
  - `<lr-data-grid>` always mutated `selectedKey` and emitted `lr-selection-change` on row
    click/activation; `<lr-table>` only does that when `selection-mode` is `"single"` or
    `"multiple"` (default `"none"`, presentational) — listen on `lr-row-click` (`detail: { row }`)
    instead if you don't need `<lr-table>`'s own selection bookkeeping.
  - `accessibleLabel`/`aria-label` — unchanged; `<lr-table>` reads a plain `aria-label` attribute the
    same way.

  `<lr-eval-dataset>` and `<lr-eval-result>` composed `<lr-data-grid>` internally and now compose
  `<lr-table>` instead. `<lr-eval-result>`'s public `columns` property changes type accordingly from
  `DataGridColumn<EvalRunResult>[]` to `TableColumn<EvalRunResult>[]` — update any `value(row)`
  column definitions you pass in to `cell(row)`.

### Minor Changes

- 5c93d1b: Add a `3xs` size tier to `<lr-chip>`, one step below `2xs`, for dense inline count pills.
- 8e6e045: `lr-confirm-bar`: swap the hand-rolled `deny-button`/`approve-button` native `<button>`s for
  `<lr-button>`, so `--lr-button-*` theming and a consumer's existing `lr-button` style fragments
  reach them like every other button in an app. Adds a host-writable
  `pending: 'approved' | 'denied' | null` property and makes `lr-approve`/`lr-deny` cancelable: a
  listener calling `preventDefault()` sets `pending` to the decision being made (showing `loading`
  on that button, `disabled` on the other) instead of resolving synchronously, so a host whose
  approval hits a network call can keep the UI honest about being in flight. Finalize by setting
  `.decision`, or bounce back by clearing `.pending` to `null`.

  **Breaking (CSS only):** `::part(deny-button)`/`::part(approve-button)` now select an `<lr-button>`
  host, not a native `<button>`.

  Before:
  lr-confirm-bar::part(deny-button) { padding: 4px 8px; border: ...; }
  After (use the re-exported sub-parts):
  lr-confirm-bar::part(deny-button-base) { padding: 4px 8px; border: ...; }

  Runtime API (events, `tone`, `compact`, slots, the new `pending` property) is unchanged.

- 050c43c: Fix `<lr-control-group>` collapsing to 0 inline size when placed as an ordinary flex-basis:auto
  child of a shrink-to-fit flex row (its own stated primary use case). The `@container`
  narrow-allocation breakpoint is now opt-in via a new `responsive` property instead of always-on.
- 18e7b10: Add `--lr-dashboard-grid-cell-hover-outline-color` to `<lr-dashboard-grid>`, theming the mouse-hover preview outline on `[part="cell"]` independently of the shared `--lr-color-border-strong` token. Set it to `transparent` to opt out of the hover treatment entirely.
- bd2e594: Add `--lr-flow-canvas-node-hover-outline-color` to `<lr-flow-canvas>`, theming the mouse-hover preview outline on `[part="node"]` independently of the shared `--lr-color-border-strong` token. Set it to `transparent` to opt out of the hover treatment entirely.
- 76690c7: Add `--lr-button-gap` and `--lr-button-radius` custom properties to `<lr-button>`, so the
  icon/label gap and corner radius are retunable without a `::part(base)` rule — matching the
  retunable-without-`::part()` treatment `--lr-button-padding-block/-inline` and
  `--lr-button-font-size` already have.
- f8810d7: Add `<lr-locale-picker>`: a closed-list locale switcher over the locale registry
  (`getRegisteredLyraLocales()`) or an explicit `locales` catalog, form-associated and mirroring
  `<lr-select>`'s hand-rolled listbox. Selecting a row emits a cancelable `lr-change` and, unless
  vetoed, applies the pick via `setLyraLocale()`.
- 77377ed: Add `getRegisteredLyraLocales()` and `subscribeLyraLocaleRegistry()` so a consumer can enumerate
  and live-track every locale registered via `registerLyraLocale()` (plus `'en'`) — the piece that
  unblocks a locale-picker component built on top of the existing locale runtime.
- 0771a83: Add a `renderExcerpt` hook to `<lr-thread-list>`, rendering rich per-row excerpt content into the
  row `<lr-conversation-item>`'s own `excerpt` slot — where it wins over the plain-string `excerpt`
  property — for cases like a server-highlighted search-match snippet, without giving up the built-in
  title layout and inline-rename affordance the way `renderRowContent` requires.
- 02cd69d: Add `<lr-reorder-item>`, one row of the new `<lr-reorder-list>` flat-list reorder primitive.
- 2cf4206: Add `<lr-reorder-list>`, a generic flat-list reorder primitive with move-up/move-down buttons and
  a Ctrl/Cmd+ArrowUp/ArrowDown keyboard shortcut, emitting the full new order on every move.
- c6af1b7: `lr-tool-approval-dialog`: swap the hand-rolled `deny-button`/`approve-button` native `<button>`s
  for `<lr-button>` (`variant="neutral"`/`"brand"`), so `--lr-button-*` theming and a consumer's
  existing `lr-button` style fragments reach them like every other button in an app. Adds a
  host-writable `pending: 'approve' | 'deny' | null` property and makes `lr-approve`/`lr-deny`
  cancelable: a listener calling `preventDefault()` sets `pending` to the decision being made
  (showing `loading` on that button, `disabled` on the other) instead of closing immediately, so a
  host whose approval hits a network call can keep the dialog honest about being in flight.
  Finalize by calling `close('approve'|'deny')`, or bounce back by clearing `.pending` to `null`.
  While `pending` is set, Escape and backdrop dismissal are suppressed; `pending` itself resets to
  `null` every time the dialog re-opens. The `edit-button` is unaffected.

  **Breaking (CSS only):** `::part(deny-button)`/`::part(approve-button)` now select an `<lr-button>`
  host, not a native `<button>`.

  Before:
  lr-tool-approval-dialog::part(deny-button) { padding: 4px 8px; border: ...; }
  After (use the re-exported sub-parts):
  lr-tool-approval-dialog::part(deny-button-base) { padding: 4px 8px; border: ...; }

  Runtime API (events, `editable` and its editing behavior, slots, the new `pending` property) is
  unchanged.

- 65d6a2b: Add `--lr-virtual-list-hover-outline-color` to `<lr-virtual-list>`, theming the mouse-hover preview outline on `[part="base"]` independently of the shared `--lr-color-border-strong` token. Set it to `transparent` to opt out of the hover treatment entirely.

### Patch Changes

- 3fd9bbd: Regenerate the agent-tools reference docs and custom-elements manifest for the `lr-confirm-bar`/
  `lr-tool-approval-dialog` `lr-button` swap and their new `pending` properties (see the sibling
  changesets for the runtime changes themselves).
- 9ebb38c: `lr-icon-button`: restore rendering for slotted bare SVG geometry (`<path>`, `<circle>`, etc. with
  no enclosing `<svg>`) when `icon` is unset. 5.2.0's natural-aspect-ratio change made the default
  slot a sibling of the internal glyph instead of nesting it inside an SVG, which silently stopped
  this narrow case from painting (no console error, no type error). A small whitelist of raw SVG
  geometry tag names is now cloned into a real SVG-namespaced element the same way `<lr-icon>`'s own
  custom-content slot already does — every other case (complete `<svg>`, `<img>`, custom elements) is
  untouched, so the `createElementNS`-on-custom-elements bug 5.2.0 fixed for `<lr-flag>` cannot
  regress.

## 5.2.0

### Minor Changes

- 602177a: `lr-button`: expose its per-size geometry and its outlined fill as custom properties, so a consumer
  no longer needs a `::part(base)` rule to fit a button into a dense toolbar or to tint an outline.

  - `--lr-button-padding-block`, `--lr-button-padding-inline` and `--lr-button-font-size` now carry
    each `size` tier's padding/font-size (the `:host` defaults are the `m` tier). Every tier is now
    pure custom-property re-assignment — matching `lr-input`, `lr-select`, `lr-combobox`,
    `lr-segmented` and `lr-date-input` — so overriding one knob retunes the tier instead of fighting
    the stylesheet.
  - `--lr-button-min-height` carries the active tier's `min-block-size` floor (it resolves to that
    tier's existing `--lr-button-size-*` token), and the new `--lr-button-height` pins an exact
    height — flooring _and_ capping the button, e.g. to match a fixed toolbar row. It is deliberately
    left undeclared by default so each tier's floor still applies when it is unset.
  - `--lr-button-outlined-fill` (default `transparent`) tints `appearance="outlined"`. Like
    `--lr-button-quiet-*` it is not swapped per `variant`. Note that the existing hover
    `filter: brightness()` visibly brightens a tinted fill, where a transparent one showed no change.

  `appearance="link"` continues to ignore all of these and render as zero-chrome inline text. With
  every property unset, all six tiers render byte-identical to before.

- fe06b7d: `lr-card`: `interactive` now grants real activation semantics when `href` is not also set.
  `[part='base']` becomes focusable (`tabindex="0"`), responds to Enter and Space (Space calls
  `preventDefault()` so the page does not scroll under the focused card), and emits a new
  `lr-card-activate` event (no detail) — so a clickable tile no longer needs a consumer-supplied
  wrapper or a `::part(base)` hack to be keyboard-operable.

  - The card deliberately carries **no** `role="button"`. A card is a container that routinely holds
    slotted buttons and links, and `role="button"` around focusable descendants is the axe-core
    `nested-interactive` violation this library's own a11y gate enforces (unlike `lr-chip`'s
    `toggleable` mode, which can forbid focusable children and therefore can carry the role).
  - Because of that, "did the user aim at the card or at a control inside it?" is answered at event
    time: the handler walks `composedPath()` from the original target up to `[part='base']` and bails
    out if anything on the way is itself a control (a link, button, form control, `[tabindex]`, or an
    interactive `role`). A click on a slotted `lr-button` or `<a>` therefore never activates the card.
  - With `href` set, the root is still a real `<a>`: native navigation remains the activation, no
    extra `tabindex` is added, and `lr-card-activate` is never fired.
  - Without `interactive`, the rendered output is unchanged — no `tabindex`, no listeners, no events.

- 76b4ef7: `lr-chat-composer` gains `appearance="plain"` (reflected, `'card' | 'plain'`, default `'card'`), so a
  composer docked inside a chat panel, dialog footer or toolbar that already draws its own border
  doesn't double the frame. `plain` drops `[part="base"]`'s border, background, padding and corner
  radius; the row layout, disabled treatment and the send/stop button's own chrome are unaffected.

  Focus stays visible either way. The card's only focus affordance is a border-color shift, and there
  is no border left to recolor under `plain` (the internal textarea sets `outline: none`), so `plain`
  swaps in an underline across the input row instead — drawn as an inset box-shadow from
  `--lr-focus-ring-width`/`--lr-focus-ring-color`, so it costs no layout.

  An unset composer renders byte-identically to before.

- 89dc89a: Add density and chrome-less escape hatches to six card-chrome components so an embedded card no
  longer forces its own frame on a host that already draws one:

  - `lr-agent-run`, `lr-entity-card`, `lr-source-card` each gain both a reflected `compact` boolean
    (tighter padding/gap, tunable via `--lr-<component>-compact-padding` / `-gap`) and
    `appearance="plain"` (drops border, background, padding and radius). `plain` wins over `compact`
    when both are set.
  - `lr-stack-trace` and `lr-flow-run-overlay` gain `appearance="plain"` — for nesting inside an
    `lr-result-card` / `lr-agent-run` or a host toolbar that already draws a border, without doubling
    the frame. `lr-flow-run-overlay`'s `plain` also drops its floating-surface shadow.
  - `lr-file-input` gains a reflected `compact` boolean (tighter dropzone padding, gap and label
    font, tunable via `--lr-file-input-compact-padding` / `-gap` / `-font-size`) so the dropzone fits
    a toolbar or table cell.

  All escapes default off; an unset component renders byte-identically to before. Interactive
  affordances that live on child controls (agent-run's Cancel/Retry, stack-trace's copy/frame
  buttons, source-card's title/toggle) keep their own chrome under `plain`.

- bca1353: `lr-flow-controls` gains `appearance="plain"` (reflected, `'card' | 'plain'`, default `'card'`), for
  clusters placed in a host toolbar or panel that already draws its own surface. `plain` drops
  `[part="base"]`'s border, background, padding, corner radius **and** its floating-surface
  `box-shadow` — a lift shadow with no surface under it reads as a stray smudge — matching what
  `lr-flow-run-overlay`'s `plain` already does.

  The cluster keeps its layout, its `orientation` axis, every button's shared minimum hit area
  (`--lr-icon-button-size`) and their hover/focus rings. No `compact` is offered: the padding is
  already the smallest spacing step, and the only remaining room is that hit-area floor.

  The existing `for`, `orientation` and `hideLock` properties are now documented too. An unset cluster
  renders byte-identically to before.

- ddf52ba: `lr-flow-node` now exposes its card as a CSS part and gains a density escape:

  - The bordered, filled card is reachable as `::part(card)` (it keeps its `.card` class, so nothing
    that already targeted it changes).
  - New reflected `compact` boolean tightens the card padding for dense canvases and palette previews,
    tunable via `--lr-flow-node-compact-padding` (default `var(--lr-space-xs)`) and
    `--lr-flow-node-compact-gap` (default `var(--lr-space-2xs)`). The border, background, shadow and
    the `selected`/`status="running"` treatments all stay.

  Two documentation/CSS bugs are fixed in passing: the `base` part is documented as what it actually
  is (the row wrapping the handles and the card, carrying no chrome of its own), and a duplicated
  `min-inline-size: 0` that overrode the card's own minimum width is removed — the documented
  `--lr-flow-node-min-inline-size` custom property was dead until now and once again sets the card's
  minimum inline size (default `11rem`).

  An unset node renders as before apart from that restored minimum width.

- 81f615b: `lr-checkbox` / `lr-radio`: publish the label indent, and stop hard-sizing the radio's circle.

  - **New `--lr-checkbox-label-indent` and `--lr-radio-label-indent`** carry the distance from the
    control's start edge to the start of the label — the box/circle's own floor
    (`min(--lr-icon-button-size, 1.75rem)`) plus the label gap (`--lr-space-s`), i.e. `2.25rem` at the
    default tokens. Consumers composing per-option hint text under a checkbox previously had to
    hardcode that `2.25rem` after reading it out of the shadow styles, where neither term was a public
    contract, so the hint silently de-aligned on any retheme. `[part='base']`'s `gap` is now _derived
    from_ the published property rather than repeating `--lr-space-s`, so the advertised value and the
    rendered geometry cannot drift: setting the property moves the label. Rendering is byte-identical
    when it is left unset.

    **Read this before assuming it closes the filed case.** The property is declared on the
    component's `:host`, so it is readable by the element itself and overridable from your own
    stylesheet (`lr-checkbox { --lr-checkbox-label-indent: … }` beats a `:host` rule), but custom
    properties inherit _down_, not sideways — a **sibling** `<p>` in your own tree can never read it
    off the checkbox. What actually solves that case is the `--lr-theme-icon-button-size` bridge that
    landed alongside this release: compute `calc(min(var(--lr-theme-icon-button-size, 2.5rem), 1.75rem)

    - var(--lr-theme-space-s, 0.5rem))` on your own wrapper from tokens you control, and both the
      control and your hint text stay aligned through a retheme. The new "Aligning per-option hint text"
      stories show both halves. This is not an unfixed gap; please do not re-file it as one.

  - **Bug fix — `lr-radio`'s `[part='circle']` was hard-sized**, with `inline-size`/`block-size` where
    `lr-checkbox`'s `[part='box']` correctly uses `min-inline-size`/`min-block-size`. Since
    `[part='base']` carries no box of its own, that circle _is_ the entire tap target for a label-less
    radio, and a hard size can be smaller than its own content — an enlarged indicator overflowed it
    instead of growing it. It is now a floor, matching `lr-checkbox` exactly. Default rendering is
    unchanged (28×28 at the default tokens, above the WCAG 2.2 SC 2.5.8 24×24 minimum).

    Note the residual, unchanged in this release: neither control guarantees the 24×24 minimum once
    `--lr-icon-button-size` is themed below it — `min()` still tracks the token down 1:1. Both
    controls behave identically here; a hard floor would need its own decision, since it would also
    block a deliberately dense checkbox.

  `lr-checkbox` deliberately still has no `hint`/`errorText` chrome of its own (see its class docs);
  that omission is intentional and adding it would require a `form-control` wrapper that changes the
  part structure for existing consumers.

- 6bf969f: Themeable code tab width, chat bubble geometry, and the code-block active-line outline color.

  - `--lr-code-block-tab-size` (default `2`) sets the tab width of rendered code. It is honoured by
    `lr-code-block`, `lr-code-block-core`, `lr-markdown`, and `lr-markdown-core`, and shares the
    default of the existing `--lr-code-editor-tab-size`, so the editable and read-only code surfaces
    agree. The markdown viewers declare it themselves because they are sibling elements of
    `lr-code-block`, not descendants — one declaration could not have reached them. `lr-code-block`
    reads the token rather than writing `tab-size` inline, so the override survives shiki's own
    inline `style` on the highlighted `<pre>`. Note that a markdown code block wraps
    (`white-space: pre-wrap`) while `lr-code-block` does not, so the same value can render
    differently on a wrapped line, where tab stops restart.
  - `--lr-chat-message-bubble-padding` (default `var(--lr-space-m)`) and
    `--lr-chat-message-bubble-radius` (default `var(--lr-radius)`) reshape `lr-chat-message`'s
    bubble. Use these instead of a `::part(bubble)` padding/radius override: an outer-tree `::part`
    declaration outranks every rule inside the component's shadow tree, which silently suppressed
    the per-`status` (`failed`, `streaming`) and per-role bubble treatments. The radius prop is
    bubble-only — `collapse-button` and `retry-button` keep reading the shared `--lr-radius`.
  - `--lr-code-block-active-line-outline-color` (default `var(--lr-color-brand)`) retints only the
    outline of the line marked active by `active-highlight-id`, leaving the language pill, hover,
    and focus surfaces on `--lr-color-brand`.

  All three default to exactly today's rendering, so a consumer who overrides none of them sees no
  visual change.

- fe06b7d: `lr-confirm-bar`: new reflected `compact` property that collapses the bar from a full card
  (bordered, padded, `display: block` surface) into a chrome-less inline row, for a confirmation that
  has to live inside an existing container — a table cell, a card's action row, a toolbar.

  - The **host** flips to `inline-flex` under `[compact]`, not just `[part='base']`: restyling the
    part alone still leaves a `display: block` host that breaks the row it was dropped into.
  - The narrow-allocation container query is switched off with it (`container-type: normal`). A
    compact bar is _expected_ to be narrow, so leaving the query live would fire it essentially
    always and stretch the Deny/Approve buttons to fill — the opposite of the intent.
  - Re-chrome it through `--lr-confirm-bar-compact-padding` (default `0`),
    `--lr-confirm-bar-compact-gap` (default `var(--lr-space-s)`), `--lr-confirm-bar-compact-border`
    (default `none`), `--lr-confirm-bar-compact-radius` (default `0`) and
    `--lr-confirm-bar-compact-background` (default `transparent`).
  - Everything else is unchanged: `lr-approve`/`lr-deny` shapes, `role="group"` and its heading
    label, and the contract that focus moves synchronously to `[part='status']` _before_ the
    Deny/Approve buttons unmount. Leaving `compact` unset renders exactly as before.

- 49e0738: `lr-conversation-item` gains a `compact` density flag

  A reflected boolean `compact` (default `false`, matching `lr-empty`'s convention) tightens
  `[part='base']`'s padding from `var(--lr-space-s) var(--lr-space-m)` to
  `var(--lr-space-xs) var(--lr-space-s)`, its gap from `var(--lr-space-xs)` to `var(--lr-space-2xs)`,
  and collapses `[part='content']`'s inter-line gap to `0`. Both tuned values sit behind the new
  `--lr-conversation-item-compact-padding` / `--lr-conversation-item-compact-gap` custom properties —
  declared as inline `var()` fallbacks at the point of use, never on `:host`, so a surrounding list can
  retune every row at once from an ancestor. Unset, a row renders exactly as before.

  Nothing else changes. In particular `[part='rename-button']` keeps its
  `min-inline-size`/`min-block-size: var(--lr-icon-button-size)` floor under `compact`, so a density
  flag can never silently drop a row's icon target below the shared minimum; the excerpt stays visible
  (it is already single-line ellipsised and `hidden`-bindable per row) and the excerpt/timestamp font
  sizes stay at their existing steps. `:host([compact]) [part='base']` is ordered before
  `:host([active]) [part='base']`, which is equal specificity, so an active row keeps its background
  and its promoted excerpt/timestamp contrast when both are set.

- 3737d4c: Add consumer-settable CSS custom properties for state-styled surfaces in the data and agent-tools
  families that previously took their color straight from a library-wide `--lr-color-*` token with no
  component-scoped indirection. Because CSS Shadow Parts forbids an attribute selector after `::part()`
  (`::part(row)[aria-selected]` is invalid), these states could only be restyled by hijacking the
  shared token, which repaints everything else that reads it. Each new property uses an inline
  `var()` fallback to its old token value, so an unset consumer renders byte-identically to before:

  - `lr-data-grid`: `--lr-data-grid-row-selected-bg` (selected row background).
  - `lr-env-list`: `--lr-env-list-reveal-active-bg`, `--lr-env-list-reveal-active-border` (pressed
    reveal toggle background/border).
  - `lr-flow-node`: `--lr-flow-node-selected-border` (selected card border color).
  - `lr-flow-canvas`: `--lr-flow-canvas-node-current-outline-color` (current node outline color).
  - `lr-artifact-panel`: `--lr-artifact-panel-view-active-bg`, `--lr-artifact-panel-view-active-color`
    (pressed preview/code toggle background/text).
  - `lr-test-results`: `--lr-test-results-filter-active-bg`, `--lr-test-results-filter-active-border`,
    `--lr-test-results-filter-active-color` (pressed status filter toggle).
  - `lr-span-waterfall`: `--lr-span-waterfall-row-active-bg` (active row background).
  - `lr-trace-tree`: `--lr-trace-tree-row-active-bg` (active row background).
  - `lr-agent-trace`: `--lr-agent-trace-handoff-active-bg` (active handoff quick-jump entry background).
  - `lr-policy-summary`: `--lr-policy-summary-count-allow-color`,
    `--lr-policy-summary-count-deny-color`, `--lr-policy-summary-count-needs-review-color` (per-state
    count text colors).

- 8e4e5cc: `<lr-filter-bar>` gains a `'text'` filter type, composing `<lr-input>` for an open-ended query, plus
  an optional per-filter `debounce` (ms). A dashboard whose toolbar is a search box next to a few
  dropdowns can now be a single filter bar — the search box participates in the same `value` object,
  the same removable active-filter chips (shown verbatim, so a query containing a slash is no longer
  mangled), the same reset button and `loading` state — and can delete its own hand-rolled debounce
  timer. A pending debounce is flushed by the field's own change/blur and cancelled by `reset()`, a
  chip removal, and disconnection, and the text field stays uncontrolled-with-sync so a re-render
  mid-typing never disturbs the caret.
- f8bc916: Form controls: exact-height escape hatches and `start`/`end` adornment slots.

  - `--lr-combobox-trigger-height` and `--lr-input-control-height` are new custom properties that pin
    an exact control height — flooring _and_ capping the row — so `lr-select`, `lr-combobox` and
    `lr-input` can be pixel-matched in one toolbar without a `::part()` rule. Both are deliberately
    left undeclared by default, so each tier's existing `*-min-height` floor still applies when they
    are unset. Because the component never declares them, they can also be set from an ancestor or an
    outer-tree rule, not only inline on the element. On `lr-combobox` the hatch is a single-row
    affordance: in `multiple` mode a tag row long enough to wrap overflows the pinned box visibly
    (nothing is clipped), so leave it unset there.
  - **Behaviour change:** `lr-select` declared `--lr-select-trigger-height: auto` on `:host`, which
    made the `var()` fallback to `--lr-select-trigger-min-height` unreachable and left that property
    dead at the default `m` tier (four extra specificity rules patched the floor back for
    `xs`/`s`/`l`/`xl` only). The sentinel is now genuinely undeclared and the patch rules are gone, so
    `--lr-select-trigger-min-height` is live at every tier. The visible consequence is that a
    default-size `lr-select` trigger now honours the `2.5rem` floor it already declared — byte
    identical to `lr-input`'s and `lr-combobox`'s own `m` floor, so the three controls line up.
    `getComputedStyle(el).getPropertyValue('--lr-select-trigger-height')` now returns `''` rather than
    `'auto'`; assert the rendered `min-block-size`/`block-size` instead.
  - `lr-combobox` and `lr-date-input` gain `start`/`end` adornment slots with matching `start`/`end`
    CSS parts, mirroring `lr-input`'s existing implementation: the wrappers are `hidden` while nothing
    is slotted, and they inherit the control's own padding so no consumer spacing is needed. `end`
    renders before the dropdown chevron (`lr-combobox`) and before the calendar toggle
    (`lr-date-input`), so consumer content never sits outboard of the built-in trigger. Slotted
    adornments are never collected as `lr-combobox` options.
  - `lr-select` is deliberately excluded from `start`/`end`: its `[part='trigger']` is a native
    `<button>`, whose content model forbids interactive descendants, and its `justify-content:
space-between` would push the label to the middle. `lr-date-input` is deliberately excluded from
    the exact-height hatch: its row has no `min-block-size`, and its height is pinned transitively by
    `--lr-icon-button-size` on the calendar button — capping it would crush the 24x24 target.

- 4a43cc0: `<lr-heatmap>`: `CalendarCellPos` now carries the resolved ISO `yyyy-mm-dd` `date` alongside
  `week`/`weekday`. Every calendar-mode position handed to `cellText`, `cellColor` and
  `cellInteractive` is populated — **including grid positions with no matching entry in `days`**
  (a gap in a sparse calendar still sits on a real calendar day) — so a callback can key off the date
  directly instead of re-deriving the grid's `firstWeekStart + week * 7 + weekday` anchor arithmetic,
  which was the only way to answer "is this cell in the future?" before.

  The date comes from a per-grid cache built once whenever the calendar grid is rebuilt, so it costs
  an array read rather than a `Date` allocation per cell per repaint, and it is deliberately excluded
  from the internal hover/focus position-equality check so repaint diffs are unchanged. Matrix mode's
  `MatrixCellPos` is untouched, and `lr-cell-click`'s detail shape is unchanged.

  `date` is a **required** field of `CalendarCellPos`. No API on this component accepts a
  `CalendarCellPos` as input — it is purely a callback parameter type — so this is additive for every
  supported use; the only way to notice it is hand-constructing a `CalendarCellPos` literal in
  TypeScript, which now needs a `date`.

- 4a43cc0: `<lr-heatmap>`: `HeatmapLegendStop.color` is now optional, so a `legendStops` entry can be a
  **caption-only** stop. A stop with no `color` (or an empty-string `color`) renders its
  `[part="legend-stop-label"]` with **no `[part="legend-swatch"]` element in the DOM at all**, rather
  than an empty 0.6rem swatch box — the shape a GitHub-style "Less ▢▢▢▢ More" key needs for the bare
  captions bracketing its colored ramp. Colored stops are unchanged, and an all-colored `legendStops`
  array renders exactly as before.

  The trailing `valueLabel` caption that closes the legend row also gained
  `part="legend-value-label"` (it was the one unaddressable node in `[part='legend']`), in both the
  gradient and the `legendStops` branch. Nothing else in the legend markup changed.

- 4a43cc0: `<lr-heatmap>` gained `maxCellSize` (`max-cell-size`) and `minCellSize` (`min-cell-size`), bounding
  the cell size `fit-to-width` derives from the host's measured width in **both** calendar and matrix
  mode. Without a ceiling, a 5-week calendar or a 3-column matrix in a wide pane inflates into a few
  giant blocks; without a raisable floor, a year-long calendar in a narrow pane collapses onto the
  built-in 4px minimum.

  Both are ignored while `fit-to-width` is unset — an explicit `cell-size` is an exact request and is
  never clamped — and both default to unset, so an untouched consumer's geometry is byte-identical.
  `min-cell-size` can only raise the built-in 4px floor, never lower it; when both are set and
  `max-cell-size < min-cell-size` the ceiling wins. A non-finite or empty attribute means unset rather
  than `0`.

  Note that the canvas is sized from the _clamped_ cell size, so a capped grid leaves the host's
  remaining width unfilled instead of stretching to it — align it with normal CSS on the host.

- 068cb85: `lr-icon-button` hosts natural-aspect-ratio content

  The default slot is now rendered as a **sibling** of the built-in glyph instead of being piped
  through `<lr-icon>`, and `<lr-icon>` is mounted only when `icon` is set. The button box is also
  floored with `min-inline-size`/`min-block-size: var(--lr-icon-button-size)` instead of being pinned
  to it, matching that token's documented contract (a minimum tappable box, not a fixed size).

  Slotted content previously went through `lr-icon`'s node-cloning path, which rebuilds every
  assigned node with `document.createElementNS('http://www.w3.org/2000/svg', localName)` — a slotted
  custom element such as `<lr-flag>` became an SVG-namespaced element that never upgraded and never
  painted. It now renders normally, at its own aspect ratio.

  **Migration.** Slotted **bare SVG geometry** (`<path>`, `<circle>`, …) with no `icon` attribute
  relied on the removed `<lr-icon>` wrapper to supply an SVG parent, and must now be wrapped
  explicitly:

  ```html
  <!-- before -->
  <lr-icon-button aria-label="Star"><path d="…"></path></lr-icon-button>
  <!-- after -->
  <lr-icon-button aria-label="Star"
    ><lr-icon path="…"></lr-icon
  ></lr-icon-button>
  ```

  A complete element — an `<svg>`, an `<img>`, an `<lr-flag>` — keeps working, renders more reliably,
  and is no longer constrained to a 1:1 box: content larger than `--lr-icon-button-size` now grows the
  button and keeps its own aspect ratio, while a small glyph still pads out to the full tappable
  target on both axes.

- 9ed6aa8: Add component-scoped state-styling cssprops to eight layout/forms components, so a selected/active/current state can be restyled from outside without hijacking a library-wide `--lr-color-*` token (which repaints everything else reading it). `::part(x)[state]` is invalid CSS — an attribute selector cannot follow `::part()` — so hijacking the shared token used to be the only lever. Each new prop is an inline `var()` fallback (never declared on `:host`, which would re-stamp per instance and shadow any ancestor value), and every default is the exact token the rule used before, so an unset consumer renders byte-identically.

  - `lr-app-rail-item`: `--lr-app-rail-item-current-bg`, `--lr-app-rail-item-current-color` for the `active`/`aria-current="page"` item.
  - `lr-stepper`: `--lr-stepper-current-color`, `--lr-stepper-error-color`, `--lr-stepper-current-index-bg`, `--lr-stepper-current-index-color`.
  - `lr-widget`: `--lr-widget-view-toggle-active-bg`, `--lr-widget-view-toggle-active-color` for the pressed view toggle.
  - `lr-carousel`: `--lr-carousel-indicator-current-bg`, `--lr-carousel-indicator-current-border-color` for the current slide's indicator dot.
  - `lr-breadcrumb-item`: `--lr-breadcrumb-current-color` for the current-page item.
  - `lr-command-palette`: `--lr-command-palette-active-bg` for the active command row.
  - `lr-time-range`: `--lr-time-range-preset-active-bg`, `--lr-time-range-preset-active-border-color`, `--lr-time-range-preset-active-color` for the active preset button.
  - `lr-emoji-picker`: `--lr-emoji-picker-active-bg` for the keyboard-active and hovered emoji (both share one rule, so one hook retints both).

- cea6d8e: New `localeNativeName(tag)` helper next to `languageToCountry()` / `LANGUAGE_TO_COUNTRY`: it returns
  a locale's endonym — its name written in that locale itself (`'fr'` → `français`, `'pt-BR'` →
  `português (Brasil)`) — which is what a language switcher should list. It reads through the shared
  memoized `Intl.DisplayNames` cache, so no name table ships and repeat lookups are free, and it
  degrades to the tag itself for an unknown or structurally invalid tag instead of throwing. Paired
  with `languageToCountry()` and `lr-popover`, it composes the locale-picker recipe shown in the new
  Flag story.
- 184bfff: `lr-menu`: Escape from `header`/`footer` content closes the menu and refocuses the trigger
  unconditionally, with no opt-in required.

  That matches `<lr-popover>`, which already dismisses on Escape from arbitrary popup content, and it
  is the only sensible contract for a region the component now positively invites you to fill: a
  filter field you can Tab into but not Escape out of is a trap.

  - `closeOnEscapeAnywhere` is **unchanged** — not deprecated, still `false` by default, and still
    governing exactly one thing: Escape from non-item content slotted into the **default** slot.
    Escape bubbling up from inside `[part='list']` is left entirely to the list's own handler.
  - Arrow/ArrowUp/Home/End/Enter/Space from header/footer content keep their full native behavior;
    the item-target gate that guarantees that is untouched, and nothing in the new region handler
    calls `preventDefault()` for those keys.

- 184bfff: `lr-menu`: new `header` and `footer` slots for composed, non-menu-item content, rendered inside
  `[part='popup']` but **outside** the `role="menu"` list — with matching `header`/`footer` CSS parts.

  A filter field, a section title, an "Apply"/"Done" button and friends have always been a real use
  case for this component (`closeOnEscapeAnywhere` exists for exactly that), but the only place to put
  them was the default slot — i.e. inside `role="menu"`, where ARIA permits only
  `menuitem`/`menuitemradio`/`menuitemcheckbox`/`group`/`separator` children. Anything else there is an
  `aria-required-children` violation. The new slots give that content a valid home.

  - Nothing about the default slot changes: item discovery, roving tabindex, type-ahead,
    `closeOnEscapeAnywhere` and its `false` default all behave exactly as before, and `items` still
    only ever contains `<lr-menu-item>`s no matter what the new slots hold.
  - With neither slot filled the rendered result is unchanged — both wrappers collapse to no box at
    all, `[part='list']` keeps its exact position and size inside the popup, and the host gains no
    attribute of any kind.
  - Emptiness is tracked from each slot's own `slotchange` (reflected as `data-has-header` /
    `data-has-footer` / `data-list-empty` on the host) rather than with `:empty`, which can never match
    a part that contains a slot: Chromium counts the whitespace-only text nodes Lit leaves there.
  - Non-item content in the **default** slot keeps working exactly as it did, with no runtime warning,
    but the new slots are now the supported place for it.

- 184bfff: `lr-menu`: Tab now moves focus into the `header`/`footer` regions instead of closing the menu, and
  tabbing out of the popup's last focusable finally closes it.

  Two halves of the same defect. `onListKeyDown` gated every key except Escape behind "is the event
  target a real `<lr-menu-item>`?", so (a) Tab from an item always closed the menu — you could never
  Tab _into_ composed content, in either direction, since Shift+Tab is `key === 'Tab'` too — and
  (b) Tab from composed content did nothing at all: focus walked out of the popup while the menu
  stayed open, an untested dismissal hole.

  Tab handling therefore moves from `[part='list']` to `[part='popup']`, which also sees keydowns from
  the new regions, and the menu now closes only when Tab would leave the popup entirely:

  - Tab from an item with a focusable `footer` (or Shift+Tab with a focusable `header`) keeps the menu
    open and lets the browser's own Tab advance carry focus into the region.
  - Tab out of the last focusable in the popup — in either direction, from an item or from composed
    content — closes the menu.
  - **With no header/footer content, Tab closes exactly as before**, and non-item content in the
    default slot stays deliberately Tab-unreachable from an item.
  - `preventDefault()` is still never called for Tab, in any branch: native focus navigation proceeds
    untouched, only the now-stale open state is cleared.

- fe06b7d: `lr-menu`: `show(focus?)` and `hide(options?)` are now public.

  - `hide({ focusTrigger: true })` closes the menu **and** returns DOM focus to the `trigger`-slotted
    element — the case the trigger alone cannot express, e.g. a slotted "Apply"/"Done" button inside
    the menu, or a consumer-owned keyboard shortcut. `hide()` on its own closes without moving focus,
    for dismissals where the interaction has already put focus somewhere the user chose.
  - `show()` is promoted alongside it (rather than shipping an asymmetric API) and still accepts the
    `'first' | 'last'` initial focus target.
  - The roving-tabindex reset moved from `hide()` into `updated()`, so a bare `el.open = false` from
    outside now resets `activeIndex` too. Previously that path left a stale `tabindex="0"` tab stop on
    whichever item was last active, so Tab could land inside a closed menu. `hide()` stays thin and
    `updated()` remains the single owner of positioning, listeners and the `lr-show`/`lr-hide` events;
    focus restoration deliberately stays in `hide()` so `disconnectedCallback()`'s own `open = false`
    teardown reset can never steal focus.

- 09bdfde: `lr-activity-feed`: make the virtualized entry rows actually styleable, by this component and by a
  consumer.

  At/above `virtualizeThreshold` the entries are produced by this component's `renderItem` but
  committed into the embedded `<lr-virtual-list>`'s own shadow root, one boundary deeper than a
  `[part='entry']` selector can reach — so every entry, icon, text and timestamp rule was silently
  inert and a long feed rendered as unstyled rows. Each rule now pairs its plain selector (still
  correct below the threshold, where the same template renders into this component's own shadow root)
  with an `lr-virtual-list::part(…)` twin, and an `exportparts` forwarding declaration makes the same
  parts reachable as `lr-activity-feed::part(entry)` etc. from a consuming stylesheet.

  The tone dot is promoted from an internal class to a named `tone-dot` part, since a class selector
  cannot cross a shadow boundary either. `::part()` cannot be followed by an attribute selector, so
  the tone carries a second name in the dot's part list rather than being matched through
  `[data-tone]` (`::part()` matches with `part~=` semantics, so both names select the same element).
  New parts: `tone-dot`, plus `tone-dot-neutral`/`tone-dot-brand`/`tone-dot-success`/
  `tone-dot-warning`/`tone-dot-danger`. The `data-tone` attributes are unchanged, and a consumer can
  now retint a single tone instead of overriding a library-wide color token.

- 9150bb1: `lr-archive-viewer`: make the virtualized entry rows actually styleable, by this component and by a
  consumer.

  Entry rows are produced by this component's `renderItem` but committed into the embedded
  `<lr-virtual-list>`'s own shadow root, one boundary deeper than a `[part='entry']` selector can
  reach — so all five row-level rules were silently inert and the listing rendered as unstyled stacked
  text with no row layout, no icon sizing, no truncation and no size column treatment. They now reach
  through `lr-virtual-list::part(…)`, and an `exportparts` forwarding declaration makes the same parts
  reachable as `lr-archive-viewer::part(entry)` etc. from a consuming stylesheet.

  New part `entry-name-dir`: `::part()` cannot be followed by a descendant combinator, so the
  directory-row emphasis that used to be written as a descendant selector now targets a second part
  name on the name element itself. A directory row's name is `part="entry-name entry-name-dir"`, and
  `::part()` matches with `part~=` semantics, so both names select it.

- 3e171e6: Fix `lr-av-player`'s transcript cue styling never applying, and make every cue-level part reachable
  from a consumer stylesheet.

  Cues are composed through `lr-virtual-list`, whose `renderItem` result is committed inside that
  element's **own** shadow root — one boundary below the player's. A bare `[part='cue']` selector in
  the player's stylesheet cannot cross that boundary, so every cue rule was silently inert and each
  transcript row fell back to the raw browser button appearance: a grey background, a visible border,
  `1px 6px` padding and centered text, with no timestamp or speaker treatment and no visual state for
  the playing cue or the search matches. Every one of those rules now goes through
  `lr-virtual-list::part(…)`.

  `::part()` cannot be followed by an attribute selector, so the three cue states get their own part
  names, added alongside `cue` as a part list (`::part()` carries `part~=` semantics, so both names
  match the same element):

  - **New:** `cue-current` — the row the playhead is inside.
  - **New:** `cue-match` — a row matching the current search query.
  - **New:** `cue-active-match` — the row holding the current search match.

  The `aria-current`, `data-match` and `data-active-match` attributes are unchanged and still describe
  each row's state.

  This also makes two documented custom properties live for the first time:
  `--lr-av-player-cue-current-bg` now retints the playing cue, and
  `--lr-av-player-cue-active-match-color` now recolors the active search match's outline. Both
  previously resolved against a rule that never matched anything.

  The player forwards `cue`, `cue-current`, `cue-match`, `cue-active-match`, `cue-time`, `cue-speaker`
  and `cue-text` through `exportparts`, so `lr-av-player::part(cue)` and friends work from a consumer
  stylesheet for the first time.

- c0f00ac: `lr-csv-viewer` and `lr-spreadsheet-viewer`: make the documented `cell-highlight` part actually
  visible, and reachable from a consumer stylesheet.

  Both viewers already emitted `part="cell cell-highlight"` for a cell covered by a `highlights`
  entry, but neither had a single CSS rule for it anywhere — a highlighted cell rendered
  indistinguishably from a plain one. Highlighted cells render inside the internal
  `<lr-virtual-list>`'s own shadow root (they are `renderItem`'s output), so the styling is applied
  through `lr-virtual-list::part(cell-highlight)`, using the same outline tokens `lr-dataset-viewer`
  gives its own `cell-highlight` so a highlight reads identically across the table viewers.

  - New `--lr-csv-viewer-highlight-color` / `--lr-spreadsheet-viewer-highlight-color` custom
    properties (default `var(--lr-color-brand)`) set the outline color; the active highlight sets it
    inline to `var(--lr-color-warning, var(--lr-color-brand))`, so the active match is now
    distinguishable from the other highlighted cells.
  - A paired `:focus-visible` rule restores the shared focus ring, which the unconditional highlight
    outline would otherwise swallow on this focusable cell.
  - Both viewers now forward `exportparts` for `data-row`, `cell` and `cell-highlight` from the
    internal `<lr-virtual-list>`, so `lr-csv-viewer::part(cell)` and friends reach the real rendered
    rows instead of matching nothing.

- 99d5500: Fix `lr-chunk-inspector`'s entire chunk-row styling never applying above `virtualize-at`, and make
  every row-level part reachable from a consumer stylesheet.

  Past the threshold the row template becomes `lr-virtual-list`'s `renderItem`, whose result is
  committed inside that element's **own** shadow root — one boundary below this component's. A bare
  `[part='chunk']` selector cannot cross that boundary, so a long chunk list lost its row layout and
  separators, the score line's size/color/tabular figures, the score bar and its tone-mapped fill, the
  line clamp on the collapsed text preview, and the borderless brand styling on the open and
  show-more buttons, which fell back to the raw browser button appearance. Both documented custom
  properties (`--lr-chunk-inspector-current-bg`, `--lr-chunk-inspector-current-color`) were dead
  there too. Every rule now pairs its original selector with an `lr-virtual-list::part(…)` arm, so
  both rendering paths present identically — below the threshold the rows are still rendered into this
  component's own shadow root, where the bare selector is the one that matches.

  `::part()` cannot be followed by an attribute selector, and it cannot be followed into the matched
  element's subtree either, so row state is now carried by an additional part name (added alongside
  the base name as a part list — `::part()` carries `part~=` semantics, so both names match the same
  element):

  - **New:** `chunk-current` — the row matching `activeId`.
  - **New:** `score-current` — that row's score line, previously reached through a descendant
    selector no `::part()` can express.
  - **New:** `score-fill-success`, `score-fill-warning`, `score-fill-danger` — the score bar fill in
    each scoring tier.
  - **New:** `text-clamped` — the text preview while still collapsed.

  The `aria-current`, `data-tone` and `data-clamped` attributes are unchanged and still describe each
  element's state.

  While virtualized, the chunk row no longer carries its own `role="listitem"`: `lr-virtual-list`
  already wraps every row it renders in one, and the nested duplicate left the inner list item with a
  list-item rather than list parent — an invalid ARIA containment that axe flags.

  The internal `lr-virtual-list` now forwards every row part through `exportparts`, so
  `lr-chunk-inspector::part(chunk)` and friends work from a consumer stylesheet in both paths.

- 5bdb6d7: Fix `lr-notebook-viewer`'s cell and output styling never applying, and make every cell-level part
  reachable from a consumer stylesheet.

  Cells are composed through `lr-virtual-list`, whose `renderItem` result is committed inside that
  element's **own** shadow root — one boundary below the viewer's. A bare `[part='cell']` selector in
  the viewer's stylesheet cannot cross that boundary, so the rules for `cell`, `cell-gutter`,
  `outputs`, `output` and `output-toggle` were all silently inert: cells rendered without their
  two-column grid, padding and separator, the execution-count gutter without its monospace/quiet
  treatment, stderr and error outputs untinted, and the show-all-output control as a raw browser
  button. Every one of those rules now goes through `lr-virtual-list::part(…)`, including the
  narrow-allocation `@container` block — container queries resolve through the flat tree, so they
  still evaluate against the viewer's own `:host` container across the shadow boundary.

  `::part()` cannot be followed by an attribute selector or a descendant combinator, so three states
  and one descendant get their own part names, added alongside the existing ones as a part list
  (`::part()` carries `part~=` semantics, so both names match the same element):

  - **New:** `cell-active` — the cell an anchor currently targets. This is what
    `--lr-notebook-viewer-active-bg` retints; that custom property had no effect until now.
  - **New:** `output-error` — a stderr stream or an error output, carrying the danger tint.
  - **New:** `error-output-label` — the label introducing an error output's traceback.

  The `data-active`, `data-stream` and `data-output-type` attributes are unchanged and still describe
  each element for scripting.

  The viewer forwards `cell`, `cell-active`, `cell-gutter`, `cell-source`, `outputs`, `output`,
  `output-error`, `error-output-label` and `output-toggle` through `exportparts`, so
  `lr-notebook-viewer::part(cell)` and friends work from a consumer stylesheet for the first time.

- 2e3be2e: `lr-page-rail`: make the virtualized page rows actually styleable, by this component and by a
  consumer.

  Page rows are produced by this component's `renderItem` but committed into the embedded
  `<lr-virtual-list>`'s own shadow root, one boundary deeper than a `[part='page']` selector can
  reach — so all 13 row-level rules were silently inert and every page button rendered as a raw
  browser `<button>` (UA background, UA border, UA padding) instead of the intended rail row. They now
  reach through `lr-virtual-list::part(…)`, and an `exportparts` forwarding declaration makes the same
  parts reachable as `lr-page-rail::part(page)` etc. from a consuming stylesheet.

  `--lr-page-rail-current-bg` becomes live with this fix: it previously documented a background that
  nothing applied. It now tints the current page row, and keeps it tinted while the row is hovered so
  the current page stays identifiable under the pointer.

  `::part()` cannot be followed by an attribute selector, so state variants carry a second part name
  in the element's part list instead (`::part()` matches with `part~=` semantics, so both names select
  the same element). New parts: `page-current` on the current page button (alongside `page`), and
  `heat-dot-accent`/`heat-dot-success`/`heat-dot-warning`/`heat-dot-danger`/`heat-dot-neutral`/
  `heat-dot-overflow` on the heat markers (alongside `heat-dot`). The `data-tone`/`data-overflow`
  attributes are unchanged.

- 3217988: Fix `lr-pdf-viewer`'s page styling never applying, and make every page-level part reachable from a
  consumer stylesheet.

  Pages are composed through `lr-virtual-list`, whose `renderItem` result is committed inside that
  element's **own** shadow root — one boundary below the viewer's. A bare `[part='page']` selector in
  the viewer's stylesheet cannot cross that boundary, so the rules for `page`, `text-layer`, the page
  canvas, the generated text runs, the selection tint, and both search-match states were all silently
  inert: pages rendered without their centering/padding wrapper, the canvas without its border,
  the text layer unpositioned, and search matches unhighlighted. Every one of those rules now goes
  through `lr-virtual-list::part(…)`, including the RTL text-layer mirror.

  Because `::part()` cannot be followed by a descendant combinator, two elements that were previously
  addressed as descendants get their own names:

  - **New:** `page-canvas` — the canvas a page's content is painted onto.
  - **New:** `text-span` — one generated text run inside a page's text layer. The selection tint hangs
    off this part (`::part(text-span)::selection`), since a highlight pseudo is matched against the
    element the selected text originates in.

  `search-match` / `search-match-active` are now matched directly by name (`::part()` already carries
  `part~=` semantics), and the viewer forwards `page`, `page-canvas`, `text-layer`, `text-span`,
  `search-match` and `search-match-active` through `exportparts`, so `lr-pdf-viewer::part(page)` and
  friends work from a consumer stylesheet for the first time.

- 6f3db46: Fix `lr-retrieval-results`' row, selection and metadata styling never applying while virtualized,
  and make every row-level part reachable from a consumer stylesheet.

  Rows are composed through `lr-virtual-list`, whose `renderItem` result is committed inside that
  element's **own** shadow root — one boundary below this component's. A bare `[part='row-body']`
  selector in this component's stylesheet cannot cross that boundary, so the checkbox offset, the
  row-body layout, the selected-row indicator and the whole metadata list were silently inert
  whenever the list virtualized. `grouping="source"` always virtualizes, so every grouped consumer
  saw an unstyled result set, and the documented `--lr-retrieval-results-selected-border` custom
  property had nothing to recolor there. Each of those rules now pairs its original selector with an
  `lr-virtual-list::part(…)` arm, so both rendering paths present identically — the flat path below
  `virtualize-at` still renders these parts into this component's own shadow root, where the bare
  selector is the one that matches.

  `::part()` cannot be followed by an attribute selector, nor by a descendant combinator, so two
  kinds of rule needed new part names:

  - **New:** `row-body-selected` — added alongside `row-body` as a part list (`::part()` carries
    `part~=` semantics, so both names match the same element) on the selected row. The `data-selected`
    attribute is unchanged and still describes the row's state.
  - **New:** `metadata-term` and `metadata-value` — the `<dt>`/`<dd>` inside a `metadata-entry`,
    previously styled through a descendant selector that `::part()` cannot express. The trailing colon
    after a metadata key is now `::part(metadata-term)::after`.

  The group header in grouped mode also gains a separator matching the one this component's rows use;
  `lr-virtual-list` supplies the rest of its appearance.

  `exportparts` now forwards `select`, `row-body`, `row-body-selected`, `metadata`, `metadata-entry`,
  `metadata-term` and `metadata-value` alongside the existing `row`/`group-header`, and forwards each
  per-row `lr-chunk-inspector`'s own parts onward under a `chunk-` prefix (`chunk`, `chunk-current`,
  `chunk-score`, `chunk-score-current`, `chunk-score-bar`, `chunk-score-fill`,
  `chunk-score-fill-success`/`-warning`/`-danger`, `chunk-open-button`, `chunk-title`, `chunk-text`,
  `chunk-text-clamped`, `chunk-toggle`) — those live two shadow hops deep and were unreachable from
  outside the component entirely.

- 583f359: `lr-phone-input`: rebuild the country selector's closed state and add an opt-in `flags` API.

  The old closed control was the bare native `<select>` showing each option's full
  `"Country name (+code)"` text: long localized names clipped under the UA chevron (the trigger was
  capped at 45% of the field), the calling code appeared twice (inside the option text and again in
  `calling-code`), and the popup fell back to UA colors (a white panel in dark themes). The native
  `<select>` is kept — its popup, localized full country names, keyboard type-ahead, and native
  mobile pickers are irreplaceable and fully accessible — but it is now stretched invisibly over a
  compact decorative trigger:

  - New closed state: selected alpha-2 code (localized "Select" placeholder when no countries exist)
    plus the shared design-system chevron, with a pointer cursor, a hover tint, and an inner
    focus-visible ring so keyboard focus on the selector is distinguishable from focus on the
    telephone input. No more clipping and no duplicated calling code.
  - Popup options now pin `--lr-color-surface`/`--lr-color-text` so the open list follows the theme
    in dark mode.
  - New `flags` boolean attribute renders the selected country's flag in the trigger as
    `<lr-flag variant="compact" aria-label="">` (decorative — the select already announces the
    country). The `<lr-flag>` definition is registered lazily on first use, so nothing flag-related
    is bundled while `flags` stays off; flag artwork keeps the standalone `<lr-flag>` contract
    (install optional `@aceshooting/lyra-flags` + import
    `components/media/flag/flag-peer.js` once). Without it the trigger simply omits the image.
  - New CSS parts: `country` (selector region), `country-trigger`, `flag`, `country-code`
    (`data-placeholder` when empty), `expand-icon`. Existing parts are unchanged in name, but
    `country-select` is now the invisible overlay — a consumer rule that painted its text/background
    should target `country-trigger`/`country-code` instead.

- e83deb1: Selected-state styling hooks for `lr-segmented` and `lr-tabs`, an exact-height hatch for the
  `lr-segmented` track, and a marker legend row for `lr-sequence-strip`.

  - `lr-segmented` gains `--lr-segmented-selected-bg`, `--lr-segmented-selected-color`,
    `--lr-segmented-selected-font-weight`, `--lr-segmented-selected-shadow` and
    `--lr-segmented-hover-color`. Recoloring the checked pill previously required hijacking
    library-wide `--lr-color-surface`/`--lr-color-text`, which necessarily repainted hovered
    _unselected_ segments too (they read the same tokens); `::part(segment)[aria-checked='true']` is
    not valid CSS, so there was no other route. The hover color is now its own hook, so the two states
    are independent.
  - `lr-segmented` also gains `--lr-segmented-track-height`, pinning the track to an exact height at
    every `size` tier for a row that must line up with a hard-sized toolbar control. It is genuinely
    unset by default, so each tier keeps its `--lr-segmented-track-min-height` floor until you set it.
  - `lr-tabs` gains `--lr-tabs-selected-color`, `--lr-tabs-indicator-color` and
    `--lr-tabs-hover-color` for the same reason: the selected tab's text/underline and the hovered
    tab's text no longer share `--lr-color-brand`/`--lr-color-text` with the rest of the library.
  - `lr-sequence-strip` gains `markerLabel` (`marker-label`). When set alongside `show-legend` it adds
    one trailing legend row — `[part="legend-marker-swatch"]`, a neutral chip (themeable via the new
    `--lr-sequence-strip-legend-marker-bg`) carrying the cell's own bottom bar in
    `--lr-sequence-strip-marker-color` — and the marker's count joins the strip's auto-generated
    `aria-label` summary, so the visual legend keeps no entry without a spoken counterpart.

  Every new custom property is an inline `var()` fallback resolving to the token the rule already
  used, so an unset consumer renders exactly as before.

- 36dce60: Fill the sized-control cssprop gaps for `lr-date-input`, `lr-pagination`, `lr-known-date`,
  `lr-chip`, `lr-avatar`, and `lr-avatar-group`, matching the per-tier theming surface
  `lr-input`/`lr-select`/`lr-combobox` already expose.

  - **`lr-avatar` / `lr-avatar-group` (visible bug fix):** the initials fallback and the "+N"
    overflow badge were painted at a fixed `--lr-font-size-sm` at every `size`, so initials did not
    scale with the avatar circle. They now scale via new per-tier `--lr-avatar-font-size` and
    `--lr-avatar-group-badge-font-size` knobs (`sm`/`md`/`lg`). The `md` default is unchanged, so
    existing avatars render identically.
  - **`lr-date-input`:** adds a per-tier `--lr-date-input-control-min-height` floor and an exact-height
    `--lr-date-input-control-height` hatch on the input row (it previously had neither). The calendar
    toggle keeps its own 24x24 touch target even when the height hatch pins a shorter row.
  - **`lr-known-date`:** adds a per-tier `--lr-known-date-field-min-height` floor and an exact-height
    `--lr-known-date-field-height` hatch on each field input.
  - **`lr-chip`:** the interactive tap-target floor is now the per-tier `--lr-chip-min-height` (was a
    single hardcoded `1.5rem` shared by every tier), and a new `--lr-chip-height` hatch pins an exact
    height. Interactive chips keep the 24px WCAG 2.2 SC 2.5.8 minimum at every tier; a `--lr-chip-height`
    below that is for non-interactive chips only.
  - **`lr-pagination`:** the nav buttons' and page input's inner padding is now the
    `--lr-pagination-control-padding` knob (was a hardcoded `var(--lr-space-xs)`), kept uniform across
    tiers so current rendering is unchanged.

  All new knobs default to today's exact values, so unset consumers render byte-identical at every
  tier (the `lr-avatar` `sm`/`lg` font-size fix is the sole deliberate exception).

- 6ab596d: `<lr-split>`: `rail-breakpoint` and `float-breakpoint` now accept a CSS length (`'640px'`,
  `'68.75rem'`, `'3em'`) as well as the original bare pixel number, and a new
  `collapse-breakpoint-basis="viewport"` measures both against the viewport via `matchMedia` instead
  of the split's own `[part="base"]` allocation — for collapsing in step with a page-level `@media`
  layout. Both thresholds are classified together on every change, so a fast resize crossing both at
  once still lands on one correct state and fires `lr-split-collapse-change` once; under viewport
  basis the first paint already carries the right `data-collapse-state` with no `ResizeObserver`
  round-trip, and that initial state is not announced as a transition. Note `(max-width:)` is
  inclusive while container basis compares strictly `<`, so switching basis shifts each crossing
  point by 1px. An unparseable length (`'80vw'`, `'calc(…)'`, garbage) falls back to the documented
  `640`/`400` defaults rather than switching collapse off, and the "rail must sit above float"
  invariant is still enforced, in pixel space, under both bases.

  Because both properties now accept a string, they use Lit's default string converter: reading
  `el.railBreakpoint` after `rail-breakpoint="640"` returns `'640'` rather than `640` (matching how
  `orientationBreakpoint` already behaves). Authored values and crossing behavior are unchanged.

- e1d4af8: `lr-stat` gains two layout axes and stops reserving space for an absent label.

  - `appearance="card" | "plain"` (default `card`, reflected). `plain` removes the border,
    background, padding, corner radius and the `block-size: 100%` stretch, so a stat can sit inline
    in prose, a toolbar or a table cell instead of only as a card. A `plain` stat with a safe `href`
    underlines its `[part="value"]` on hover/focus, since the card's border-color-shift affordance is
    invisible with no border; the focus ring is unchanged. `plain` also wins over `compact` when both
    are set, and drops `emphasis`'s accent edge (card chrome) while keeping its brand value tint.
  - `orientation="vertical" | "horizontal"` (default `vertical`, reflected). `horizontal` lays label,
    value + unit, trend, sub and caption out on a single wrapping baseline row; `[part="spark"]` and
    `[part="rows"]` stay stacked on their own full-width line beneath it.
  - `[part="label"]` is now `hidden` whenever `label` is empty, so a label-less stat no longer leaves
    a blank gap above its value. A non-empty label is never hidden and its `aria-labelledby` pairing
    with `[part="value"]` is unchanged.

- 3312708: Add component-scoped CSS custom properties for state styling across thirteen conversation, retrieval, viewer and media components. Each of these components previously painted a selected/active/current state straight from a library-wide `--lr-color-*` token, which left the state unrestylable from outside: `::part(x)[data-active]` is invalid CSS, so the only lever was hijacking the shared token — repainting every other surface on the page that read it.

  Every new property uses the inline `var()` fallback form and is deliberately **not** declared on `:host`, so a value set on the element or any ancestor is honoured rather than shadowed. With none of them set, rendering is byte-identical to before.

  - `lr-conversation-item` — `--lr-conversation-item-active-bg`, `--lr-conversation-item-active-color`
  - `lr-push-to-talk` — `--lr-push-to-talk-recording-color`
  - `lr-chunk-inspector` — `--lr-chunk-inspector-current-bg`, `--lr-chunk-inspector-current-color`
  - `lr-retrieval-results` — `--lr-retrieval-results-selected-border`
  - `lr-retrieval-trace` — `--lr-retrieval-trace-active-border`
  - `lr-source-picker` — `--lr-source-picker-checked-bg`, `--lr-source-picker-checked-border`, `--lr-source-picker-mixed-bg`
  - `lr-page-rail` — `--lr-page-rail-current-bg`
  - `lr-notebook-viewer` — `--lr-notebook-viewer-active-bg`
  - `lr-svg-viewer` — `--lr-svg-viewer-active-border`
  - `lr-document-preview` — `--lr-document-preview-active-border`
  - `lr-xml-viewer` — `--lr-xml-viewer-active-match-color`
  - `lr-av-player` — `--lr-av-player-marker-active-color`, `--lr-av-player-cue-current-bg`, `--lr-av-player-cue-active-match-color`
  - `lr-image-viewer` — `--lr-image-viewer-annotate-active-bg`, `--lr-image-viewer-annotate-active-border`, `--lr-image-viewer-highlight-active-color`

  `--lr-conversation-item-active-*` and `--lr-chunk-inspector-current-*` are documented as contrast-sensitive pairs: each background is half of a WCAG-AA dependency with the text color rendered on it.

  Also fixes a WCAG-AA contrast failure in `lr-chunk-inspector`: the current (`active-id`) chunk's score line rendered in `--lr-color-text-quiet`, which reaches only ~4.24:1 against the `--lr-color-brand-quiet` current-row background — under the 4.5:1 floor for normal-size text. It now uses full-strength text while current, matching the identical fix already carried by `lr-attachment-chip`, `lr-chat-message` and `lr-conversation-item`. Non-current rows keep the quiet treatment.

- 5e9a18e: `lr-table`: keep focus inside a persistent (`editable: 'always'`) cell editor when the rows are
  re-sorted underneath it. Row rendering is keyed by row key, so a re-sort _moves_ the editor's
  `<input>` node — the typed value rides along, but a DOM move drops focus on its own — so the table
  now records the focused editor's cell and restores focus to it after the move. A row that has left
  the rendered set entirely (paginated away, filtered out) only clears the record: focus is not yanked
  to whichever unrelated row now occupies that position.
- 5e9a18e: `lr-table`: give a persistent (`editable: 'always'`) cell editor its own Enter/Escape semantics.
  Enter commits and keeps focus in the field rather than closing an editor that has no closed state,
  and Escape — which has nothing to cancel back to — is no longer cancelled, so an ancestor
  dialog/popover still acts on it. A double-click editor's Enter-commits-and-closes and
  Escape-cancels behavior is unchanged. Adds the accompanying `AlwaysOnEditors` story.
- 5e9a18e: `lr-table`: widen `TableColumn.editable` to `boolean | 'always'`. `true` keeps today's
  double-click-to-open editor unchanged; the new `'always'` renders a persistent editor in every body
  cell of that column from first paint, for settings/rate-style grids where double-clicking each cell
  to change a value is the wrong interaction. Persistent editors are plain tab stops (no `tabindex` of
  their own), exactly like the existing row-expand toggle, so the roving header/row tabindex model is
  untouched; each one keeps its individually interpolated `tableEditCell` accessible name, and
  double-clicking an `'always'` cell no longer opens a second, competing editor inside it.
- 5e9a18e: `lr-table`: a persistent (`editable: 'always'`) cell editor binds its `value` as a content attribute
  rather than as the `.value` property, so native dirty-value-flag semantics apply — an out-of-band
  `rows` update to a cell the user has already typed into no longer replaces the draft they are still
  editing, while an untouched editor still picks up a new `rows` value normally. Double-click editors
  (`editable: true`) keep the property binding and its deliberate re-assert, unchanged. `lr-cell-edit`
  remains the only mutation channel; the table still never mutates `row`.
- 43ee7d0: `lr-table`: the empty state is now addressable, cells can carry a native tooltip, `table-layout` is
  settable, and the selected row has its own background custom property.

  - Every built-in `<lr-empty>` the table renders carries `part="empty"` and re-exports its own inner
    parts as `empty-base`/`empty-icon`/`empty-heading`/`empty-description`/`empty-actions`, so the
    empty state can be restyled from outside without replacing it. Note that the no-columns and
    no-rows states return the empty element as the shadow root's own root, so `::part(base)` does not
    apply in those two states — only in the filtered-to-zero one.
  - A new `empty` slot replaces the built-in empty state wholesale on the two _data_-empty branches
    (no rows at all, and filtered/paginated down to zero). The no-columns branch keeps its own
    `noColumnsHeading` copy and is deliberately not slot-replaceable — it reports a configuration
    problem, not an empty result set.
  - New `emptyCompact` property (`empty-compact` attribute) overrides the built-in empty state's
    `compact` density. Left unset it preserves today's per-branch behaviour exactly: spacious for the
    whole-table states, compact for the in-table filtered-to-zero one.
  - New `columns[].cellTitle(row)` renders the generated `<td>`'s native `title`, symmetrical with
    `cellStyle`. Returning `undefined` or an empty string omits the attribute entirely rather than
    rendering `title=""`, which would suppress an ancestor's own tooltip, and the attribute is
    suppressed while that cell is in inline-edit mode so the tooltip cannot shadow the editor. Some
    screen readers announce a `<td title>` as the cell's accessible name, so use it only for a longer
    form of what the cell already shows.
  - New `layout: 'auto' | 'fixed' = 'auto'` property (reflected) sets a floor for the table's
    `table-layout`. `fixed` applies the fixed algorithm even with no column widths declared; the
    default `auto` still resolves to fixed whenever a column declares a `width` or a drag-resize is in
    flight, since resizing does not work under `table-layout: auto`. Under `fixed` with no declared
    widths the first row determines every column's width — so revealing a `priority`-hidden column
    re-measures all of them — and `columns[].minWidth`/`maxWidth` are ignored by the fixed algorithm.
  - New `--lr-table-row-selected-bg` custom property (default `var(--lr-color-brand-quiet)`) recolors
    the `aria-selected` row. Shadow Parts forbids an attribute selector after `::part()`, so
    `::part(row)[aria-selected]` is invalid CSS and the selected row could previously only be
    restyled by overriding the library-wide brand-quiet token. Unset, rendering is unchanged.

- 437bef5: `lr-table`: add a skeleton loading mode. A new `loadingAppearance: 'spinner' | 'skeleton'`
  property (attribute `loading-appearance`, default `'spinner'` — unchanged output) controls how
  `loading` renders. `'skeleton'` keeps the real `<colgroup>`, `<thead>`, filter field and
  pagination footer in place and fills the table body with placeholder `<lr-skeleton>` rows, so a
  cold load sketches the grid's shape and holds its column geometry instead of collapsing to a
  spinner and reflowing when the rows arrive. The placeholder row count comes from the new
  `skeletonRows` property (attribute `skeleton-rows`, default `0` = derive from the normalized
  `pageSize`, capped at 20, else 3). Exactly one `role="status"` live region announces the load —
  each placeholder opts out of its own announcement, so there is no per-cell live-region storm. A
  `priority`-hidden column is given no visible placeholder cell. New `skeleton` CSS part targets the
  placeholders.
- bc8cb8b: Make the focus-ring and icon-button-size tokens themeable from an ancestor, and fill out
  `theme.css` with the inputs it was missing.

  `--lr-focus-ring-width`, `--lr-focus-ring-offset` and `--lr-icon-button-size` were the only
  three tokens declared as bare literals instead of chaining through a `--lr-theme-*` input.
  That made them the only tokens genuinely unreachable for subtree theming: a `--lr-*` token is
  re-declared on **every** `LyraElement`'s `:host`, so a value set on an ancestor is shadowed at
  the first intervening lyra host and never reaches anything nested inside it. `--lr-theme-*`
  inputs are declared only at `:root` (in `theme.css`) and never in component shadow styles, so
  they _do_ inherit through nested shadow roots — which is why the bridge is the supported route.
  The three tokens now read `--lr-theme-focus-ring-width`, `--lr-theme-focus-ring-offset` and
  `--lr-theme-icon-button-size`, with their existing values as fallbacks, so nothing renders
  differently by default.

  Keep a resolved `--lr-theme-icon-button-size` at or above 24px: it backs the hit area of
  `lr-date-input`, `lr-combobox`, `lr-input` and `lr-select`, and anything smaller fails
  WCAG 2.2 SC 2.5.8 (Target Size (Minimum)).

  `src/theme.css` also gains the type scale, spacing scale, stacking layers, chart palette,
  the 16 ANSI terminal slots, the raised surface and both overlay scrims as real inputs — every
  one set to the exact value it already fell back to, so importing the sheet changes no computed
  value. Two fixes came with that:

  - `.lr-dark` never set `--lr-theme-color-surface-raised`, so a `.lr-dark` page rendered raised
    surfaces at the light `#f6f8fa` while `prefers-color-scheme: dark` rendered them at `#22272e`.
    The dark block now mirrors the raised surface and the eight chart colors.
  - `--lr-color-overlay` and `--lr-color-overlay-strong` both read a single
    `--lr-theme-color-overlay` input, so defining that input flattened the strong scrim's `0.92`
    onto the plain scrim's value. `--lr-color-overlay-strong` now has its own
    `--lr-theme-color-overlay-strong` input, chained through the old one so a theme that sets only
    `--lr-theme-color-overlay` still tints both exactly as before.

- e9c4f22: `lr-thread-list` forwards a `compact` row density

  A reflected boolean `compact` (default `false`) that sets `compact` on every data-mode row
  `<lr-conversation-item>`, mirroring how `editable` is already forwarded — the one-attribute way to
  tighten a whole sidebar, where previously the only lever was styling `::part(row-item-base)` and
  `::part(row-item-title)` by hand. The density itself lives on the row item; this property only
  forwards it, so both components stay in sync from one implementation.

  Slotted mode (empty `threads` _with_ real slotted content) is a documented no-op: that mode renders
  host-supplied `<lr-conversation-item>`s as-is, so the host sets `compact` on its own items there —
  the same division of responsibility slotted mode already has for every other row property.

- b9d78b7: `lr-thread-list` now forwards the row `<lr-conversation-item>`'s own CSS parts out of data mode under
  a `row-item-*` namespace: `row-item-base`, `row-item-option`, `row-item-leading`, `row-item-content`,
  `row-item-title`, `row-item-title-input`, `row-item-rename-button`, `row-item-excerpt`,
  `row-item-meta`, `row-item-timestamp` and `row-item-actions`.

  Data mode builds each row itself, two shadow roots down, so until now none of those eleven parts were
  reachable from outside — including the two declarations that set row height. Row density could only
  be changed with `lr-thread-list::part(row) { --lr-theme-space-s: … }`, a whole-subtree retheme that
  also shrank everything nested in the row (a `renderActions` menu's items dropped below the
  touch-target floor and had to be un-retheme'd inline). `lr-thread-list::part(row-item-base)
{ padding-block: … }` now sets row density with no token override and no collateral damage.

  The existing `row-leading`/`row-content`/`row-meta`/`row-actions`/`row-wrapper` parts are unchanged;
  they wrap this component's render-callback output, which is a different surface from the item's own
  internals. Purely additive: an unstyled thread list renders identically.

- 9010a89: `lr-thread-list` exposes a `row-wrapper` CSS part around `wrapRow` output.

  `wrapRow` was the one row hook with no library-added part -- `renderLeading`, `renderRowContent`,
  `renderMeta` and `renderActions` each get a `row-*` wrapper, so a host wrapping a whole row had to
  thread its own class through the callback to lay it out. Its return value is now placed inside a
  `part="row-wrapper"` block `<div>`, reachable from outside as `lr-thread-list::part(row-wrapper)`.

  The wrapper is deliberately unstyled and block-level, and is added only when `wrapRow` is set: the
  box the internal `lr-virtual-list` measures for windowing is its own `[part="row"]` one level up,
  and an unstyled block box contributes exactly its child's height to it, so measured row heights are
  unchanged. The part is row-only -- group headers never pass through `wrapRow` and never carry it.

- 81af4b0: Add `sticky-groups` to `lr-thread-list`: the current date/custom group's header stays pinned to the
  top of the scroll viewport while its rows are in view, and is pushed off as the next group's header
  arrives. Group headers are ordinary virtualized rows, so this renders an `aria-hidden` copy into
  `lr-virtual-list`'s sticky layer — the real row keeps the `role="heading"` semantics and the tab
  order, while the pinned copy stays clickable and requests the same `lr-group-toggle` collapse. The
  band is exported as `::part(group-sticky)`, and the copy renders the same
  `group-header`/`group-toggle`/`group-label`/`group-icon` parts as the real header, so existing
  header styling applies to both. Default `false` renders exactly as before.
- 81af4b0: Remove `lr-thread-list`'s reach into the internal `lr-virtual-list`'s shadow root. Arrowing past the
  rendered window now scrolls through the child's public `scrollContainer` and waits for its
  `lr-scroll` notification before moving focus, instead of mutating the scroll position of an element
  found by querying the child's render root and then dispatching a fabricated `scroll` event at it —
  which also raced the child's re-render rather than following it. Row lookup goes through a new
  `lr-virtual-list.renderedRows` accessor (the currently-windowed `[part="row"]` wrappers, in item
  order), added because a windowed list gives a host no other way to reach a row that may not have
  existed a frame earlier; `exportparts` forwards styling, not element references.
- cea6d8e: `lr-token-input` can now edit a token in place. Set `editable` and each token becomes a roving tab
  stop that opens an inline editor on click, Enter, or F2: Enter commits and emits
  `lr-token-edit` with `{ value, previousValue, index }`, Escape reverts silently, and a blur commits
  without stealing focus back. New `token-label` and `token-editor` CSS parts (rendered only while
  `editable` is set) and a `--lr-token-input-editor-inline-size` custom property style the two states;
  with `editable` unset the token row renders exactly as before and stays non-focusable.

  `delimiter` now accepts `null` — as a property, or via `delimiter="none"` / `delimiter=""` — so a
  token may contain commas verbatim (`Bash(git status:*)`): nothing is split and no keystroke is
  treated as a commit key. Removing the attribute restores the `,` default, and an empty delimiter no
  longer explodes a draft into one token per character.

- 0a5666d: `<lr-tree>` gains a `reorderable` opt-in for keyboard reordering. With it set, Ctrl/Cmd+ArrowUp /
  Ctrl/Cmd+ArrowDown on the focused row emits `lr-reorder` with
  `detail: { id, parentId, fromIndex, toIndex }` — sibling-scoped indices within the node's own
  parent's child list (`parentId` is `null` for a top-level item), so a reorder can never turn into
  a reparent at a subtree boundary. The keybinding matches `<lr-dashboard-grid>`'s existing
  `cells-draggable` keyboard move; Alt+Arrow was avoided because it is browser back/forward on
  Windows and Linux. `data` stays host-owned — the event is a request, and the move is announced
  through an internal `<lr-live-region>` (new `treeNodeMoved` message key).

  Also fixes a pre-existing focus bug this surfaced: reassigning `data` in a way that merely
  _re-indexes_ the focused node (rather than removing it) dropped real DOM focus to `<body>`.
  Focus now follows the node, including for nested rows several shadow roots down.

  `reorderable` is `false` by default — unset, markup and keyboard behaviour are unchanged and no
  `lr-reorder` is ever emitted. `<lr-file-tree>` deliberately does not forward it: its tree items are
  derived from `nodes` and keyed by filesystem path, an order it does not own.

- 8774f0d: Add `lr-virtual-list` position queries: `offsetForIndex(index)` returns the pixel top row `index`
  renders at (clamped to `0…items.length`, so `offsetForIndex(items.length)` is the total content
  height), and `indexAtOffset(px)` returns the row whose box contains that offset (`-1` for an empty
  list). Both work in the same coordinate space as the scroll container's `scrollTop`, so a host can
  do scroll-linked layout without duplicating the windowing math; in `row-height="auto"` mode an
  unmeasured row's offset stays estimate-based until its `ResizeObserver` measurement lands.
- 8774f0d: Add `lr-virtual-list`'s sticky group header layer. Setting `renderStickyGroup` renders a
  `[part="sticky-group"]` overlay pinned to the top of the scroll viewport showing whichever `groups`
  entry the viewport is currently inside, pushed out by the overlap as the next group's header arrives
  rather than swapped abruptly. Native `position: sticky` on the rows themselves is structurally inert
  here, since every row is absolutely positioned and transform-offset by the windowing math.

  The overlay is a visual copy of content that already exists in the list, so it is `aria-hidden`, its
  ordinary focusable content is forced to `tabindex="-1"` (the real row keeps sole ownership of the
  heading semantics and of the tab order), and it is `pointer-events: none` until a consumer opts in
  with `lr-virtual-list::part(sticky-group) { pointer-events: auto; }`. It is measured by its own
  `ResizeObserver` and never by the row observer, so a group header that is also a real row is not
  double-counted in `row-height="auto"` mode. A `groups` entry whose `label` is the empty string now
  renders no `[part="group"]` marker — it is a pure position anchor, for a host that renders its own
  group headers as rows. With `renderStickyGroup` unset, nothing about the rendered output changes.

- 8774f0d: Add `lr-virtual-list`'s public `scrollContainer` getter (the `[part="base"]` scroll box, `undefined`
  before the first render) and an `lr-scroll` event (`detail: { scrollTop, viewportHeight }`). The
  event is emitted from the animation frame that already coalesces native `scroll` events, so a burst
  of them produces at most one `lr-scroll` per frame and none at all when the position did not change.
  Together they let a host follow _sub-row_ scroll movement — which `lr-visible-range-changed`, firing
  only on index-range changes, cannot report — without reaching into the component's shadow root or
  dispatching synthetic `scroll` events at it.

### Patch Changes

- 2e16fad: Fix `lr-artifact-panel`'s restore/copy/download header buttons rendering fully raw browser chrome
  (zero CSS at all) while the adjacent header buttons in the same row are fully themed, and give
  view-button its own hover/focus-visible to match its version-previous/version-next siblings.
- c2ddee5: Fix `lr-av-player`'s playback-rate `<select>` rendering raw browser chrome with an unthemed
  (typically white) option popup regardless of theme -- it now resets native appearance, themes its
  option list, and gains hover/focus-visible states and a decorative chevron in place of the removed
  native one.
- db4e0a5: Fix `lr-calendar`'s previous-month nav button never matching its own styling rule (it rendered with
  raw browser button chrome next to a fully themed next button) and add missing `:hover`/`:focus-visible`
  treatment to the nav buttons, day-grid cells, and agenda-event buttons.
- bfaf7f9: `lr-checkbox-group`: document `value` as a read-out of child state, and warn on the two ways it is
  misused.

  `value` shipped with no documentation at all while the generated docs listed it among settable
  properties, so it read as an input. It never was one: `sync()` recomputes it from the
  `<lr-checkbox>` children and assigns it on every child toggle, `slotchange`, `name`/`required`
  change, blur and `form.reset()` — and `connectedCallback()` syncs _before the first render_, so even
  a constructor-time or template-time `.value=` binding is discarded before it is ever observed. It
  now carries that contract in its JSDoc, and:

  - assigning `value` from outside logs a `console.warn` naming the property and pointing at `checked`
    on the children (once per element — a repeat assignment is the same mistake, not new information);
  - a group with two or more children sharing a `value` logs a `console.warn` too. This is the _easy_
    mistake, not an exotic one: `<lr-checkbox>`'s `value` defaults to `'on'`, so five undifferentiated
    children yield `['on','on','on','on','on']` and a `FormData` that cannot say which was checked.

  Both warnings follow the same plain-`console.warn` shape as the library's other authoring-mistake
  warnings (`lr-task-list` over-nesting, `lr-dashboard-grid` unmatched `cell-id`, `lr-flow-canvas`
  unrecognized child). No behavior changed for the normal children-drive-value flow, which warns not
  at all.

  `value` was deliberately **not** made authoritative. Push-down is unimplementable without surprise
  while children default to `value = 'on'` (a host assigning `['on']` would check every
  undifferentiated child), and it would additionally need a re-entrancy guard and a pending-value
  retention path for children that have not upgraded yet. Recorded here so a later release can add a
  distinct `defaultValue` API without reversing anything documented now.

- 2a45da4: Fix four components (`lr-chunk-inspector`, `lr-community-card`, `lr-provenance-panel`,
  `lr-notebook-viewer`) whose real `<button>`s get UA-chrome reset (`border:none; background:
transparent; cursor:pointer;`) but no hover or focus-visible of their own -- `lr-provenance-panel`'s
  disclosure header (`aria-expanded`/`aria-controls`) had zero visible keyboard focus indicator at all.
- 1d121a9: Fix `lr-code-block`/`lr-code-block-core`'s shiki dark-theme override only activating on the OS-level
  `prefers-color-scheme` media query -- a consumer who sets `--lr-theme-color-*` explicitly, without the
  OS itself being in dark mode, now correctly gets the dark shiki syntax theme too, matching every other
  `--lr-color-*` token's consumer-overrides-first resolution.
- 1372546: Fix `lr-color-picker`'s native color swatch -- the directly visible, directly focusable control --
  having no hover or focus-visible treatment, so tabbing to it fell through to the browser's raw
  default color-input focus ring.
- f8bc916: `lr-combobox`: the `clearable` button now covers the filter axis as well as the selection.

  Typing a query that matches nothing left the user with no affordance to clear it — the button was
  gated on a committed selection alone, and `clear()` early-returned on an empty selection. It now
  renders whenever there is something to clear on either axis, and each axis announces only its own
  change: clearing a selection still emits `input`/`change`/`lr-clear`, while clearing filter text
  emits `lr-filter` with an empty `value` and no spurious selection events.

  The query half of the gate is scoped to states where the query is actually visible — the open
  listbox in single-select, or any time in `multiple` mode. A closed single-select shows the selected
  label rather than the query, so a stale query alone never surfaces a button offering to clear text
  the user cannot see.

- 77bfb28: Fix `lr-data-grid`'s sort-header focus ring targeting `<th>`, which can never itself receive
  keyboard focus (only its nested sort button can) -- tabbing to a sortable column header now shows
  the library's focus ring instead of the browser's raw default, and the sort button gains a
  matching hover state.
- dfd6199: Fix `lr-date-picker`'s previous/next month-nav buttons having a hover state but no focus-visible ring
  -- the file's only focus-visible coverage was on day cells, leaving keyboard users with no visible
  indicator on the nav buttons.
- 7c99e80: Route several stray hardcoded style values through design tokens so visually-identical states stay
  in sync across components:

  - **Disabled controls** in `lr-node-palette`, `lr-flow-controls`, `lr-compare-panel`,
    `lr-graph-query-builder`, and `lr-rubric-form` now dim through the shared `--lr-opacity-disabled`
    token instead of one-off `0.4`/`0.5`/`0.6` literals, so every disabled control fades by the same
    amount (and rethemes with one property).
  - **Anchored popovers/menus/tooltips** (`lr-menu`, `lr-select`, `lr-combobox`, `lr-date-input`,
    `lr-model-select`, `lr-voice-picker`, `lr-mention-popover`, `lr-export-button`, `lr-tour`,
    `lr-tool-call-chip`, `lr-usage-badge`, `lr-citation-badge`, `lr-entity-chip`,
    `lr-knowledge-graph-explorer`) share a new `--lr-popover-viewport-clamp` token (default `92vw`,
    themeable via `--lr-theme-popover-viewport-clamp`). Previously these split between `92vw` and
    `90vw`, so two popovers side by side could clamp to different widths; they now clamp consistently.
  - **Solid-fill hover lift** on `lr-chat-composer`, `lr-tool-approval-dialog`, `lr-message-feedback`,
    `lr-tour`, and `lr-retrieval-search` now shares a new `--lr-hover-brightness` token (default
    `1.08`, themeable via `--lr-theme-hover-brightness`), replacing per-component `filter: brightness()`
    magic numbers. Note `lr-retrieval-search`'s submit button now _brightens_ on hover like every other
    brand button, where it previously darkened (`0.92`).
  - `lr-calendar`'s narrow-container day-cell floor now references the existing `--lr-size-4rem` token
    instead of a raw `4rem`, matching its wide-container sibling.

  Also adds a new consumer override hook: `--lr-responsive-panel-sheet-max-block-size` (default `85dvh`,
  falling back to `85vh` where `dvh` is unsupported) lets you set the maximum height of an
  `lr-responsive-panel` `variant="bottom-sheet"` overlay, which previously had no override at all.

- ac5936a: Fix `lr-details`' summary -- the component's real, natively-focusable/clickable surface -- having no
  hover or focus-visible treatment at all. `lr-accordion-item` (which extends `lr-details` with no
  style override) is fixed by the same change.
- 188335c: Sync the consumer-facing agent reference (`llms/`) with the part-reachability, density and composed
  -content work that just landed across the viewers, media, retrieval, agent-tools, layout,
  conversation and data families.

  - Document the newly forwarded and newly named CSS parts on `lr-pdf-viewer`, `lr-archive-viewer`,
    `lr-page-rail`, `lr-notebook-viewer`, `lr-csv-viewer`, `lr-spreadsheet-viewer`,
    `lr-dataset-viewer`, `lr-av-player`, `lr-terminal`, `lr-ingestion-queue`, `lr-neighbor-list`,
    `lr-chunk-inspector`, `lr-retrieval-results` and `lr-activity-feed`, including why row state is
    published as an extra part name rather than an attribute on the part.
  - Replace the paragraphs that described `--lr-page-rail-current-bg`,
    `--lr-notebook-viewer-active-bg`, `--lr-av-player-cue-current-bg` and
    `--lr-av-player-cue-active-match-color` as declared-but-inert; all four now take effect.
  - Document `--lr-csv-viewer-highlight-color` and `--lr-spreadsheet-viewer-highlight-color`, and
    `--lr-trace-tree-row-active-color` (plus the pairing rule it forms with
    `--lr-trace-tree-row-active-bg`, and the knock-on note under `lr-agent-trace`).
  - Document `lr-menu`'s `header`/`footer` slots and parts, the revised Escape/Tab keyboard contract,
    and the narrowed scope of `closeOnEscapeAnywhere`.
  - Document `lr-table`'s `columns[].editable: 'always'` persistent editors, `lr-flow-node`'s
    `compact` and `card` part, `lr-flow-controls`' and `lr-chat-composer`'s `appearance`, and
    `lr-conversation-item`/`lr-thread-list`'s `compact`.

- 2be1ad5: Sync the consumer-facing agent reference (`llms/`) with the sticky group-header work on
  `lr-virtual-list` and `lr-thread-list`.

  - Document `lr-virtual-list`'s `renderStickyGroup`, the `sticky-group` CSS part, and the four
    behaviors a consumer would otherwise get wrong: the band is `aria-hidden` with its focusable
    descendants forced to `tabindex="-1"` (so it is never a second tab stop or a second heading, and a
    focus-delegating custom element inside it must set its own), it is `pointer-events: none` until
    opted back in through `lr-virtual-list::part(sticky-group)`, it is never measured as a row, and it
    stays mounted but hidden above the first group so its scroll inset is measurable before the first
    jump.
  - Document that a `groups` entry with an **empty** `label` renders no marker and acts as a pure
    position anchor, and drop the stale claim that `groups` had no visible effect and that its marker
    carried `role="heading"`.
  - Document `offsetForIndex()`/`indexAtOffset()`, the `scrollContainer`/`renderedRows` getters, the
    `lr-scroll` event and its `VirtualListScroll` detail type, and add a sticky-group usage example.
  - Document `lr-thread-list`'s `stickyGroups` property (attribute `sticky-groups`) and the
    `group-sticky` exported part, including that the real header row keeps the
    `role="heading"`/`aria-level` semantics and the tab order while the pinned copy stays clickable.

- ed762ff: `lr-xml-viewer` treats `--lr-icon-button-size` as a floor

  `lr-xml-viewer`'s node `[part='toggle']` is an interactive button that pinned the shared
  minimum-target token as a fixed `inline-size`/`block-size` with `padding: 0` and no floor — the
  opposite of what the token's own definition documents ("components pad out to this via
  `min-inline-size`/`min-block-size`, not by growing the glyph itself"). It now sizes its glyph box at
  `--lr-size-1-25rem` with `min-inline-size`/`min-block-size: var(--lr-icon-button-size)`, mirroring
  `lr-code-block`'s equivalent toggle, so lowering the token shrinks the hit area but never squashes
  the chevron.

- 4c59cc2: Fix `lr-image-viewer`'s fit-mode `<select>` rendering raw browser chrome with an unthemed option
  popup, and add missing hover/focus-visible to all three toolbar controls (fit-control, rotate-button,
  annotate-toggle) -- previously none of the three had either state.
- 10c8b91: Fix `lr-input` (and `lr-time-input`, which renders through the same template/stylesheet) keeping
  native browser chrome in three cases: the search-cancel glyph only reset while `clearable` was set
  (the common non-clearable case kept it), `type="number"` never resetting the spin-button, and
  `type="time"` never touching its calendar-picker-indicator at all -- now restyled, not suppressed,
  since it's the only mouse/touch affordance to open the native time picker.
- 0410eb7: Fix two factual errors in the shipped agent-facing reference (`llms/shared.md`, and the
  `llms.txt`/`llms-full.txt`/`llms/` artifacts generated from it): the internals section stated
  `LYRA_PREFIX = 'lyra'` when the constant is `'lr'` — on the same line that correctly showed
  `tag(name)` producing `` `lr-${name}` `` — and claimed a hardcoded count of 127 `Lyra*EventMap`
  types when there are now 181. The count is no longer stated as a number, so it cannot drift again.
- 184bfff: `lr-menu`: axe coverage for a composed popup, stories moved onto the new `header`/`footer` slots,
  and the three shipped descriptions of what this component accepts finally agree.

  - New axe assertion for a menu with a `header` `<input>` and a `footer` `<button>` — the exact shape
    that was an `aria-required-children` violation while the only place for it was inside
    `role="menu"`, and which no test covered.
  - `show() / hide({ focusTrigger: true })`'s Apply button moves to `slot="footer"`, and the filter
    field gets a new `header`-slot story. The old default-slot filter story stays, relabelled as the
    legacy shape it now is, so its `closeOnEscapeAnywhere` behavior remains covered.
  - The class doc's `@slot` tag said "menu items and `<hr>` only" while the interaction contract two
    paragraphs above it promised slotted controls "keep their own full default keyboard behavior" and
    `show()`/`hide()` named a slotted Apply button as a supported case. All three now describe the
    same component.

- 3c8a299: Add missing `:hover` to six agent-tools components (`lr-browser-frame`, `lr-commit-card`,
  `lr-terminal`, `lr-test-results`, `lr-compare-panel`, `lr-confirm-bar`) whose interactive buttons
  already had `cursor: pointer` and a correct focus-visible ring but no hover affordance for mouse
  users.
- 4ac6c31: Add missing `:hover` to six components (`lr-stack-trace`, `lr-span-waterfall`, `lr-chat-viewport`,
  `lr-checkpoint`, `lr-push-to-talk`, `lr-transcript-feed`) whose interactive controls already had
  `cursor: pointer` and a correct focus-visible ring but no hover affordance for mouse users.
- 696cc7f: Add missing `:hover` to six components (`lr-env-list`, `lr-graph-query-builder`, `lr-rubric-form`,
  `lr-chart`, `lr-scroller`, `lr-widget`) whose interactive controls already had `cursor: pointer` and a
  correct focus-visible ring but no hover affordance for mouse users; `lr-chart`'s reset-zoom-button also
  gains `font: inherit`, which it was missing entirely.
- e1b9c22: Add missing `:hover` to six components (`lr-carousel`, `lr-dashboard-grid`, `lr-callout`,
  `lr-memory-panel`, `lr-neighbor-list`, `lr-path-strip`) whose interactive controls already had
  `cursor: pointer` and a correct focus-visible ring (where applicable) but no hover affordance for
  mouse users.
- e73a243: Add missing `:hover` to six components (`lr-retrieval-results`, `lr-pdf-viewer`, `lr-ebook-viewer`,
  `lr-pptx-viewer`, `lr-email-viewer`, `lr-dataset-viewer`) whose interactive controls already had
  `cursor: pointer` and a correct focus-visible ring but no hover affordance for mouse users.
- ae8e04e: Fix nine components (`lr-combobox`, `lr-eval-dataset`, `lr-command-palette`, `lr-table`,
  `lr-tool-select-dialog`, `lr-code-editor`, `lr-message-feedback`, `lr-model-select`, `lr-voice-picker`)
  whose native `<input>`/`<textarea>` themed background/color/border correctly but left `::placeholder`
  at the browser's fixed light-tuned default -- each field's placeholder text now uses
  `--lr-color-text-quiet`, with Firefox's reduced default `::placeholder` opacity undone on the
  `type="search"` fields.
- e649e77: Fix `lr-node-palette`'s search field being the only `type="search"` field in its family with zero
  focus-ring styling (its siblings `lr-thread-list`/`lr-emoji-picker` already wire this), and reset the
  native search-cancel glyph to match.
- e879ff6: Fix `<lr-split>` and `<lr-stepper>` reporting a stale `effectiveOrientation` (and
  `data-effective-orientation`) when `orientation-breakpoint-basis="viewport"` and the viewport
  crossed the breakpoint while the element was detached from the DOM. The media-query listener is
  torn down on disconnect and a plain reconnect schedules no Lit update, so the missed crossing was
  never noticed; reconnecting now re-reads the query and announces the crossing (including
  `lr-split-orientation-change` / `lr-stepper-orientation-change`) only when the matched state
  actually differs. A plain mount, and a reconnect that crossed nothing, stay silent as before.
- c2ea153: Fix `lr-pagination`'s page-input and `lr-tool-param-form`'s numeric JSON-schema fields rendering the
  native spin-button inside a fixed-size control box -- the adjacent prev/next buttons (pagination) and
  form validation (tool-param-form) already provide stepping, so removing the spinner loses no
  functionality.
- 94fa823: `lr-terminal`, `lr-ingestion-queue` and `lr-dataset-viewer`: forward the CSS parts rendered through
  their internal `<lr-virtual-list>` so a consumer can actually reach them.

  All three already styled those parts correctly from their own stylesheets, but none forwarded
  `exportparts` from the `<lr-virtual-list>` element. Because the rows are `renderItem`'s output and
  therefore live inside that element's own shadow root, a consumer rule like
  `lr-terminal::part(line)` matched nothing at all — the documented parts were unreachable from
  outside the component.

  - `lr-terminal` now exports `line`.
  - `lr-ingestion-queue` now exports `item`, `item-header`, `item-name`, `item-progress`,
    `item-meta`, `item-error`, `item-actions`, `retry-button` and `cancel-button`.
  - `lr-dataset-viewer` now exports `data-row`, `cell`, `cell-highlight` and `cell-highlight-action`.

  No styling changed and no new parts were added.

- 56f7b65: Add a build-time guard against `::part()` CSS that parses but never matches.

  Two classes of silently-inert rule are now caught by `pnpm lint` (a new
  `scripts/check-part-reachability.mjs` in the contract-policy chain), neither of which any existing
  check — TypeScript, the style policy, or a test that inspects stylesheet text — could see:

  - **`cross-root-part`** — a component that mounts `<lr-virtual-list>` and hands it a
    `renderItem`/`renderGroup` callback renders those rows into _that element's_ shadow root, so a
    bare `[part='x']` selector in the composing component's own stylesheet can never match them. The
    checker cross-references the literal part names emitted from the callback (following the class
    members it reaches) against the bare `[part]` selectors in the sibling `*.styles.ts`, and reports
    any name that has no `lr-virtual-list::part(x)` rule anywhere in that file. Components that
    legitimately render the same part into both roots — below/above a virtualization threshold, or a
    directly-rendered header row — carry both selectors and are not flagged; a
    `policy-allow(cross-root-part):` comment covers anything else.
  - **`part-compound`** — per Selectors L4 a pseudo-element may only be followed by pseudo-classes, so
    `::part(a)[attr]`, `::part(a).cls`, `::part(a) .descendant` and `::part(a) > .child` parse and
    then match nothing. Every `*.styles.ts` is scanned for those shapes; `::part(a):hover`,
    `::part(a)::selection` and the part-list form `::part(a b)` remain valid and pass.

  No component behavior changes; the library is clean under both rules today.

- cce32a2: `lr-neighbor-list`: make the virtualized relationship rows and group headers actually styleable, by
  this component and by a consumer.

  Above `virtualizeAt` the rows are produced by this component's `renderItem` but committed into the
  embedded `<lr-virtual-list>`'s own shadow root, one boundary deeper than a `[part='row']` selector
  can reach — so every row, node-label, direction, relation, meta and expand-button rule was silently
  inert and a large neighborhood rendered as raw browser `<button>`s with no dividers. Each rule now
  pairs its plain selector (still correct at/below the threshold) with an `lr-virtual-list::part(…)`
  twin, and an `exportparts` forwarding declaration makes the same parts reachable as
  `lr-neighbor-list::part(node-label)` etc. from a consuming stylesheet.

  Group headers were unstyled whenever the list virtualized: in that path the header is the internal
  virtual-list's own `group` part, which this component neither styled nor exported. It is now styled
  to match `group-header` and exported under that same name, so grouped rows present identically
  either side of the threshold.

  The virtualized rows no longer nest a second `role="listitem"`/`part="row"` element inside the
  virtual-list's own row wrapper. `renderItem` returns just the row's content, exactly as the
  non-virtualized path's own wrapper receives it: the duplicate nesting both reported a `listitem`
  inside a `listitem` and made the row's padding and divider border apply twice, since `::part()`
  matches at any depth of the target shadow tree.

- 1e518e6: Fix `lr-playback`'s range slider only getting a pointer cursor in its disabled state, unlike the
  adjacent play button, and add a matching hover affordance.
- 5e9a18e: `lr-table`: focus the cell editor that was actually just opened by a double-click. The autofocus
  looked up `[part="cell-editor"]` across the whole grid and focused whichever one came first in the
  DOM — indistinguishable from correct while only one editor could ever exist at a time, but wrong as
  soon as a column renders persistent (`editable: 'always'`) editors of its own. It is now scoped to
  the opening cell's own row and column.
- 326973c: Fix `lr-thread-list` and `lr-emoji-picker`'s otherwise fully-themed search fields showing a raw
  gray browser "x" glyph (with its own hit target and hover behavior, ignoring every token applied to
  the field) once non-empty.
- 9010a89: `lr-thread-list` and `lr-chat-viewport` now size their virtual list to their own height.

  Both composed an `lr-virtual-list` without ever setting `--lr-virtual-list-height`, so the list
  scrolled inside that token's 24rem default no matter how tall the surrounding pane was -- a
  `<lr-thread-list>` in a 700px sidebar showed a 384px scroller with dead space underneath, and every
  consumer had to hand-set `--lr-virtual-list-height` to work around it. Both now fill the height they
  are given with no consumer CSS. `lr-thread-list` degrades safely: in a container with no resolvable
  height the internal viewport still renders at exactly the 24rem it does today (the shipped default
  becomes the list's flex-basis rather than a percentage that would collapse to zero or grow to the
  full un-virtualized content height). `lr-chat-viewport`'s virtual mode uses a percentage -- the
  slotted list lives in the consumer's light DOM, out of reach of `::part()` -- so it, like slotted
  mode's own scroll container, needs a height-bounded parent. A consumer rule or inline style setting
  `--lr-virtual-list-height` on the list still wins in both components.

  Also fixes `lr-chat-viewport`'s virtual-mode layout rules, which were written as
  `:host(:has(> lr-virtual-list))`. `:has()` is invalid inside `:host()`, so those rules were silently
  dropped: in virtual mode `[part="scroll"]` kept the padding and `overflow-y: auto` it is documented
  to give up, and `[part="content"]` never got the height the slotted list sizes against.

- 3e1d4f8: Fix `lr-token-input`'s draft-input and inline token-editor leaving `::placeholder` at the browser's
  default color, and add missing `:hover`/`:focus-visible` to the per-token remove button.
- 67a7881: Cover `lr-trace-tree`'s active row with an axe assertion. The active-row test group previously
  carried a comment explaining why no accessibility assertion could be made there — the default tint
  put the row's own secondary text below the WCAG AA contrast floor, so any axe run against a
  populated active row would have failed. With that fixed, the assertion now runs for real: a
  populated tree is asserted accessible with each status tone in turn made active, after first
  proving the fixture actually reached the `[data-active]` state so the check cannot pass vacuously.
  It was verified to bite by reverting the fix and confirming axe reports the exact contrast
  violations it is meant to catch. The active-row Storybook story now sets
  `--lr-trace-tree-row-active-bg` and `--lr-trace-tree-row-active-color` together and documents why
  they are a pair.
- 67a7881: Fix `lr-trace-tree`'s active-row secondary text falling below the WCAG AA 4.5:1 contrast floor. The
  active (`activeSpanId`) row paints `--lr-color-brand-quiet`, against which `--lr-color-text-quiet`
  lands at ~4.25:1 — so `detail`, `duration`, `tokens-in`, `tokens-out`, `cost` and the `pending`
  status label were all failing while the row was active, even though every one of them passes
  comfortably against the plain row background. Those parts now render at full-strength
  `--lr-color-text` once the row is active (15.3:1 in light mode, 11.2:1 in dark), the same fix
  `lr-conversation-item` already carries for the identical bug. Darkening the active tint instead
  would have made it worse: every failing foreground is dark text.

  This changes default rendering on the active row, which is intended — the previous default was a
  real accessibility failure. The new `--lr-trace-tree-row-active-color` custom property retunes it;
  it pairs with `--lr-trace-tree-row-active-bg`, and a consumer setting that to a dark tint in light
  mode should set both, because the defaults assume the active background stays on the same side of
  the lightness midpoint as the ambient surface.

- 67a7881: Raise `lr-trace-tree`'s active-row status labels to clear WCAG AA without flattening their hue.
  `[part='status-text']` on the active row now renders
  `color-mix(in srgb, var(--lr-color-<tone>) 75%, var(--lr-color-text))` for each semantic tone —
  success moves from 4.46:1 to 6.18:1 and `denied` from 4.28:1 to 5.96:1 against the default active
  tint, while `error` and `running` (which only barely cleared the floor) gain headroom too. Keeping
  the hue matters: an error row that stops being red once selected loses the fastest scan signal in a
  trace list.

  The mix is applied to every semantic tone rather than only the two that fail at the shipped
  defaults, because a per-status carve-out is theme-fragile — a consumer retheming one `--lr-color-*`
  moves that ratio and would silently re-break. It is also theme-symmetric by construction:
  `--lr-color-text` flips with the color scheme, so the same declaration darkens the label in light
  mode and lightens it in dark mode. `[part='bar']` is deliberately untouched — it is a non-text
  graphic on a 3:1 floor it already passes, and scoping the mix to `[part='status-text']` avoids
  re-pointing a consumer's own `--lr-color-*` override inside one row.

- 4df6ca1: `lr-virtual-list` no longer traps a popup opened from inside a row underneath the rows that follow
  it. `[part="row"]` sets `will-change: transform`, which makes every row its own stacking context, and
  rows carried no `z-index` — so they painted in DOM order and each row painted over the previous one.
  A `lr-menu` dropdown rendered in a row (for example through `lr-thread-list`'s `renderActions`) was
  positioned, visible and hit-testable, yet painted _under_ the next rows: its own `z-index: 900` only
  orders siblings inside its row's context. The last row always looked correct, so small fixtures never
  caught it.

  `[part="row"]:focus-within` now lifts the row to `var(--lr-layer-content)` — the same layer
  `[part="group"]` already uses — for exactly as long as something inside it holds focus. This also
  stops outward focus rings on a row being clipped by later rows. Nothing changes when no row holds
  focus.

- 8774f0d: Keep `lr-virtual-list`'s scroll-into-view clear of the sticky group band. With `renderStickyGroup`
  set, the band's measured height is applied as `scroll-padding-block-start` on the scroll container —
  so native keyboard and anchor scrolling get the same treatment — and subtracted from the
  top-aligned targets `active-id` and `scrollToIndex({ align: 'start' })` compute, which otherwise
  parked the target row underneath the band. `align: 'end'` is unaffected, since the band never
  covers the viewport's bottom edge. With `renderStickyGroup` unset the inset is zero and both scroll
  paths behave exactly as before, with no inline style on the container at all.

## 5.1.0

### Minor Changes

- 5f82bf7: Add role-scoped bubble cssprops to `lr-chat-message` — `--lr-chat-message-bubble-bg`,
  `--lr-chat-message-bubble-color`, `--lr-chat-message-user-bubble-bg`, and
  `--lr-chat-message-user-bubble-color` — so a consumer can retint one role's bubble fill/text
  without overriding the shared `--lr-color-brand-quiet`/`--lr-color-surface`/`--lr-color-text`
  tokens, which also drive unrelated parts of the component (e.g. `[part="collapse-button"]:hover`).
  All four default to exactly the values the bubble already used, so nothing changes for existing
  consumers who set none of them.
- abd60dd: `lr-stepper`'s `orientation-breakpoint` now accepts a CSS length, not only a bare pixel number:
  `500`, `'500'`, `'500px'`, `'31.25rem'` and `'3em'` are all valid, and equal computed values behave
  identically.

  `rem` resolves against the **document root**'s computed font size — exactly as a `rem` in a CSS
  `@media` query does, and deliberately _not_ against the stepper itself — so a breakpoint authored in
  `rem` stays numerically in step with the sibling `@media (max-width: …rem)` rule it has to agree
  with, instead of silently drifting from it when the root font size changes (browser zoom, a user
  font-size preference, an app base-size token). `em` resolves against the stepper's own computed font
  size. The length is re-resolved on every measurement and never cached, so those changes are picked
  up without any invalidation step on the consumer's side.

  A value that isn't a usable length — `%`, `vw`, `calc()`, `'auto'`, an unparseable string — now
  behaves exactly as unset: no `ResizeObserver` is armed and no `data-effective-orientation` attribute
  appears, rather than arming a breakpoint that can never be crossed. For a viewport-relative
  breakpoint, leave `orientationBreakpoint` unset and drive `orientation` from your own `matchMedia()`
  controller; `orientationBreakpoint` measures the stepper's own allocated inline size, not the
  viewport.

  The property's TypeScript type widens from `number | undefined` to `number | string | undefined`,
  and the `orientation-breakpoint` attribute is no longer coerced through Lit's `Number` converter.
  Every existing numeric usage — attribute or property — is unaffected. This mirrors the identical
  change to `lr-split`, whose `orientationBreakpoint`/`narrowOrientation` contract `lr-stepper`
  deliberately shares.

- 22cb935: `lr-heatmap` gains a `legendStops` property so the built-in legend can describe a custom
  `cellColor` domain. Because `cellColor` overrides a cell's color entirely, the legend's
  `--lr-heatmap-scale-lo`/`-hi` gradient bar could describe a ramp the grid no longer used, leaving a
  consumer to hide `::part(legend)` and hand-roll swatches.

  `legendStops: HeatmapLegendStop[]` (`{ value, color, label? }`, `attribute: false`) renders a
  discrete key **instead of** that gradient bar — one `[part="legend-stop"]` per entry in array order,
  each a `[part="legend-swatch"]` in the entry's color plus a `[part="legend-stop-label"]`. Labels
  default to the component's own locale-aware numeric formatting of `value`, so an explicit `label` is
  only needed when the number isn't the right caption. `[part="legend-lo"]`/`[part="legend-hi"]` and
  the bar are omitted while stops are supplied; labeled `annotations` still render their
  `[part="legend-annotation"]` entries alongside them.

  The stops are presentation only — they never feed back into the color ramp, the bucket math, the
  tooltip or the accessible name. Left unset (or empty), the legend renders exactly as before.

- ce2a423: `lr-combobox` now emits `lr-filter` (`detail: { value: string }`) on every user-driven change to its
  in-progress filter text, so consumers that need the live as-you-typed string — a "no matches for
  “x”" empty state, a debounced side effect — no longer have to reach into the component's shadow DOM
  for `[part="combobox-input"]`.

  The name is deliberately not `lr-input`: on `lr-combobox` the host's `value` is the _committed
  selection_, so reusing `lr-input`'s event name would make one event name carry a different string on
  different components. `lr-filter` fires for user input only — picking a row, the clear button,
  `form.reset()`, dismissing the listbox, a programmatic `value` write and `setRangeText()` all blank
  the filter silently, mirroring how `<lr-input>`'s `lr-input` only reports user edits.

  The `ComboboxFilterDetail` detail type is exported and `LyraComboboxEventMap` carries the new entry,
  so `addEventListener('lr-filter', …)` is typed.

- 7c46ced: `<lr-split>`'s `orientationBreakpoint` now accepts a CSS length string as well as a bare pixel
  number, so it can be authored in the same unit as the sibling CSS `@media` rule it has to agree
  with.

  Accepted forms: `900` / `orientation-breakpoint="900"` (unchanged), `'900px'`, `'56.25rem'`, and
  `'3em'`. `rem` resolves against the **document root**'s font size — exactly as a `rem` in a CSS
  `@media` query does, not against the element — so a breakpoint written to match
  `@media (max-width: 56.25rem)` stays in sync with it across browser zoom, a user font-size
  preference, or an app-level base-size change. `em` resolves against the split's own computed font
  size. The length is re-resolved on **every** measurement rather than cached at first render, so a
  root font-size change moves the crossing width with no invalidation step.

  Anything that isn't a resolvable length now behaves exactly as unset — no `ResizeObserver`, no
  `data-effective-orientation` marker — where before, a non-numeric attribute became `NaN` and armed
  observation for a threshold that could never be crossed. That deliberately includes `%`, `vw`/`vh`
  and `calc()`, which would mix a viewport-relative threshold into a measurement of the element's own
  allocation; drive `orientation` from your own `matchMedia()` controller for a viewport-relative
  breakpoint instead.

  One visible consequence of dropping the `Number` attribute converter: reading `.orientationBreakpoint`
  back after setting the attribute now returns the authored string (`'900'`), not the number `900`.
  The resulting layout behavior is identical, and the property type is now `number | string`.

- 2be0a50: `<lr-split>` gains `orientationBreakpointBasis` (`"container"` by default, `"viewport"`
  opt-in), selecting whether `orientationBreakpoint` is compared against the component's own
  measured inline size or a `matchMedia('(max-width: …)')` query. Viewport basis lets sibling
  components in one row flip orientation together at a single shared breakpoint — impossible
  to express with a self-measured threshold when the row stacks via a CSS `@media` rule — and
  lets the browser resolve a `rem` breakpoint with real media-query semantics. Left unset,
  behavior is unchanged.
- 96ea325: `<lr-stepper>` gains `orientationBreakpointBasis` (`"container"` by default, `"viewport"`
  opt-in), selecting whether `orientationBreakpoint` is compared against the stepper's own
  measured inline size or a `matchMedia('(max-width: …)')` query. Viewport basis is the only
  way a stepper with a fixed width in a row layout can react to that row stacking at a shared
  breakpoint. Left unset, behavior is unchanged.
- b1ce3f6: `lr-sequence-strip`: add `showLegend` for a persistent category key.

  The strip colors each cell by category, but the only way to read that mapping was to hover every
  cell one at a time — consumers were hand-rolling a swatch key underneath instead. `showLegend`
  (attribute `show-legend`, reflected, default `false`) now renders that key from the `categories`
  array the component already receives, as `legend` / `legend-item` / `legend-swatch` /
  `legend-label` CSS parts, with `--lr-sequence-strip-legend-swatch-size` to resize the chips.

  The legend is deliberately static — it lists every `categories` entry whether or not any item uses
  it, and toggles nothing (`lr-graph-legend` remains the interactive, filtering legend). Because it
  only repeats the category names the strip already announces through `[part="base"]`'s `role="img"`
  summary, the whole legend is `aria-hidden`: visible on screen, announced exactly once. Left unset,
  rendering is unchanged.

- 3127d5e: Restructure the AI-agent-facing reference so a component lookup costs a few hundred tokens instead
  of the whole catalog, and close the gaps that made it unreliable.

  **New published layout.** `llms/index.md` maps every tag to its import path and one-line purpose;
  `llms/components/<tag>.md` is a self-contained per-component reference addressed directly from the
  tag name; `llms/shared.md`, `llms/tokens.md`, `llms/peers.md` and `llms/migration.md` carry the
  library-wide contracts. `llms.txt` is now the entry index over all of it, and `llms-full.txt` keeps
  its role as the single-file concatenation. Everything is generated from per-family authored sources
  by `pnpm run llms` and diffed in CI, so the docs cannot drift from `custom-elements.json`.

  **Corrected documentation that was wrong, not merely missing:**

  - Import paths in the docs had not been updated for the family directory layout —
    `components/combobox/combobox.js` does not resolve; it is
    `components/forms/combobox/combobox.js`. CI now fails on any documented path that has no source
    module.
  - 26 components were documented twice with divergent content; the freshness check validated the
    weaker copy.
  - `lr-include` was documented with the wrong purpose, property semantics, event name and CSS parts.
  - Wrong or non-existent CSS parts on `lr-timeline`/`lr-timeline-item`, `lr-tour`, `lr-known-date`,
    `lr-random-content`, `lr-avatar-group`, `lr-breadcrumb`, `lr-swatch-picker`.
  - `lr-button` was missing the `quiet` appearance and the `2xs` size; `lr-attachment-trigger` was
    missing the `audio` capability; `lr-avatar` was documented as having no slots.
  - `lr-widget` event details are objects, not scalars; three overlay-color tokens do resolve to
    `var(--lr-color-overlay)`; `lr-histogram`'s `label` default is localized, not `'Frequency'`.
  - The root barrel skips 15 peer-gated tags, not 13 — `lr-knowledge-graph-explorer` and
    `lr-geojson-view` were undocumented omissions.

  **Newly documented:** the `@aceshooting/lyra-ui/ai` provider-neutral data types, the `locale` and
  `strings` properties present on every element, the localization API surface
  (`setLyraLocale`/`getLyraLocale`/`resolveLyraString`/`LYRA_DEFAULT_STRINGS` and its 996 message
  keys), the full design-token catalog, framework integration (React/Vue/Angular/Svelte property and
  event binding), TypeScript usage (the 127 `Lyra*EventMap` types, the typed `addEventListener`,
  `HTMLElementTagNameMap`), SSR/declarative-shadow-DOM status, the component-to-peer-dependency table,
  editor tooling metadata, and `<lr-map>`'s OpenStreetMap demo-tile-server production hazard.

  The freshness check now covers events, slots, CSS parts and themeable custom properties in addition
  to properties — it previously only checked properties, which is how 87 public names came to be
  documented nowhere.

### Patch Changes

- 7bdefd2: `lr-time-input` now accepts `min`/`max` as attributes. It inherits both from `lr-input`, where they
  are declared `type: Number` for the `type="number"` contract, so a `min="09:00"` attribute parsed to
  `NaN` and reached the native `<input type="time">` as the literal string `"NaN"` — which the browser
  discards, silently dropping the bound. Only a direct property assignment worked, and it needed a
  TypeScript widening cast to do so.

  `LyraTimeInput` now redeclares `min`/`max` with a converter that forwards the attribute verbatim, so
  `<lr-time-input min="09:00" max="17:00">` reaches the native input intact and its own constraint
  validation reports `rangeUnderflow`/`rangeOverflow` as it should. Seconds-precision bounds
  (`min="09:00:30"` alongside `step="1"`) work the same way, removing the attribute clears the bound,
  and both are typed `string | number | undefined` so an assignment no longer needs a cast.

  `<lr-input type="number">` is unchanged: its `min`/`max` attributes still parse to numbers.

- 2724dec: Add an internal `resolveCssLength()` helper that resolves a CSS length (a bare/`px` number, `rem`,
  or `em`) to pixels, reading the document root font size at call time so a `rem`-authored threshold
  tracks browser zoom, a user font-size preference, or an app changing its base size. Units that only
  make sense against a different reference box (`%`, `vw`/`vh`, `ch`), absolute units, and
  `calc()`/`var()` expressions resolve to `undefined`, which callers treat as "unset".

  No public API change yet — this is the shared groundwork for letting `lr-split` and `lr-stepper`
  accept `orientation-breakpoint` as a CSS length.

- 356f5fb: `lr-emoji-picker` now resolves its three geometry custom properties to real pixels before using
  them for the windowed layout. `--lr-emoji-picker-item-size`, `--lr-emoji-picker-gap`, and
  `--lr-emoji-picker-row-height` were read with `parseFloat(getComputedStyle(host).getPropertyValue(
token))`, which hands back the property's computed _token stream_ rather than a length: the shipped
  `2.5rem` item size was used as `2.5px`, the `0.125rem` gap as `0.125px`, and the `calc()`-based
  default row height was unparseable and always fell back to a hardcoded `64`. The windowed grid
  therefore packed its column cap of 20 emoji into a row that could only paint five, and scrolled at a
  row pitch that did not match the painted rows.

  Each token is now assigned to an off-flow probe box in the shadow root and read back as that box's
  used inline size, so the browser performs the unit math — `rem`, `em`, `ch`, `%`, `calc()`, any CSS
  length resolves correctly, and the item-size probe carries the same `--lr-icon-button-size` minimum
  the emoji buttons do, so the measured item size is the painted one. The result is cached and
  re-derived only when it can actually change: the probe boxes are themselves observed, so a token
  override applied after the first render, a theme swap, or a root/host font-size change updates the
  geometry without any per-frame measurement. Numeric fallbacks still cover the case where no box has
  been laid out yet.

  Consumers no longer have to express these tokens in `px` for the windowed geometry to line up with
  what is painted.

- 4ddf1fb: Document `orientationBreakpointBasis` on `<lr-split>` and `<lr-stepper>`, and the four
  role-scoped bubble custom properties on `<lr-chat-message>`. Also corrects a claim that
  a `rem` inside a CSS `@media` query resolves against the document root's computed font
  size — it resolves against the browser's _initial_ font size, which is exactly why the
  `'viewport'` basis, and not `'container'`, is the one that matches a CSS `@media` rule.
- 2a0cb74: Add an internal `OrientationBreakpointController` that owns orientation-breakpoint
  resolution, basis selection, and media-query lifecycle for the layout components. No
  consumer-visible change on its own.
- 8057596: Fix `<lr-pdf-viewer>`'s `search()` throwing an uncaught `IndexSizeError` when a search term occurs
  more than once inside a single PDF.js text-layer node (e.g. a repeated substring within one text
  item's `<span>`). `paintSearchMatches()` computed every match's DOM range against a pristine,
  pre-painting snapshot of each text node, but wrapping the first match with `Range.surroundContents()`
  splits/shrinks that node out from under the second match's precomputed offset, so `setStart()`/
  `setEnd()` threw before the existing `surroundContents()` try/catch ever ran. Offsets for a node are
  now tracked against the node as it actually stands after each prior match is painted, so every
  repeated occurrence within one text-layer node is now correctly highlighted instead of crashing
  `search()`.
- 415e61f: `<lr-code-editor>`: make `--lr-code-editor-tab-size` actually themeable.

  The stylesheet read the token on the `textarea` part, but `render()` also wrote an inline
  `tab-size:${tabSize}` on that same element on every update, and an inline declaration always beats a
  rule — so the documented token was inert and a host-level override was silently ignored.

  `render()` now writes the token itself, and only when `tabSize` was explicitly assigned. The
  resulting precedence, highest first: an explicitly set `tabSize` (property or `tab-size` attribute)

  > a host-level `--lr-code-editor-tab-size` > the `:host` default of `2`. `tabSize` therefore remains
  > the primary knob and still wins wherever it is used; it just stops shadowing the token while it sits
  > at its default. The Tab key follows the same order, so the indent unit and the rendered tab stops
  > cannot disagree — except for a length-valued token (`40px`, `2ch`, …), which stays a purely visual
  > tab-stop metric and leaves the inserted-space count at `tabSize`.

## 5.0.0

### Major Changes

- 3abb16e: Reorganized `packages/lyra-ui/src/components/` into 11 named family subfolders (Conversation &
  Chat, Agent Tools & Observability, Retrieval & Knowledge Graphs, Forms & Inputs, Data Display,
  Charts, Layout & Navigation, Overlays & Feedback, Document & File Viewers, Media & Files, Utility)
  instead of a flat 212-directory list.

  **Breaking:** any consumer importing an individual component's granular subpath (e.g.
  `@aceshooting/lyra-ui/components/combobox/combobox.js`) must add that component's family segment
  (`@aceshooting/lyra-ui/components/forms/combobox/combobox.js`). The root entry
  (`@aceshooting/lyra-ui`) and the `@aceshooting/lyra-ui/components/*` wildcard export are
  unaffected for consumers who only import the root barrel. See
  `packages/lyra-ui/scripts/component-families.json` for the full directory-to-family mapping.

### Minor Changes

- 0ed6e71: Added a frame-coalesced `lr-viewport-change` event to `lr-graph`, firing at most once per
  animation frame for every source that can move a rendered node's screen position (pan/zoom, a
  `focusNode()`/`fit()` tween, or a simulation tick) so a consumer anchoring its own UI to a node's
  `getBoundingClientRect()` no longer needs to poll its own `requestAnimationFrame` loop.
  `--lr-graph-dimmed-opacity` now defaults to `0.35` (previously the inert `1`), so `dimmedNodeIds`/
  `dimmedLinkIds` are visible out of the box with no host styling required.

  `lr-knowledge-graph-explorer` now computes and forwards `dimmedLinkIds` alongside
  `dimmedNodeIds`, switched its details-popover pan/zoom tracking from RAF polling to the new
  `lr-viewport-change` event, and added a `highlight: 'selection' | 'hover' | 'none' = 'selection'`
  property: `'hover'` also dims by the currently pointer-hovered node's neighborhood, `'none'` opts
  a host out of this component's own dimming entirely.

- bd501b7: Added `defaultSizes` to `lr-split` for an initialization-only fallback (a valid restored
  `storageKey` layout still wins, then `defaultSizes`, then equal distribution) that's never
  overwritten by a later reactive parent render. Added an opt-in `orientationBreakpoint`/
  `narrowOrientation` responsive-axis contract (mirrored below by `lr-stepper`): below the
  component's own measured inline size, `narrowOrientation` becomes the effective resize axis
  instead of the authored `orientation`, exposed via `effectiveOrientation`, a
  `data-effective-orientation` attribute, and `lr-split-orientation-change`. Extended
  `panelConstraints` with `minPercent`/`maxPercent`, combining with `minPx`/`maxPx` on the same side
  via the stricter bound.
- 5319ed6: Added an opt-in `orientationBreakpoint`/`narrowOrientation` responsive-axis contract to
  `lr-stepper`, mirroring `lr-split`'s identically-named properties: below the stepper's own
  measured inline size, `narrowOrientation` becomes the effective layout/navigation axis instead of
  the authored `orientation`, exposed via `effectiveOrientation`, a `data-effective-orientation`
  attribute, and `lr-stepper-orientation-change`. Unset (the default), behavior is unchanged.

## 4.2.0

### Minor Changes

- f3ae130: Adds an `@aceshooting/lyra-ui/ai` entrypoint re-exporting the provider-neutral AI/agent data
  contracts from `src/ai/types.ts` (also re-exported as types from the root `lyra.ts` barrel), so
  consumers importing these shared types don't have to reach into `./ai/types` directly.
- 46eb4d2: `<lr-diff-view>` gains a `contextLines` property: collapses a run of unchanged lines longer than
  `2 * contextLines` behind a single localized fold marker reporting how many lines it hides, keeping
  only `contextLines` lines of context immediately before/after each change (leading/trailing runs
  keep only their nearest `contextLines` lines) — the same context-window convention `git diff -U<n>`
  uses. Default `undefined` renders every line unconditionally, exactly as before this property
  existed. Works identically in both `unified` and `split` layout.

### Patch Changes

- fffd101: `<lr-chart>` no longer tracks its resolved Chart.js draw-time chart-area geometry as a reactive
  `@state()` field — recording it during Chart.js's own draw pass could trigger a second synchronous
  Lit update mid-draw. It's now a plain private field with a microtask-coalesced `requestUpdate()`,
  so repeated geometry updates within the same draw pass collapse into a single re-render.
- 273d5da: Fixed `lr-csv-viewer` and `lr-spreadsheet-viewer`: data rows rendered as unstyled stacked text
  instead of a proper grid, because their styling lived in a `[part='data-row']`/`[part='cell']`
  CSS selector scoped to the wrong shadow root (data rows render inside the nested
  `<lr-virtual-list>`'s own shadow tree via its `renderItem` callback, not the viewer's own). Only
  the header row, rendered directly by the viewer, was ever actually styled. Fixed with
  `lr-virtual-list::part(data-row)`/`::part(cell)` rules that correctly reach across that shadow
  boundary.
- a15cb97: `<lr-notebook-viewer>` now interprets ANSI SGR color/style escape codes embedded in stream and error
  outputs (common in colorized Python tracebacks and console output), rendering them as styled spans
  via the same shared `internal/ansi.ts` parser `<lr-terminal>` uses, instead of showing the raw
  escape sequences as literal text.
- ef988d8: `<lr-trace-tree>` now syncs `focusedId` from `activeSpanId` in `willUpdate()` instead of `updated()`,
  so the roving-tabindex target updates before render rather than one tick after it.

## 4.1.0

### Minor Changes

- b28758d: New `<lr-agent-run>` component: the top-level shell for one `AgentRun` (from `@aceshooting/lyra-ui/ai/types`) -- lifecycle-status badge, elapsed time, current step, model/cost summary, and built-in Cancel/Retry controls in a header, plus four named composition slots (`tasks`/`tools`/`reasoning`/`output`) for the run's actual content. Composes `lr-generation-status` for the live elapsed-time ticker while a run is in progress, `lr-usage-badge` for the cost summary, `lr-task-list` for the `tasks` slot's default content (mapped from `run.steps`), and `lr-badge`/`lr-empty` for the status pill and empty state -- no new step-rendering logic. Emits `lr-cancel`/`lr-retry` (`CancelEventDetail`/`RetryEventDetail`) rather than cancelling or retrying anything itself.
- f33364d: New `<lr-agent-trace>` component: a provider-neutral agent/LLM trace view combining a span-kind
  filter row, a handoff quick-jump list, and the full trace hierarchy, all driven by one shared
  `LyraSpan[]` array. All trace rendering -- hierarchy, expand/collapse, keyboard navigation,
  duration bars, empty state -- is entirely `<lr-trace-tree>`'s own; this component only ever hands
  it a (possibly filtered) `spans` array plus pass-through properties, never building its own row
  markup. The filter row composes `<lr-graph-legend>` (the same type/visibility-toggle legend
  pattern already established for `<lr-graph>` node types, reused here for `LyraSpan.kind`
  visibility) and the handoff list composes `<lr-handoff-divider>` for each visible `'agent'`-kind
  span. Selection (`activeSpanId`) is controlled end-to-end for deep-linking: both a tree-row click
  and a handoff quick-jump activation update it and fire the identical `lr-span-select` `{ id }`
  shape.
- eb3e833: New provider-neutral shared type surface at `@aceshooting/lyra-ui/ai/types`: `AgentStatus`,
  `AgentRun`, `AgentStep`, `ChatMessage`, `ToolInvocation`, `RetrievalQuery`, `RetrievalChunk`,
  `Citation`, `DocumentRef`, `GroundingAssessment`, and shared run-lifecycle/retrieval-progress/
  citation-select/tool-approval/cancel/retry/export event-detail types. A foundational types-only
  module (no runtime code, no new custom elements) for the upcoming retrieval, agent-run,
  knowledge-graph, dashboard, and evaluation component families -- structurally compatible with the
  prop shapes `lr-chat-message`, `lr-citation-badge`, `lr-tool-call-chip`, `lr-tool-result-view`,
  `lr-source-card`, `lr-attachment-chip`, and `lr-document-preview` already expose, so these types
  assign directly onto those components' own properties with no adapters. `ToolInvocation.status`
  reuses `lr-tool-call-chip`'s own `ToolCallStatus` union rather than the broader `AgentStatus`
  shape, since a single tool call's terminal state is exactly what that existing vocabulary already
  covers.
- c2d8f05: `lyra-chat-message` gains a `failure` slot, only ever rendered while `status="failed"`. Left empty
  (the default), today's built-in failed-state UI is unchanged: the `[part="status-text"]` message,
  the `[part="retry-button"]`, and the `chatFailedAnnounce` live-region announcement all keep working
  exactly as before. Once the slot has assigned content, that built-in status text and retry button
  are suppressed -- the host is now fully responsible for its own failure presentation (e.g. a
  prominent, translated `role="alert"` banner with its own retry control), and the built-in
  live-region announcement is suppressed too, so a host's own alert content doesn't get double
  announced alongside a generic built-in one. The `failure` slot itself contributes no box
  (`display: contents`), so the host's content lays out exactly as authored without needing any
  `::part(failure)` override. Content assigned to it should carry `role="alert"` itself when it
  represents an actionable send failure -- this component has no way to add that role on the host's
  behalf. Programmatic focus is rescued to `[part="bubble"]` (mirroring the existing built-in retry
  button's own focus rescue) whenever the failure slot's content held focus and `status` changes away
  from `"failed"`, so a host's own retry control clearing the failed state never silently drops focus
  to `document.body`. The existing `lr-retry` event contract is untouched; a host's own retry control
  can dispatch it manually to stay consistent with listeners elsewhere in a conversation surface, but
  nothing requires it to.
- 3f35f20: New `<lr-context-inspector>` component: an inspection view of the exact context assembled for a
  model call. Renders per-segment token estimates through an embedded `<lr-context-meter>`, source
  attribution through `<lr-citation-badge>`, and copy/export affordances through
  `<lr-copy-button>`/`<lr-export-button>` -- composing all four rather than re-implementing any of
  their rendering. Adds two small, purpose-built presentational features no existing primitive
  covers: a truncation-boundary marker for a segment cut short of its original content
  (`ContextInspectorSegment.truncated`/`omittedTokens`), and titled `<mark part="redaction">`
  highlighting for character ranges a segment's `text` already carries a redaction placeholder in
  (`ContextInspectorSegment.redactions`) -- this component never receives or renders unredacted
  content, only marks where a host-side redaction already happened. Pure projection: never fetches,
  estimates tokens, or performs redaction itself.
- 607b832: Add `<lr-dashboard-grid>`, a responsive, keyboard-accessible widget-layout shell: a controlled
  `layout: DashboardCell[]` (grid-unit `x`/`y`/`w`/`h` + a widget descriptor) drives a CSS Grid,
  composing `<lr-widget>`/`<lr-widget-renderer>` for each cell's default content unless a
  light-DOM `[cell-id]` child is authored instead. Pointer drag/resize and Ctrl/Cmd+Arrow (move) /
  Ctrl/Cmd+Shift+Arrow (resize) keyboard equivalents both route through the same `collision`-policy
  resolution (`'reject'` the default, `'push'`, or `'overlap'`), emitting `lr-cell-move`/
  `lr-cell-resize`/`lr-collision`/`lr-layout-change` -- the component never mutates `layout` itself
  nor touches `localStorage`/network; the host applies (or ignores) every event and owns persistence
  entirely, matching `lr-flow-canvas`/`lr-table`'s own controlled-component convention. Below a
  ~40rem container allocation (`@container`, not the viewport), cells stack into a single flowing
  column instead of shrinking columns unreadably.
- 21de4b4: `lyra-date-input` gains a `size: '2xs' | 'xs' | 's' | 'm' | 'l' | 'xl' = 'm'` property, matching
  `lyra-input`/`lyra-select`/`lyra-combobox`'s shared control-size scale. `size="s"` now renders the
  field at the same height/density as `lyra-input size="s"`, so a date field can sit flush beside a
  compact input or select in the same form row or toolbar. The calendar-toggle and clear buttons
  keep their existing minimum touch target at every tier instead of shrinking below it. The default
  `m` tier is pixel-identical to this component's previous, only rendering.
- d44e979: New `<lr-document-compare>` component: side-by-side or inline comparison of two document versions,
  composed entirely from existing primitives -- `<lr-diff-view>` (the real two-string line diff,
  `view="diff"`, the default) and `<lr-document-preview>` (each version's own actual rendered
  content, `view="side-by-side"`). The side-by-side panes are independently scrollable, so this
  component adds two minimal, purpose-built coordination mechanisms scoped narrowly to that: a
  proportional scroll-position sync (`syncScroll`, default `true`, mirroring `<lr-compare-panel>`'s
  own proven algorithm) and highlight-anchor sync (activating a region highlight shared by `id`
  across both versions' `highlights` scrolls the other pane to its own matching highlight). A shared
  `anchor` property (matching `<lr-document-viewer>`'s own) drives both panes to the same target at
  once. New `DocumentCompareVersion` type extends the shared `DocumentRef` (from `@aceshooting/lyra-ui/ai/types`)
  with `text` (diffed directly, no fetch) and per-version `highlights`.
- 7dfbed7: New `<lr-document-library>`: a searchable, filterable document inventory with versions, tags,
  owners, freshness, and bulk selection. Consumes the shared `DocumentRef` type from
  `@aceshooting/lyra-ui/ai/types` (`id`/`name`/`mimeType`/`uri`/`version`) as its base row shape,
  extended locally (`LibraryDocument`) with `tags`/`owner`/`updatedAt`/`freshness` -- the fields an
  inventory view needs that a provider-neutral document reference deliberately doesn't carry.
  Composes `<lr-table>` for the grid itself (`<lr-data-grid>` was evaluated and ruled out: it only
  supports a single `selectedKey` and stringifies every cell value, so it cannot host the checkbox/
  chip/icon content bulk selection, tags, and per-row type icons need; `<lr-table>` supports
  arbitrary cell content and `priority`-driven responsive column hiding), `<lr-chip>`/`<lr-chip-
group>` for tags and the freshness badge, `<lr-file-icon>` for per-document type icons, `<lr-
input type="search">` for free-text search, and `<lr-combobox multiple>` for a tag facet filter
  (AND semantics -- a document must carry every selected tag). Bulk selection renders a `<lr-
checkbox>` per row plus a header select-all checkbox (indeterminate when some but not all visible
  rows are selected) independently of `<lr-table>`'s own built-in `selectionMode`, since that mode's
  click-anywhere-on-the-row toggle would conflict with the row's own name button opening the
  document. A controlled data view like this package's other orchestration-level list surfaces: no
  upload/sync/mutation logic of its own, only `lr-filter-change` / `lr-sort` / `lr-selection-change`
  / `lr-open` request-and-notification events. `selectedIds` referencing a document no longer present
  in `documents` is silently pruned (no event fires for that pruning, mirroring `<lr-chip-group>`'s
  identical silent-resync convention) rather than left dangling.
- a012673: New `<lr-drilldown-panel>` component: controlled navigation from a chart/table datum to its related
  evidence, documents, entities, or agent runs. A navigation shell only -- an `lr-breadcrumb` trail
  over a host-owned `path`, plus, for whichever categories the current node actually has content for,
  the one existing primitive that already renders that content type (`lr-source-card` for evidence,
  `lr-document-preview` for documents, `lr-entity-card` for entities), wrapped in an `lr-tabs` strip
  only when more than one category has content. Agent-run content has no dedicated rendering primitive
  yet in this library, so it composes via a `runs` slot instead of inventing bespoke rendering.
  Activating a non-current breadcrumb step fires `lr-drilldown-navigate` (`detail: { id, index }`) --
  the component never mutates `path` itself.
- a0bb717: New `<lr-entity-dossier>` component: a full knowledge-graph entity detail surface combining
  properties, relationships, supporting chunks, confidence, and provenance into one composed
  layout. A persistent header renders `<lr-entity-card>` (the entity's summary/properties) next to
  an optional confidence `<lr-stat>`, above an `<lr-tabs>` strip for Relationships
  (`<lr-neighbor-list>`), Supporting chunks (`<lr-chunk-inspector>`), and Provenance
  (`<lr-provenance-panel>`). Pure layout -- it never fetches, ranks, or mutates graph/document state,
  and never re-renders what any of those five composed components already render themselves; every
  one of their own events bubbles through unmodified rather than being re-declared as this
  component's own. Tab labels reuse the exact `localize()` keys the composed child underneath
  already uses for its own accessible name (`neighborListLabel`, `chunkInspectorLabel`,
  `provenancePanelLabel`), so no new localization keys were needed and a translated locale only has
  to cover each string once.
- 5f2927f: New `<lr-eval-dataset>` component: dataset management for an evaluation suite -- a filterable,
  taggable list of `EvalExample` rows (`id`, `input`, `expectedOutput?`, `tags?`, `metadata?`) with
  add/remove/import/export affordances. Fully controlled, matching this library's established
  convention for the rest of the agentic-AI orchestration layer: `examples` is the host's own data,
  and the component never mutates it or performs any I/O itself -- every action fires an
  `lr-example-select` / `lr-example-add-request` / `lr-example-remove-request` /
  `lr-import-request` / `lr-export-request` event and the host decides how to act on it.

  Composes `<lr-data-grid>` for the row list, `<lr-chip>`/`<lr-chip-group>` as a toggleable,
  OR-matched tag-browse filter, `<lr-file-input>` for the import affordance, and
  `<lr-export-button>` for the export affordance (its own built-in client-side download is
  suppressed so every configured format routes through `lr-export-request` uniformly). Ships with a
  searchable free-text filter, RTL and 320px-allocation coverage, and localized strings with
  `.strings` override support.

- f5870ef: New `<lr-eval-result>`: rubric scoring, human review, and comparison across a single evaluation
  example's runs (one per model or prompt version), LangSmith/Arize-eval-result style. Composes
  three existing primitives directly rather than re-deriving their behavior: `<lr-data-grid>` renders
  the `runs` comparison table (`columns` is a plain pass-through to its own `DataGridColumn[]`
  shape); `<lr-rubric-form>` is the human-review scoring surface for whichever run is selected,
  reading/writing that run's own `review` value and re-emitting its
  `lr-input`/`lr-validity-change`/`lr-submit`/`lr-skip` events as
  `lr-review-input`/`lr-review-validity-change`/`lr-review-submit`/`lr-review-skip` with the run id
  attached; `<lr-diff-view>` compares the selected run's output against `baselineRunId`'s output --
  `layout="split"` once they resolve to two distinct runs, `layout="unified"` (an all-equal diff,
  i.e. a plain read of the one run's output) once they resolve to the same run or no baseline
  resolves at all. `selectedRunId`/`baselineRunId` are both fully controlled (never mutated
  internally) and fall back to `runs[0]?.id` purely for rendering when unset, so the component
  renders something useful with zero configuration beyond `runs`; a `selectedRunId`/`baselineRunId`
  that matches no entry in `runs` degrades gracefully (the comparison grid still renders, the
  review/diff sections simply don't).
- 3827a19: New `<lr-evaluation-run>` component: an evaluation batch's live progress. An overall
  `<lr-progress-bar>` counts terminal (done/error/cancelled) examples against the batch's `total`
  (or `examples.length` when unset), with running/failed count badges alongside it. Each example
  renders as its own `<lr-details>` disclosure showing input/output via `<lr-markdown>` or
  `<lr-code-block>` (per `inputFormat`/`outputFormat`), a composed `<lr-grounding-summary>` when the
  example carries a `GroundingAssessment` (plus optional evidence `citations`), and a composed
  `<lr-tool-timeline>` when it carries `toolTrace` entries -- this component defines no grounding-
  scoring or tool-call rendering of its own. `status` reuses the shared `AgentStatus` contract from
  `@aceshooting/lyra-ui/ai/types`, the same run-lifecycle vocabulary an agent step already uses.
  Nested `<lr-grounding-summary>`/`<lr-tool-timeline>` selection and approval events are intercepted
  and re-emitted as this component's own `lr-example-citation-select`/
  `lr-example-tool-approval-decide`, correlated with the originating example's `id` so a host never
  needs to walk the DOM to find out which example a nested interaction came from. Per-example
  disclosure toggling fires `lr-example-toggle`. A live region announces per-example status
  transitions (started/completed/failed/cancelled/needs input/needs approval), gated so a freshly-
  mounted run never announces its initial statuses.
- 0669f01: New `<lr-filter-bar>` component: a row of composable dashboard filters, each declared by the host
  (`filters: FilterBarFilterDefinition[]`) rather than invented by this component -- every filter
  renders an existing Lyra input (`<lr-select>`/`<lr-combobox>` for closed choice sets,
  `<lr-date-input>` in single or `mode="range"` for dates), plus a `<lr-chip-group>` of removable
  `<lr-chip>`s summarizing the active filters, an `<lr-button>` reset action, and (while `loading`)
  an `<lr-spinner>` status indicator. Controlled, like every other Lyra data component: `value` is a
  plain, JSON-serializable `FilterBarValue` object the host reads/writes directly -- this component
  never touches `location`/`history`/storage itself, so turning `value` into (and back out of) a URL
  querystring or app-state store is entirely the host's own concern. `required`-flagged filters get
  live `invalidFilterIds`/`checkValidity()`/`reportValidity()` and a `lr-validity-change` event, with
  each filter's own inline error rendered by its already-chromed composed control rather than a
  second, duplicate label/hint/error frame. `reset()` restores every filter to its own
  `defaultValue` (or unset) and emits both the standard `lr-input` and a dedicated `lr-reset`,
  mirroring `<lr-combobox>`'s own `clear()`/`lr-clear` pattern.
- c5a4786: New `<lr-graph-query-builder>` component: an editor for a single typed relationship/path filter
  (`GraphQuery`) over a knowledge graph -- start/end entity anchors, relationship-type and
  node-type "add" pickers (`<lr-select>`) with a removable active-filter chip display
  (`<lr-chip>`/`<lr-chip-group>`), a traversal direction, a min/max hop range, validation
  (`value`/`checkValidity()`/`reportValidity()`/`lr-validity-change`, form-associated via
  `ElementInternals` the same way `<lr-rubric-form>`/`<lr-tool-param-form>` are), and a
  host-persisted saved-query list (`savedQueries` + `lr-query-save`/`lr-query-load`/
  `lr-query-delete`). `GraphQuery` is a serializable, provider-neutral query model suitable for
  handing straight to a GraphRAG retrieval/traversal backend via `value` or the `lr-query-run`
  event's payload.
- a5723c3: New `<lr-grounding-summary>` component: the claim-level scorecard for one generated answer,
  consuming `GroundingAssessment` from `@aceshooting/lyra-ui`'s `src/ai/types.ts` directly as its
  `assessment` property. Composes `lr-stat` for the supported/unsupported claim counts, citation
  coverage, and optional confidence numeric displays (tone-mapped via a `thresholds` property), and
  `lr-citation-badge` for an optional `citations` list linking each evidence entry back to its exact
  `span`. Activating a citation badge emits `lr-citation-select` (detail: `{ citation }`, the
  `CitationSelectEventDetail` shape from `src/ai/types.ts`) carrying the full citation record, in
  addition to the badge's own `lr-citation-activate` still bubbling through unmodified. Warnings
  render verbatim as caller-supplied data; every other label is localized via `this.localize()`.
- 497c8d3: New `<lr-ingestion-queue>` component: a controlled list of documents moving through an ingestion
  pipeline (`queued` → `uploading` → `extracting` → `chunking` → `embedding` → `indexing`, plus the
  terminal `done`/`failed`/`cancelled` stages), each row composing `lr-badge` for its stage label,
  `lr-progress-bar` for in-flight progress, and chunk-count/embedding-status/attempt-count text.
  `lr-empty` renders the zero-items state. Presentation only -- this component runs no ingestion
  itself and never mutates `items`; retrying a `failed` row or cancelling any non-terminal row fires
  a controlled `lr-retry`/`lr-cancel` request event (`detail` extends the shared `RetryEventDetail`/
  `CancelEventDetail` from `src/ai/types.ts` with the `itemId` identifying which row) and waits for
  the host to supply an updated `items` array, the same request/response convention
  `<lr-thread-list>`'s row-action events already establish. At or above `virtualizeThreshold` items
  the list renders through an internal `<lr-virtual-list>` instead of a plain keyed list, matching
  `<lr-thread-list>`'s data mode and `<lr-activity-feed>`'s own `virtualizeThreshold` precedent.
- 593e879: New `<lr-knowledge-base>` component: a knowledge-base source list showing sync status, indexing
  health, and permissions per source, plus an aggregate summary row. A controlled data view -- it
  never syncs or indexes anything itself, only presents `sources: KnowledgeSource[]` and emits
  request-only `lr-kb-create`/`lr-kb-sync`/`lr-kb-pause`/`lr-kb-delete` events for the host to act on
  and reflect back into a new `sources` value, mirroring `lr-thread-list`'s `lr-thread-pin`/
  `-archive`/`-delete` convention. Composes `lr-table` for the source list (its own interactive-cell
  click guarding keeps the per-row `lr-menu` from misfiring row activation), `lr-badge` for the
  sync-status/indexing-health/permission indicators, `lr-stat` for the aggregate summary, and
  `lr-menu` for the per-row Sync now/Pause sync/Delete source actions.
- f04b670: Add `<lr-knowledge-graph-explorer>`, an orchestration-level knowledge-graph surface composing the
  existing `lr-graph` canvas with entity search, type filters (via `lr-graph-legend`), neighborhood
  expansion, pinned nodes, path finding between pins (via `lr-path-strip`), node selection, and a
  node-details popover (via `lr-popover.showAt()` and `lr-entity-card`/`lr-neighbor-list`). Composes
  existing primitives rather than re-implementing graph rendering. New events `lr-path-request` and
  `lr-pin-change`; every composed primitive's own event (`lr-node-click`, `lr-node-expand`,
  `lr-selection-change`, `lr-community-click`, `lr-relation-activate`, etc.) bubbles straight through
  unmodified.
- 593e879: New `<lr-memory-panel>` component: an agent's working memory surface -- short-term context and
  long-term memories, each item's confidence and optional grounding provenance, and add/remove/forget
  actions gated behind an explicit confirmation step. Composes `<lr-provenance-panel>` for a per-item
  provenance breakdown (behind a disclosure toggle, only rendered when an item defines one) and
  `<lr-confirm-bar>` for every add/remove/forget confirmation, reusing this repo's existing inline
  confirmation pattern rather than inventing a new one. A memory item's confidence reuses
  `<lr-citation-badge>`'s own confidence vocabulary, tiered against `thresholds` the same way
  `<lr-chunk-inspector>` tiers a chunk's relevance score. `shortTerm`/`longTerm` are controlled and
  never mutated by the component -- approving a pending action only fires the matching `lr-add` /
  `lr-remove` / `lr-forget` event; the host applies the resulting state change.
- ac4857d: Add `showAt(rect, options?)` to `<lr-popover>` and `<lr-tooltip>`, a virtual-anchor API that opens
  the overlay positioned against an arbitrary rectangle (`{ x, y, width?, height? }`, defaulting to a
  zero-size point) instead of the slotted `trigger`. This lets a canvas/SVG surface -- a `<lr-graph>`
  node, a chart datum, a text-selection range -- get flip/shift/RTL-aware positioning, Escape,
  light-dismiss, and (optional, via `options.returnFocusTo`) focus-return for free, without a
  consumer hand-rolling absolute positioning and dismissal logic around it. Both components remain
  fully backward compatible: a component that never calls `showAt()` behaves byte-identical to
  before. `place()` (`src/internal/positioner.ts`) is widened from `HTMLElement` to `Element |
VirtualAnchor` to support this, with no behavior change for existing real-element anchors.
- 823f19b: New `<lr-policy-summary>` component: a read-only list of guardrail, permission, privacy, and
  tool-policy decisions, each carrying an `allow` / `deny` / `needs-review` state and an
  always-visible, accessible explanation of why that decision was made -- never conveyed by color
  alone. Composes `<lr-badge>` for the compact per-decision state indicator and `<lr-callout
inline>` for the explanation text, whose own `role="alert"`/`role="status"` semantics already
  carry the right urgency per state, plus `<lr-details>` for a decision's optional richer `detail`
  (matched rule text, policy id, cited evidence) behind progressive disclosure. `decisions` is
  controlled and never mutated by the component -- this is a summary surface, not an approval gate
  (see `<lr-tool-approval-dialog>`/`<lr-confirm-bar>` for that).
- c6dd883: New `<lr-query-builder>` component: a composable structured-query builder for tabular/dashboard
  data queries -- a flat list of field/operator/value condition rows combined with one AND/OR
  combinator. Distinct from `<lr-graph-query-builder>`, which builds typed relationship/path
  queries over a knowledge graph -- a genuinely different data model that never shares a file or a
  value type with this one.

  Fully controlled: a host supplies `fields` (available columns, each carrying a
  `QueryBuilderFieldType` of `string` / `number` / `boolean` / `date` / `enum` that determines its
  offered operators and value control) and a plain, serializable `value: { combinator, conditions }`
  object, the same controlled-plain-object-`value` convention as `<lr-rubric-form>`. Each row
  composes `<lr-select>` for the field and operator pickers and a value control chosen from the
  selected field's type: `<lr-input type="text">`, `<lr-input type="number">`, `<lr-select>` with
  True/False options, `<lr-date-input>`, or `<lr-select>`/a multi-select `<lr-combobox>` for `enum`
  fields (`eq`/`neq` vs. `in`/`notIn`). A unary operator (`isEmpty`/`isNotEmpty`) renders no value
  control. `<lr-icon-button icon="trash">` removes a row and `<lr-button>` appends one, both
  surfaced through public `addCondition()`/`removeCondition(id)` methods and `lr-add-condition`/
  `lr-remove-condition`/`lr-input` events -- the component never mutates `fields`/`value` in place
  or touches storage/network itself.

- b443be6: New `<lr-retrieval-results>` component: the orchestration-level ranked-chunk-list surface for
  retrieval/grounding workflows, consuming `RetrievalChunk[]` from `@aceshooting/lyra-ui/ai/types`.
  Composes an internal `<lr-chunk-inspector>` per row (reusing its score bar, tier coloring, title/
  page rendering, expandable text, and `compact` mode verbatim -- no hand-rolled chunk-card markup)
  and an internal `<lr-virtual-list>` for windowing once the result count is large or `grouping` is
  active. Adds deduplication by `id` (keeping the higher-scoring duplicate), optional grouping by
  `source.id` (bucketed, best-scoring group first, same convention `<lr-thread-list>`'s date grouping
  already uses), multi-selection via a per-row `<lr-checkbox>` (`selectedIds` controlled, `lr-select`
  emits the updated ids and matching chunks), pagination/infinite loading (`has-more`/`loading`
  forwarded to the internal `<lr-virtual-list>` while virtualized, or a `[part="load-more"]` button
  otherwise -- both paths emit `lr-load-more`), and a `compact`/`expanded` `presentation` switch.
  `metadata` (arbitrary `Record<string, unknown>`, not rendered by any existing primitive) shows as a
  plain key/value list in `expanded` presentation. A row's `lr-chunk-open` is forwarded verbatim for
  routing into `<lr-document-viewer>`.
- 5f2927f: New `<lr-retrieval-search>` component: the query bar for a retrieval/RAG surface, composing
  `lr-input` (query text), `lr-segmented` (vector/keyword/hybrid mode), `lr-chip`/`lr-chip-group`
  (removable active-filter/scope chips), `lr-spinner` (loading), and `lr-empty` (empty results).
  Fully controlled and network-free -- `query`/`mode`/`filters`/`scope` are host-owned properties,
  and the component only emits `lr-search` (detail: a `RetrievalQuery` from `@aceshooting/lyra-ui`'s
  `src/ai/types.ts`) on Enter or the submit button; the host performs the actual retrieval and
  toggles `loading` around it. Because this component has no way to know when a request resolves,
  submitting again while already `loading` is treated as superseding the in-flight request:
  `lr-cancel` fires immediately before the new `lr-search`, and the submit button itself doubles as
  an explicit Cancel affordance while `loading`. Filter/scope chip removal updates this component's
  own copy first, then emits `lr-filters-change` with the complete next `{ filters, scope }` state,
  mirroring `lr-source-picker`'s existing round-trip convention.
- 3d6479f: New `<lr-retrieval-trace>` component: a retrieval pipeline's stage timeline (query rewriting,
  embedding, retrieval, reranking, filtering), rendered through `<lr-span-waterfall>`'s existing
  time-scaled bar rendering rather than a new timeline widget -- each `RetrievalStage` projects to
  one `LyraSpan`, with `kind` mapped onto whichever existing `LyraSpan['kind']` fits best (`embed`
  -> `'embedding'`, `retrieve` -> `'retriever'`, `query-rewrite` -> `'llm'`, `rerank`/`filter` ->
  `'tool'`). Below the timeline, a disclosure list exposes each stage's expandable evidence panel:
  free-form text, retrieved/reranked/filtered chunks via a compact `<lr-chunk-inspector>` (`chunks`
  accepts `RetrievalChunk` from `@aceshooting/lyra-ui/ai/types` directly), and/or arbitrary stage
  metadata as a key/value list. Controlled `stages`/`activeStageId` properties; emits `lr-stage-select`
  and `lr-stage-toggle`. Never fetches, ranks, or computes retrieval results itself.
- 5597050: `lyra-thread-list` gains a `renderActions?: (thread: ChatThread) => TemplateResult` data-mode
  property, an escape hatch for a fully custom per-row action surface (e.g. a `<lr-menu>` with
  Rename/Delete, a rename dialog, delete-confirmation state) that the existing `rowActions`'s closed
  `pin | archive | delete` set can't express. Its content is appended after any built-in `rowActions`
  buttons in the same row's `actions` slot -- additive, not a replacement, the same composition
  direction `wrapRow` already takes elsewhere on the row. Set `rowActions` to `[]` (its default) to
  use only the callback's content. `renderActions` is re-invoked per row on every render with the
  current thread (never memoized/stale) and its content sits as a DOM sibling of the row's own
  selectable region, so activating a custom action never also fires `lr-select` -- the same
  structural mechanism the built-in row-action buttons already rely on. Leaving `renderActions`
  unset leaves `rowActions`' rendered output byte-for-byte unchanged, and `wrapRow` continues to
  compose independently around the result either way.
- c67e88b: New `<lr-tool-timeline>` component: a chronological list of an agent run's tool calls, rendering
  each entry through `<lr-tool-call-chip>` (name/status/duration) and `<lr-tool-result-view>`
  (args/result) -- both already built for exactly this -- plus one shared `<lr-tool-approval-dialog>`
  for entries gated behind a human decision. Its own job is strictly ordering and layout on top of
  those existing primitives: `entries` (a new `ToolTimelineEntry[]`, extending `ToolInvocation` from
  `@aceshooting/lyra-ui/ai/types` with `startedAt`/`endedAt`, `retryCount`, `redactedFields`,
  `needsApproval`, and `approved`) sorts ascending by `startedAt`, with untimed entries trailing in
  their original relative order; duration is derived from `startedAt`/`endedAt` and handed to the
  chip's own `durationMs`; a retry badge renders only while `retryCount > 0`; and per-entry
  `redactedFields` (dotted paths, or a bare `"args"`/`"result"`/`"error"` for a whole branch) mask
  sensitive values in the read-only detail view with a "Value hidden" placeholder -- the copy of
  `args` handed to the approval dialog is always the real, unredacted value, since approving a call
  requires seeing what will actually run. Activating a pending entry's chip opens the shared dialog;
  approving or denying it emits this component's own `lr-tool-approval-decide`
  (`{ invocationId, approved, args? }`, extending the shared `ToolApprovalEventDetail`) and never
  mutates `entries` itself -- a host applies the decision and re-assigns `entries`, and the dialog
  closes on its own if the entry under review disappears or resolves out from under it in the
  meantime.

## 4.0.0

### Major Changes

- cf2cbbb: Release 4.0.0 renames the public custom-element, event, and design-token prefixes from
  `lyra-*`/`--lyra-*` to `lr-*`/`--lr-*`. The package name, JavaScript `Lyra*` class names,
  and `lyra-ui` repository/package paths remain unchanged. This is a breaking migration:
  update element tags, library-specific event names, and CSS custom-property overrides.

### Minor Changes

- cf2cbbb: New `lr-control-group` primitive: a responsive layout wrapper (`role="group"`, `flex-wrap: wrap`,
  `align-items: center`) for a row of mixed form controls and action buttons — a segmented switcher
  beside a select and an export button, for example. Distinct from `lr-button-group` (which
  stretches uniform-height buttons to a shared row height): `lr-control-group` centers children of
  differing intrinsic heights instead, since it makes no assumption about child type. Gap is
  themeable via `--lr-control-group-gap`.
- aa1fb49: `lr-segmented` gains a `size: '2xs' | 'xs' | 's' | 'm' | 'l' | 'xl' = 'm'` property, matching
  `lr-select`/`lr-combobox`'s compact-form-control scale (`xs`-`xl`) plus `lr-input`'s `2xs`
  tier. `size="s"` now renders at the same control height as `lr-select size="s"`/
  `lr-combobox size="s"`, so a segmented metric switcher can sit flush beside a compact select or
  combobox in the same toolbar without consumer CSS reaching into `::part(base)`. The default `m`
  tier is pixel-identical to this component's previous, only rendering.

### Patch Changes

- 5266832: Fixed `lr-select`'s `size="xs"`/`"s"`/`"l"`/`"xl"` to actually enforce their documented
  per-size minimum trigger height. A `var()` fallback bug meant `--lr-select-trigger-min-height`
  was silently dead code at every size — only padding and font-size ever varied, height did not.
  The default (`m`) tier's rendering is unchanged; a consumer-set `--lr-select-trigger-height`
  override still wins over the per-size floor, as before.

## 3.9.0

### Minor Changes

- abdd967: `lr-activity-feed` gains `renderText?: (entry: ActivityEntry) => TemplateResult`, overriding the
  default plain-text `[part="entry-text"]` rendering with arbitrary rich content — rendered markdown,
  or markdown plus a trailing tool-call chip list — identically whether or not the feed is currently
  virtualized, since both the plain and virtualized paths render every entry through the same
  internal template. Previously `ActivityEntry.text` could only ever render as plain escaped text,
  with no way to attach richer per-entry content.
- b64d4d2: `lr-graph` gains `dimmedNodeIds`/`dimmedLinkIds` (controlled, mirroring
  `selectedNodeIds`/`selectedLinkIds`): a host can now apply a themeable low-opacity treatment to
  arbitrary nodes/links -- e.g. dimming every non-neighbor of a hovered node -- via a new
  `--lr-graph-dimmed-opacity` custom property, in both the `svg` (default) and `canvas` renderers.
  Previously the only way to express this was reaching into the shadow DOM; `1` (no-op) by default,
  so existing usage is unaffected.
- 1d1935e: `lr-input` gains `'search'` as a documented `LyraInputType` member. It already worked at runtime
  via unchecked passthrough to the internal native `<input type="search">` (`type` has no runtime
  validation), but the exported type union didn't include it, so a consumer setting `type="search"`
  got no compile-time confirmation it was supported and no protection against a future stricter-typed
  release silently dropping it.
- f6b4957: New `<lr-markdown-core>` entry point: a build-lean variant of `<lr-markdown>` for a consumer
  whose `languages` map already covers every language it renders, mirroring the existing
  `<lr-code-block>`/`<lr-code-block-core>` split. Its own module never imports shiki's ~200-
  language default dynamic-import table -- `<lr-markdown>`'s existing `languagesOnly` flag can't
  give a bundler that guarantee, since it's checked at runtime, not statically provable. Every other
  capability (GFM, heading anchors, text-quote highlights, math) is unchanged from `<lr-markdown>`;
  a fenced block whose language isn't in `languages` always renders the plain-text fallback.
- 0a5227e: `lr-thread-list` gains `wrapRow?: (thread: ChatThread, row: TemplateResult) => TemplateResult`
  (data mode only): wraps each row's built-in `lr-conversation-item` with host-supplied content
  that has no home in the item's own `title`/`excerpt`/`meta`/`actions` surface — most notably a
  leading purpose icon, since `lr-conversation-item` has no default slot to receive one at all.
  Previously data mode forced an all-or-nothing choice between its built-in grouping/virtualization
  and a host's need for row content outside that surface, which only slotted mode (no grouping, no
  virtualization) could accommodate.
- d3f2e13: `lr-usage-badge` gains `formatLatency?: (ms: number) => string`, overriding the built-in duration
  algorithm (which has no minutes/hours tier — `'{ms}ms'`, or one-decimal seconds above 1000ms) in
  both the visible strip and the tooltip row. Mirrors `lr-activity-feed`'s `formatTimestamp`
  convention. Previously a consumer whose latencies commonly exceed a minute (e.g. a long-running
  agent run) had no way to render its own duration scale instead of a bare seconds count.

## 3.8.0

### Minor Changes

- c4cb188: Adds `<lr-activity-feed>`: an append-only streaming log of granular agent actions, collapsing to
  a localized "Completed N steps" summary once the run is over. Implements the shared follow
  (stick-to-bottom) contract (`follow` property, `lr-follow-change` event) and virtualizes its body
  through an internal `<lr-virtual-list>` at/above `virtualizeThreshold` entries, using that
  component's `scrollToIndex()` method to drive its stick-to-bottom follow. `<lr-virtual-list>`
  also gains `aria-label` forwarding from the host element onto its internal `role="list"`
  container, usable independently of `<lr-activity-feed>`.
- 5a0276e: Adds an internal, dependency-free ANSI/SGR parser (`src/internal/ansi.ts`, not a public export) —
  shared groundwork for `lr-terminal`'s streamed console-output rendering. No public API surface
  change on its own; ships alongside the `lr-terminal` component in the same release.
- b92b5d4: Adds `<lr-artifact-panel>`: a shell around one generated artifact — title/kind header, a
  preview↔code toggle (rendered only once the `code` slot has content), version navigation with a
  "Restore this version" affordance (`lr-version-change`/`lr-restore`, versions are host state),
  `streaming`/`aria-busy` state, and built-in copy/download actions. Renders none of the artifact
  itself — content is slotted.
- cf005b9: `lr-attachment-trigger` gains an `'audio'` capability, following the existing `camera` capability's
  request-only pattern exactly: activating it fires `lr-audio-request` (no embedded recorder), and the
  host opens its own capture UI — typically `<lr-push-to-talk>` in a `<lr-overlay>`/popover — then
  hands the resulting blob to its attachment tray. Purely additive: the default `capabilities` stays
  `['files']`, and every existing `files`/`image`/`camera` behavior is unchanged.
- b85934b: Adds `<lr-audio-visualizer>`: a presentational, canvas-drawn voice-activity visualization (bars or
  waveform), driven by a `MediaStream` (lazily wired to a WebAudio analyser), a numeric `level`, or a
  `state` (`idle`/`listening`/`thinking`/`speaking`) alone for an ambient animation. Pairs with
  `lr-push-to-talk`'s `stream`/`lr-level` output. Zero dependencies — native Web Audio only,
  reduced-motion-aware.
- 3310f16: Adds `<lr-av-player>`: an audio/video player built on a native media element with a cue transcript
  synced to playback, `time-range` anchor/highlight support, an optional dependency-free waveform
  (peaks-in, no in-component decoding), playback-rate control, and imperative transcript search.
  Self-registers into the document-viewer registry for the common audio/video MIME types. Owns
  recorded-media transcript sync — distinct from `lr-transcript-feed` (live voice-session captions)
  and from `lr-playback` (an index stepper, no media).
- 0fe240b: Adds `<lr-branch-picker>`: a controlled "‹ 2 / 5 ›" navigator across regenerated/edited variants of
  one chat message, mirroring `lr-pagination`'s "never mutates its own state" contract. Fires
  `lr-branch-change` with the requested (always in-bounds) index; the host swaps the displayed branch
  content and applies the new index back. Designed to slot into `lr-message-actions`' default slot or
  directly into `lr-chat-message`'s `actions`/`badges` slots.
- bc75a1f: Adds `<lr-browser-frame>`: a presentational "agent computer" viewport — a safe-URL-gated
  screenshot/frame stream `<img>` (or slotted live media), read-only address bar, visible (never
  color-only) connection status, kind-distinct action-ping overlays, and take-over/stop affordances
  (`lr-take-over`, `lr-stop`). No automation transport and no input relay — take-over is an event;
  the host swaps in its own interactive element.
- e29f575: `lr-button` and `lr-input` gain `size="2xs"`, a sub-`xs` tier for dense, toolbar-embedded controls
  (e.g. a search input and text buttons inside a compact dialog header). Composes with `appearance`/
  `variant` the same way the existing five sizes already do.
- e4762fd: `lr-button` gains `appearance="quiet"`: a bordered, transparent-until-hover tier for a toolbar-style
  icon+label action whose border/text read fixed `--lr-color-border`/`--lr-color-text-quiet` tokens
  regardless of `variant`, unlike `appearance="outlined"`'s variant-tinted text — for a call site that
  needs a genuinely muted resting state rather than a bold bordered button. New
  `--lr-button-quiet-border`/`--lr-button-quiet-text` custom properties back the two tokens.
- 4ac983b: `lr-chat-message` gains `actionsOutsideBubble` (reflects to `actions-outside-bubble`): renders the
  `actions` slot's content as a sibling immediately after the message bubble instead of nested inside its
  footer's own padding/background box. Previously a consumer whose action row (e.g. a hover-reveal copy
  button) had to sit visually outside the bubble's chrome could not adopt this component at all, since
  `::part(footer)` styling alone cannot detach it from the bubble's box.
- 65a1f8c: Adds `<lr-chat-viewport>`: the transcript scroll container for a chat/agent conversation surface —
  owns the stick-to-bottom `follow` state machine (`follow` property, `lr-follow-change` event,
  matching the same shared follow contract `<lr-activity-feed>` already implements) while an answer
  streams, a built-in "jump to latest" pill with a pluralized unread count, and an unread divider. Two
  content shapes are auto-detected: ordinary element children (slotted mode) or exactly one
  `lr-virtual-list` (virtual mode, built on that component's `scrollToIndex()` method). Renders no
  messages and computes no unread state itself — the host supplies `unreadStartIndex` and slots its
  own message elements or a virtual list.
- bf601c8: Adds `<lr-checkpoint>`: an inline conversation restore point — a labeled marker between messages
  whose Restore affordance confirms inline (an accessible-name-carrying button swap, focus-managed,
  Escape/focus-out-aware) before firing a `lr-restore { checkpointId, label }` event. Persists and
  restores nothing itself — host state in, events out. `confirmRestore="false"` skips the inline
  confirm step entirely; `restorable="false"` renders a plain, non-interactive marker for read-only
  views or the currently-restored point.
- 22c1006: Adds `<lr-chunk-inspector>`: a ranked retrieved-chunks "why this answer" panel — relevance score
  bars with tier-mapped tones, expandable chunk text (state keyed by chunk id, survives streaming
  reassignment), and `lr-chunk-open` for landing a chunk in `lr-document-viewer` with its anchor.
  Virtualizes automatically above `virtualizeAt` rows via the existing `lr-virtual-list`.
- c274bd6: `lr-code-block` and `lr-code-block-core` gain `highlight-lines` (declarative `"3-5,7"`-style
  line emphasis), `interactive-lines` (turns the line-number gutter into a keyboard-navigable,
  clickable roving-tabindex group emitting `lr-line-click`), and `line-range` anchor-target support
  (`highlights`, `activeHighlightId`, `scrollToAnchor()`, event `lr-text-select`) — identical on
  both components since they share the new line-addressing logic. Previously there was no way to
  emphasize or deep-link to a specific line/range of lines in a rendered code block.
- f71fcac: Adds `<lr-commit-card>`: a compact commit summary card — abbreviated/copyable hash, subject/body
  message split, author/time meta, a non-color-only aggregate `+N -M` diffstat, and a collapsible
  per-file change list (`lr-file-select` on activation) reusing `lr-file-tree`'s `GitStatus`
  vocabulary and shared `gitStatus*` labels.
- 22c1006: Adds `<lr-community-card>`: a GraphRAG community-report card — label, summary excerpt, member
  count, member chips with a "+N" overflow chip, and a drill-in action (`lr-drill`) surfaced from
  the header, an explicit drill button, and the overflow chip alike. A `compact` mode renders just
  title + member count + drill button for dense listings (e.g. inside `lr-provenance-panel`).
- 1432601: Add `lr-compare-panel`: side-by-side A/B output comparison with a winner vote (LMSYS-arena /
  LangSmith-pairwise style) — two slotted panes (`a`/`b`), an optional shared `prompt` header, a
  `role="group"` vote bar (better-A / better-B / tie / both-bad, the last two individually
  hideable), and optional proportional `syncScroll` between panes. No hotkeys (slotted content may
  contain inputs); casting a vote announces through an internal live region.
- bc75a1f: Adds `<lr-confirm-bar>`: an inline, non-modal approve/deny block for one proposed action — the
  in-flow sibling of `lr-tool-approval-dialog` for confirmations that belong in the transcript instead
  of an overlay. Same `lr-approve`/`lr-deny` event shapes and the same heading/args-label/deny/approve
  localization keys as the dialog, so the two stay in lockstep. No focus trap, scroll lock, or
  Escape/backdrop handling; on activation, focus moves synchronously to the always-present decided-state
  text before the Deny/Approve buttons unmount, and an internal live region announces the outcome.
- 23bfb7b: `lr-conversation-item` gains a `meta` slot (small, non-focusable structured fields below the
  title/excerpt — e.g. a day label, project name, cost) and an `excerpt` slot that wins over the
  existing `excerpt` property whenever it has assigned content, mirroring `lr-timeline-item`'s own
  `timestamp` slot-wins-over-property pattern. Previously a consumer needing a rich excerpt (e.g. a
  search-hit snippet with `<mark>` highlighting) or a multi-field meta line had to flatten that
  structure into the plain-text `excerpt` property or hand-roll the row entirely.
- 2ad038b: `lr-dataset-viewer` now virtualizes through `lr-virtual-list` (a new `item-role="row"` mode,
  mapping to a proper `role="table"`/`role="row"`/`role="rowgroup"` accessibility tree) instead of a
  single synchronous `<table>`, lifting its row cap from 1,000 to the shared 10,000-row default every
  other tabular viewer already uses. It also gains `cell-range` anchor-target support (`highlights`,
  `activeHighlightId`, `scrollToAnchor()`, event `lr-highlight-activate`) and an imperative
  in-document search API (`search()`, `searchNext()`, `searchPrevious()`, `clearSearch()`, event
  `lr-search-change`), sharing the same raw-grid cell addressing as `lr-csv-viewer`, with the
  header row always included since this viewer always parses with PapaParse's `header: true`. The
  `lyra:dataset` document-viewer registration now declares `capabilities: { anchors: ['cell-range'],
search: true, textSelect: false }`. `lr-virtual-list` itself gains the underlying
  `item-role`/`row-index-offset` properties this required, additive and defaulting to today's exact
  `listitem` behavior for every other consumer. Previously a 1,001+ row dataset file failed to load at
  all, and there was no way to highlight or search a cell.
- 2ad038b: `lr-diff-view` gains `layout="split"` (two side-by-side columns derived from the same line-diff
  alignment as the default unified view — unbalanced replace hunks pad the shorter side with empty
  placeholder rows) and optional syntax highlighting via `language`/`languages` (same fine-grained
  shiki-core-only shape as `lr-code-block-core`, so the peer-free default stays truly peer-free).
  Previously diff-view only rendered a single interleaved unified view with no highlighting option.
- dc168c7: `lr-docx-viewer` gains `getHeadingTree()` (a document-ordered heading outline stamped with
  GitHub-slugger-style ids, using the same slugging algorithm as `lr-markdown`), `fragment`/
  `text-quote` anchor-target support (`highlights`, `activeHighlightId`, `scrollToAnchor()`, events
  `lr-highlight-activate`/`lr-text-select`/`lr-anchor-result`), and an imperative in-document
  search API (`search()`, `searchNext()`, `searchPrevious()`, `clearSearch()`, event
  `lr-search-change`). Previously there was no way to deep-link into a section, highlight a quoted
  passage, or search inside a rendered Word document.
- d3edf31: `lr-ebook-viewer` gains `getToc()` (a flat, nested table of contents from the EPUB's own
  navigation document), a `location` property (get/set the current CFI or spine href, with
  `lr-location-change` on user navigation), an imperative in-book search API (`search()`,
  `searchNext()`, `searchPrevious()`, `clearSearch()`, event `lr-search-change`), and `cfi`/
  `text-quote` anchor-target support (`highlights`, `activeHighlightId`, `scrollToAnchor()`, events
  `lr-highlight-activate`/`lr-text-select`). Previously there was no way to read an EPUB's table
  of contents, deep-link into a specific location, or search inside a rendered book.
- 2ad038b: `lr-email-viewer` attachments become interactive: each row is now a real button emitting
  `lr-attachment-open { attachment: { filename, mimeType, content } }` with the attachment's decoded
  bytes attached (the component itself never opens/downloads anything — host-owned routing, e.g. into
  `lr-document-viewer`). A new `fold-quotes` property collapses trailing quoted-reply text/HTML
  (`>`-prefixed text runs, `gmail_quote`/`yahoo_quoted`/Outlook-style HTML blocks) behind a localized
  toggle. Previously attachments were inert metadata with no way to retrieve their content, and quoted
  reply chains always rendered in full.
- ba094cb: Adds `<lr-emoji-picker>`: a searchable, keyboard-navigable, form-associated emoji picker
  (`value`/`lr-change`, matching this library's other form-control conventions). `groups` is fully
  consumer-suppliable — this component ships no emoji data of its own — with an optional convenience
  auto-loader for a default set via the `emoji-picker-element-data` peer when `groups` is left unset.
  Lets a consumer currently wrapping the third-party `emoji-picker-element` custom element (plus its
  locale-data package) as a direct dependency replace it with a first-party `lr-*` component instead.
- 22c1006: Adds `<lr-entity-card>`: a dossier card for one knowledge-graph entity (`LyraEntity`) — type
  badge, description, key/value property rows, relationship-degree and community rows, and a
  built-in "focus in graph" action that emits `lr-entity-activate` for a host to route into
  `lr-graph`'s `focusNode()`.
- 22c1006: Adds `<lr-entity-chip>`: an inline `@entity` mention for agent prose with a hover/focus preview
  popover, reusing `lr-citation-badge`'s interaction contract wholesale (200ms hover-leave grace,
  independent hover/focus hold-open state, Escape dismissal, Space opens/Enter activates). The
  knowledge-graph sibling of `lr-citation-badge` — renders its `label` text rather than a `[n]`
  index, and reflects `type` for host-level per-type theming.
- 2ab49e6: Adds `<lr-env-list>`: a masked key/value list for environment variables and secrets
  (`<dl>`/`<dt>`/`<dd>` semantics), defaulting every entry to masked (a fixed eight-bullet run,
  length-independent so value length is never leaked) with per-row reveal (`lr-reveal-change`, state
  keyed by name and position, and reset for a row whose name shifts position) and copy (`lr-copy`,
  always copies the real value). `revealable=false` for screen-share-safe hosts. Masking is
  presentational, not a security boundary.
- 892c9d3: Adds `<lr-file-tree>`: a file-explorer preset over `lr-tree` + `lr-file-icon` with path-keyed
  nodes, per-file git-status badges and `+N -M` diffstat, lazy directory loading (`setChildren()`,
  `lr-load-children`), `revealPath()`, and `lr-file-select`/`lr-file-open` events (matching the
  "Enter/click on an already-selected file opens it" keyboard parity rule).
- 22c1006: Adds `<lr-flow-canvas>`: a dependency-free, pannable/zoomable DAG workflow canvas — HTML card
  nodes with typed connection handles, SVG Bézier edges with arrowheads and labels, a shared layered
  auto-layout for unpositioned nodes, and controlled selection/drag/connect gestures behind three
  independent opt-in flags (`nodes-draggable`, `connectable`, `droppable`). Readonly viewer by default;
  never mutates `nodes`/`edges` itself. Ships a `registerCompanion()` hook so `lr-flow-minimap`,
  `lr-flow-controls`, and `lr-flow-run-overlay` (following in subsequent releases) can attach
  without reaching into its shadow DOM.
- 22c1006: Adds `<lr-flow-controls>`: the zoom in/out, fit, and interaction-lock button cluster for
  `lr-flow-canvas`, so every flow surface ships the same affordances without hosts rebuilding them.
  Zoom buttons disable at the resolved canvas's `minZoom`/`maxZoom` bounds; the lock toggle stays in
  sync with the canvas's `locked` attribute regardless of what changed it.
- 22c1006: Adds `<lr-flow-minimap>`: a corner overview map for `lr-flow-canvas` — scaled node rectangles
  (status-tinted) plus a draggable, keyboard-operable viewport rectangle for orientation and fast
  navigation on canvases larger than the screen. Attaches via `registerCompanion()`, either slotted
  into one of the canvas's corner slots or externally via `for="canvas-id"`.
- 22c1006: Adds `<lr-flow-node>`: the workflow node card — header/body/toolbar chrome, tool-lifecycle status
  tones with a visible (never color-only) status chip, a determinate progress bar, and named
  connection-handle elements. Used automatically by `lr-flow-canvas` as the default card for any
  node without a slotted override, and usable standalone for palette previews or docs.
- 22c1006: Adds `<lr-flow-run-overlay>`: execution-state presentation for `lr-flow-canvas` — mirrors a
  `FlowRunDecorations` map into the resolved canvas (which owns the actual node/edge paint) and
  renders a compact "{done} of {total} steps complete" summary strip with per-status counts.
  Status transitions announce through a throttled live region. Pure pushed state — no execution,
  polling, or internal clock.
- 2ad038b: Adds an internal `application/geo+json` document-viewer registry bridge (`<lr-geojson-view>`,
  `.geojson` filename matching included): fetches and validates a GeoJSON `Feature`/`FeatureCollection`/
  bare-geometry payload, computes a bounding-box fit, and renders it through `lr-map`'s new
  `dataLayers` property with a feature-count status line. Falls back to `lr-json-viewer` with a
  missing-library callout when the optional `maplibre-gl` peer isn't installed. Not a documented public
  tag this round — importing `geojson-view/geojson-view.js` opts a host into the bridge, matching how
  `lr-map`/`lr-graph`/the chart family already stay out of the root barrel import.
- ca9258f: `lr-graph` gains `renderer: 'svg' | 'canvas'` (default `'svg'`, unchanged). `'canvas'` swaps the
  per-node/per-link SVG DOM for a single DPR-aware `<canvas>` (reusing `lr-heatmap`'s proven backing-
  store/resize/DPR-watch machinery), targeting roughly 5,000 nodes / 10,000 links versus SVG's ~500/
  ~1,500 ceiling. Hit-testing uses an offscreen color-picking canvas (exact hits for all three node
  shapes, stroked/dashed links, and hull blobs, one code path, zero new dependencies); pointer drag,
  click, double-click-to-expand, and hover tooltips all work via that same hit-test. Keyboard/screen-
  reader parity is preserved through an offscreen virtual-cursor button list driving the identical
  roving/announcement logic as SVG mode — the honest v1 trade-off is no `::part(node)`/`::part(link)`
  styling (pixels, not elements) and a drawn focus ring instead of a CSS one, both documented. Fully
  additive — the default `renderer: 'svg'` reproduces today's DOM exactly.
- c6ab7c8: `lr-graph` gains `GraphNode.communityId` and a `communities` property, rendering one translucent
  convex-hull blob per entry (membership = union of `memberIds` and matching `communityId`) behind
  links/nodes. Hulls are keyboard/click-activatable (`lr-community-click`), join the roving focus
  ring after nodes and links, and are included in `fit()`'s bounding-box calculation. Fully additive
  — an empty `communities` array (the default) renders no hulls and leaves the roving ring/`fit()`
  behavior unchanged.
- c996af0: `lr-graph` gains `showEdgeLabels` (default `false`) to draw each link's `label` as visible SVG
  text at the segment midpoint, and `edgeLabelMinZoom` (default `0.6`) to hide all edge labels below
  that zoom scale. A per-label length gate also hides a label whose measured text width exceeds 85%
  of its edge's current on-screen length. Labels are `aria-hidden` (the accessible name already
  carries `label` via the existing link announcement) and fully opt-in — a graph that never sets
  `showEdgeLabels` renders no edge-label DOM at all.
- 7f7511a: `lr-graph` gains a double-activate expand gesture: double-clicking a node, or activating the same
  focused node twice via Enter/Space within 500ms, emits `lr-node-expand { id }`. A new
  `GraphNode.expandable` flag renders a "+" badge and adds "expandable" to the node's spoken text. A
  node newly linked to an already-positioned neighbor (e.g. appended after an expand) now spawns near
  that neighbor instead of a random position. Fully additive — no existing click/keyboard behavior
  changes, and a graph that never sets `expandable` never renders the badge (though the
  `lr-node-expand` event itself fires for any double-activated node, matching native
  dblclick semantics).
- 5d77b48: `lr-graph` gains a programmatic camera (`focusNode(id, { zoom? })`, `fit({ padding? })`, both
  reduced-motion-aware rAF tweens that keep d3-zoom's own state consistent), a declarative
  `focusId` twin (centers once, renders a persistent `focus-halo` ring), and a controlled selection
  model (`selectionMode: 'none' | 'single' | 'multiple'`, `selectedNodeIds`/`selectedLinkIds`,
  `lr-selection-change`) mirroring `lr-heatmap.selectedCell`'s controlled contract — the
  component only ever emits intent, never assigns the selection props itself. Fully additive: default
  `selectionMode: 'none'` and unset `focusId` reproduce today's behavior exactly.
- 844fe95: `lr-graph` gains `lr-node-enter`/`lr-node-leave`/`lr-link-enter`/`lr-link-leave` hover
  events (mirroring the existing `lr-node-click`/`lr-link-click` detail shapes) plus a `data-hovered`
  attribute toggled on the hovered node/link element for pure-CSS theming. Both are suppressed while a
  drag or pan gesture is in progress, so a drag crossing over other nodes/links doesn't spam
  enter/leave pairs. Previously a consumer computing an adjacency-based neighbor highlight on hover
  (e.g. dimming every unconnected node/link) had no way to observe which node/link was currently
  hovered from outside the component.
- f8d6b9e: `lr-graph` gains `layout: 'force' | 'layered'` (default `'force'`, unchanged). `'layered'`
  computes a deterministic Sugiyama-lite layout instead of running d3-force — longest-path layering,
  barycenter crossing reduction, cycle-safe (back edges reversed internally, the caller's data is
  never mutated). The algorithm itself lives in a new shared, dependency-free
  `src/internal/layered-layout.ts`, a standalone util suitable for any future layered-diagram
  consumer. Node drag is disabled in layered mode; pan/zoom, keyboard, focus/fit, hulls, edge labels,
  and type filtering all work identically to force mode. Fully additive — the default `layout:
'force'` reproduces today's simulation-driven layout exactly.
- 22c1006: Adds `<lr-graph-legend>`: a node-type legend for a paired `lr-graph`, rendering one swatch +
  label + count row per declared node type and doubling as a visibility filter. Event-decoupled from any
  graph instance — a host forwards `graph.nodeTypes` in as `types` and forwards
  `lr-visibility-change`'s `hiddenTypes` back out to `graph.hiddenTypes`.
- 942798e: `lr-graph` gains `GraphNode.type` and a new `nodeTypes` property declaring each type's legend
  label, fill color, and shape (`circle`/`square`/`diamond`). Fill resolution precedence is
  `node.color` > the type's own color > an ordered categorical fallback palette
  (`--lr-graph-cat-1`…`--lr-graph-cat-8`, new tokens) by the type's index in `nodeTypes` > the
  existing untyped default. Typed nodes also gain richer spoken text ("{label} ({type})"). Fully
  additive — a graph with no `type`/`nodeTypes` set renders identical circles, unchanged.
- 32f7b12: `lr-graph` gains `hiddenTypes: string[]`, hiding every node whose `type` is listed (plus incident
  links) from rendering, the simulation, the keyboard roving ring, and the accessible data list/
  counts. Positions round-trip via a new remembered-position cache, so toggling a type off and back
  on restores each node where it was instead of re-randomizing. Fully additive — an empty
  `hiddenTypes` (the default) renders every node/link exactly as before.
- e022166: Adds `<lr-handoff-divider>`: a labeled semantic separator marking control transfer between agents
  in a transcript (e.g. "Transferred to Research Agent"), with an optional `avatar` slot. Root is
  `role="separator"` named by the computed label; the label is announced once on first connect
  through an internal live region, since a handoff lands mid-stream and later property changes never
  re-announce.
- 4cddc07: Adds `<lr-highlight-layer>`: a presentational overlay that paints highlight rectangles
  (percent-of-box coordinates) over positioned content — a pdf page, an image, any relatively-positioned
  frame. Roving-tabindex keyboard access (ArrowUp/Down/Left/Right honoring RTL, Home/End, Enter/Space),
  `aria-current` on the active rect, a one-shot `flash()` emphasis pulse with a reduced-motion static
  fallback, and token-mapped tones. Zero dependencies. `lr-pdf-viewer` adopts it next for per-page
  highlight painting.
- 4c707de: Adds `<lr-image-viewer>`: a full pan/zoom raster-image viewer with labeled region highlights and
  opt-in region annotation (pointer-drag or keyboard), self-registering into the document-viewer
  registry for `image/png`, `image/jpeg`, `image/webp`, `image/gif`, `image/avif`, and `image/bmp`.
  Distinct from `<lr-svg-viewer>` (vector documents) and `<lr-image-comparer>` (before/after
  comparison) — this is the landing surface for `region`-anchored citations (bounding-box grounding).
- 2ad038b: `lr-json-viewer` gains an imperative search API (`runSearch()`, `searchNext()`,
  `searchPrevious()`, `clearSearch()`, event `lr-search-change`) as a thin layer over its existing
  declarative `search` property -- the property, its highlighting, and its force-expand behavior are
  unchanged; the new methods add match-count resolution and a navigable cursor (`data-active` on the
  current match) on top. The count-resolving entry point is named `runSearch()` rather than `search()`
  (unlike this same quartet on other viewers) because `search` is already this component's own public
  string property -- a method can't share its name. Previously there was no way to count matches or
  step between them programmatically.
- ac19eb0: `lr-lite-chart` gains a `legendText?: (label: string, datasetIndex: number) => string` hook,
  appending formatter-supplied text (e.g. a value or percentage share) after each series' label in the
  built-in legend row — mirrors the existing `pointText`/`tickFormat` opt-in-hook convention. Previously
  a consumer needing per-series legend text beyond the bare label had to hand-roll an entire replacement
  legend instead of using the built-in `legend` prop.
- c721d97: `lr-map` gains a `dataLayers: GeoJsonDataLayer[]` property: each entry adds a GeoJSON source plus
  fill/line/circle layers (colored from `--lr-*` tokens by an optional `tone`), independent of the
  existing `choropleth` prop (which requires `field`/`stops` and can't display plain geometry). Defaults
  to an empty array — zero behavior change for existing `lr-map` users. This is the enabler for the
  upcoming GeoJSON-file document-viewer bridge, and is useful standalone for rendering arbitrary
  GeoJSON shapes (routes, zones, points of interest) without hand-building maplibre-gl layers.
- 92955fc: `lr-markdown` gains `heading-anchors` (stamps computed GitHub-slugger-style ids on headings),
  `getHeadingTree()` (a document-ordered heading outline, computed regardless of `heading-anchors`),
  `fragment`/`text-quote` anchor-target support (`highlights`, `activeHighlightId`, `anchor`,
  `scrollToAnchor()`, events `lr-highlight-activate`/`lr-text-select`/`lr-anchor-result`), and
  `math` (renders `$...$`/`$$...$$` TeX as MathML via the optional `katex` peer, falling back to
  literal source text when the peer isn't installed). Previously there was no way to deep-link into a
  section, highlight a quoted passage, or render math in rendered Markdown content.
- 3492739: `lr-markdown` gains real shiki syntax highlighting for fenced code blocks, reusing
  `<lr-code-block>`'s own optional `shiki` peer and grammar-loading machinery directly (not by
  embedding `<lr-code-block>` itself, which would have hit DOMPurify's default custom-element
  blocklist and re-mounted — losing state and re-triggering async loads — on every streaming chunk).
  On by default whenever the `shiki` peer is installed (set `highlightCode="false"` to opt out); new
  `languages`/`languagesOnly` properties mirror `<lr-code-block>`'s own fine-grained bundle-size
  controls. Highlighting is skipped entirely while `streaming` is `true` and applied once a stream
  settles, so there is no added per-chunk cost while content is still arriving.
- e5df5af: Adds `<lr-message-actions>`: the per-message action toolbar for `lr-chat-message`'s `actions` slot
  — opt-in built-ins (`copy` / `regenerate` / `edit` / `feedback`, in `controls`-array order) that emit
  intent events (`lr-regenerate`, `lr-edit`, plus bubbled `lr-copy`/`lr-change`/`lr-submit`
  from the embedded copy button and thumbs-only feedback), and a default slot for custom controls (e.g.
  a slotted `lr-branch-picker`) that participate in the toolbar's ArrowLeft/ArrowRight/Home/End
  navigation. Optional `reveal-on-hover` hides the bar until the enclosing `lr-chat-message` is
  hovered or a control inside has focus.
- 9544450: Add `lr-message-feedback`: thumbs up/down for one assistant message, with an optional inline
  detail step (multi-select reason chips + a free-text comment) that opens as a disclosure directly
  below the thumbs rather than a floating overlay. Fires `lr-change` on every rating toggle and
  `lr-submit` (`{ value, reasonIds, comment }`) from the panel's submit button; stores nothing
  itself — a host persists the rating and may reflect a previously-recorded one back via `value` +
  `disabled`. Re-activating the pressed thumb clears the rating unless its own detail panel is open,
  in which case that click re-opens the panel with any surviving draft instead.
- 22c1006: Adds `<lr-mind-map>`: a radial expandable topic tree (NotebookLM-style Mind Maps) — zero-dependency
  SVG, closed-form arc-subdivision layout in its own `mind-map-layout.ts` module, single-tab-stop
  keyboard roving (mirroring `lr-word-cloud`), and `lr-topic-select`/`lr-topic-toggle` events.
  Multiple root topics hang off an implicit center hub; expansion state is keyed by topic id and
  survives streaming `topics` reassignment.
- 2ad038b: Recorded decision: `.msg` (Outlook) files are not supported this round. `.msg` is OLE/CFB binary per
  MS-OXMSG; the available npm parser (`@kenjiuno/msgreader` plus its `decompressrtf` companion) is
  below this library's maintenance bar for an optional peer. `.msg` files continue to resolve to
  `<lr-document-preview>`'s generic download fallback, exactly like any other unregistered format —
  convert to `.eml` server-side to use `<lr-email-viewer>` instead. No API change; this changeset
  exists to document the decision, guarded by a permanent regression test.
- 22c1006: Adds `<lr-neighbor-list>`: one entity's relationship rows (relation, direction, neighbor) with
  per-row navigate (`lr-entity-activate`) and expand-in-graph (`lr-node-expand`, matching
  `lr-graph`'s own event name/detail) affordances, optional relation grouping, and automatic
  `lr-virtual-list` virtualization above `virtualizeAt` rows.
- 22c1006: Adds `<lr-node-palette>`: a searchable, categorized node library for workflow editors — drag an
  item onto a `droppable` `lr-flow-canvas`, or place it by keyboard (`lr-palette-place`/
  `lr-select`). Fully decoupled from the canvas itself, agreeing only on the exported
  `FLOW_PALETTE_MIME_TYPE` drag-payload constant.
- a0e579a: Adds `<lr-notebook-viewer>`: a read-only Jupyter notebook (nbformat 4.x) renderer that parses
  `.ipynb` JSON natively and composes `lr-markdown`/`lr-code-block`/`lr-json-viewer` per cell,
  with `node-path`/`fragment` cell anchors and imperative search over cell sources and text outputs.
  Self-registers into the document-viewer registry for `application/x-ipynb+json`. Execution, kernels,
  and ipywidgets are out of scope; stream/error outputs render as plain preformatted text this round.
- 15062d0: Adds `<lr-page-rail>`: a virtualized vertical thumbnail rail for page-addressed documents, with
  per-page highlight heat markers. Wired mode (`viewer`/`for`) tracks page/count from a
  `PageThumbnailSource`-shaped viewer's own `lr-load`/`lr-page-change` events and lazily renders
  thumbnails as rows materialize (`lr-pdf-viewer` satisfies this structurally); mediated mode
  (`page-count`/`page`) works as a fully functional pager without a wired viewer. Roving-tabindex
  keyboard access via `lr-virtual-list`, typed-digit page jump, `lr-page-select` event.
- 22c1006: Adds `<lr-path-strip>`: a compact, horizontally scrollable node -> relation -> node chain
  rendering a GraphRAG reasoning path, with one roving tab stop across every element (nodes and
  relations alike), logical (RTL-mirroring) directed-edge arrows, and `lr-entity-activate`/
  `lr-relation-activate` events.
- 75c17bd: `lr-pdf-viewer` becomes the reference `DocumentAnchorTarget` implementation: resolves `page`,
  `text-quote`, and `region` anchors (`scrollToAnchor()`), paints highlights per page via
  `lr-highlight-layer`, exposes `getPageText(page)` and `renderPageThumbnail(page, canvas, options?)`
  for rail/search/chunking consumers, and emits `lr-load { pageCount }`,
  `lr-highlight-activate`/`lr-text-select`/`lr-anchor-result`. The `application/pdf` document-
  viewer registration now declares its anchor/text-select capabilities and forwards `anchor`/
  `highlights`. All additive — existing `src`/`page`/`zoom`/`nextPage()`/`previousPage()`/`zoomIn()`/
  `zoomOut()` and their events are unchanged.
- 1879c40: `lr-pdf-viewer` gains an imperative in-document search API (`search()`, `searchNext()`,
  `searchPrevious()`, `clearSearch()`, event `lr-search-change`), a public `goToPage(page):
Promise<boolean>` method, and `getOutline(): Promise<PdfOutlineItem[]>` for reading a PDF's table of
  contents. Search matches paint as `<mark part="search-match">` (`search-match-active` for the
  current one) without touching any highlight state. The `application/pdf` document-viewer
  registration now declares `search: true` in its capabilities. Previously there was no way to search
  inside a rendered PDF, jump to a page programmatically, or read its outline.
- 22c1006: Adds `<lr-provenance-panel>`: the grounding breakdown for one answer — a four-section disclosure
  panel (Entities / Relationships / Communities / Text chunks) composing `lr-entity-chip`,
  `lr-path-strip`, compact `lr-community-card`s, and a compact `lr-chunk-inspector`. Every child
  event bubbles straight through unmodified; its own `lr-toggle` event tracks per-section
  expand/collapse state, which survives streaming `provenance` reassignment.
- 2d15c51: Adds `<lr-push-to-talk>`: a mic capture button owning the full `getUserMedia`/`MediaRecorder`
  lifecycle — permission request, hold or toggle recording, optional chunked streaming
  (`lr-record-chunk`) for streaming STT, an opt-in RMS level meter (`lr-level`), a `max-duration-ms`
  auto-stop guard, and `lr-record-start`/`lr-record-stop`/`lr-record-cancel`/`lr-record-error`
  events. No SDK dependency — native browser APIs only. Previously lyra-ui had no voice-capture
  component at all; every agentic voice UI had to hand-roll this lifecycle from scratch.
- 3a2f6d2: Add `lr-rubric-form`: a configurable annotation rubric (LangSmith annotation-queue style) —
  score, category, and freeform-comment keys with a submit-and-next flow for working through an eval
  queue. Follows `lr-tool-param-form`'s exact `ElementInternals`-attached-directly, JSON-serialized
  form-value pattern; a `score` key renders `lr-segmented` (≤10 integer steps) or `lr-slider`,
  `category` renders `lr-select` or `lr-checkbox-group` (`multiple`), and `comment` renders
  `lr-textarea`.
- c388b94: Add themeable static edge fades and native horizontal scrolling to overflowing `lr-segmented` and
  `lr-tabs` rows.
- de5b8b7: Adds `<lr-sequence-strip>`: a compact, one-thin-cell-per-item strip visualizing a sequence of
  categorical states with an optional secondary per-cell marker (e.g. a CI build-step strip, a
  log-severity strip, or — the motivating case — a per-turn conversation-history strip). Pure CSS/flex,
  zero dependencies, `role="img"` with an auto-generated per-category "label: count" `aria-label`
  summary (matching `lr-sparkline`'s accessibility model), plus a pointer-hover tooltip showing each
  item's own label.
- 22c1006: Adds `<lr-source-picker>`: a checkbox tree/list scoping which sources ground the next answer —
  tri-state folders, select-all, `lr-file-icon` type icons, and built-in search that keeps matching
  descendants' ancestors visible. Deliberately not `FormAssociated` (a scoping panel, not a form
  control, mirroring `lr-tool-select-dialog`'s stance) and renders its own `role="tree"` rather than
  composing `lr-tree`, since `TreeItem` has no tri-state checkbox model.
- 685eb35: Add `lr-span-waterfall`: the horizontal-timeline projection of the same `LyraSpan[]`
  `lr-trace-tree` consumes — a time axis, one row per span in start order, and status-toned,
  keyboard-navigable bars (Langfuse timeline / Temporal event-history style). Declarative
  `viewStartMs`/`viewEndMs` window props (composable with `lr-time-range` as a brush) stand in for
  zoom/pan gestures this round. Both components emit the same `lr-span-select { id }` and accept
  the same `activeSpanId`, so a host syncs selection between them with two listeners and one property
  binding.
- 2ad038b: `lr-spreadsheet-viewer` and `lr-csv-viewer` gain `cell-range` anchor-target support
  (`highlights`, `activeHighlightId`, `scrollToAnchor()`, event `lr-highlight-activate`) and an
  imperative in-document search API (`search()`, `searchNext()`, `searchPrevious()`, `clearSearch()`,
  event `lr-search-change`) — identical on both viewers, addressing cells by the same 1-based raw
  grid (header row included) an A1 reference already implies. Spreadsheet's search/anchor resolution
  additionally spans every sheet, switching `lr-tabs` as needed. Both registry entries now declare
  `capabilities: { anchors: ['cell-range'], search: true, textSelect: false }`. Previously there was
  no way to highlight or search a specific cell/range in a rendered spreadsheet or CSV file.
- 761ab24: Adds `<lr-stack-trace>`: parses V8/JS-TS, Firefox/Safari, and Python stack traces (including
  chained-error groups) into a message plus collapsible, activatable frames (`lr-frame-select`),
  folding internal frames (`node_modules/`, `node:internal`, `site-packages/`, ...) behind a
  count-labeled toggle. Falls back to verbatim raw text when nothing parses.
- b33bb35: Adds `<lr-suggestion-chips>`: starter prompts (empty thread) and follow-up suggestions (after a
  response) as a horizontally scrollable chip row (or a wrapping grid via `wrap`), each with an optional
  secondary detail line. Fires `lr-suggestion-select` (`{ id, label }`) on activation — never writes
  into a composer or sends anything itself. Keyed `repeat()` on `id` preserves focus across a mid-stream
  suggestions replacement.
- 2ad038b: `lr-svg-viewer` and `lr-document-preview` (its image-format path) gain an opt-in `zoomable`
  property that wraps the rendered content in an internal `lr-zoomable-frame` for pan/zoom
  inspection, plus display-only `region` anchor-target support (`highlights`, `activeHighlightId`,
  `scrollToAnchor()`, event `lr-highlight-activate`) for percent-unit bounding-box highlights that
  scale with the zoom level. `zoomable` defaults to `false` on both, so an inline thumbnail (e.g. in a
  chat stream) doesn't unexpectedly grow a focusable zoom-chrome viewport. Previously neither viewer
  had any pan/zoom or region-highlighting capability.
- 1e051a4: `lr-swatch-picker` options gain an optional `icon` field (`SwatchOption.icon`, mirroring
  `lr-segmented`'s `SegmentedItem.icon`): a consumer-supplied shape (e.g. a brand glyph) rendered in
  place of the plain filled circle, exposed as `::part(swatch-icon)`. A `currentColor`-based SVG picks up
  the option's `color` automatically through the swatch's `color` custom property, so consumers who
  previously hand-rolled a row of colored icon buttons (rather than plain color circles) can now use the
  picker directly.

  The selected swatch also gains two new opt-in, off-by-default custom properties for a more emphatic
  selected state: `--lr-swatch-picker-selected-blur` (0 by default, a crisp ring; set a real length for
  a soft glow tinted by the swatch's own color -- works for both a plain color circle and an icon swatch,
  via a `box-shadow`/`drop-shadow` split so the glow follows the icon's actual silhouette rather than an
  invisible transparent box) and `--lr-swatch-picker-shine-duration` (0s by default, static; set a real
  duration for a rhythmic brighten-and-settle pulse, disabled under `prefers-reduced-motion: reduce`).
  Together they cover a "shining" gemstone-style accent-theme picker without changing the default look
  for any existing consumer.

- 55140c3: `lr-table` gains heat-tint mode: a per-column `heatValue(row)` accessor drives a `color-mix()`-based
  cell background computed from a shared min/max scale across the whole grid (auto-derived from the
  data, or overridden via the new `heatTintScale` property), matching `lr-heatmap`'s own
  `--lr-heatmap-scale-lo`/`-hi` ramp-token convention via new `--lr-table-heat-tint-lo`/`-hi` custom
  properties. Previously a consumer needing a value-driven cell background had to hand-compute a color
  string themselves via the existing `cellStyle` escape hatch.
- 6f7c938: `lr-table` gains `rowTotal`/`grandTotal`: a trailing column showing each row's total (`rowTotal`)
  and, when at least one column also defines `footer`, a grand-total cell at its bottom-right
  intersection (`grandTotal`). Both share the existing `footer(rows)` hook's "consumer computes/renders,
  table only positions" contract rather than assuming addition. Previously a consumer needing row/grand
  totals alongside `lr-table`'s existing per-column `footer` had to render them outside the table
  entirely, breaking column alignment.
- 4cae327: Adds `<lr-task-list>`: a live, collapsible tracker for an agent's plan, embedded in the
  transcript. Renders ordered steps with per-step lifecycle status (`pending`/`running`/`success`/
  `error`) and one level of nested sub-steps; status changes are announced through an internal
  throttled live region. A dynamic `detail-<id>` slot per item accepts rich content such as a
  `<lr-tool-call-chip>`. Unlike `<lr-stepper>` (a single-selection navigation control),
  `<lr-task-list>` is a read-only status report — several steps may be `running` at once, and
  there is no selection.
- bf223ca: Adds `<lr-terminal>`: a read-only, virtualized ANSI console for streamed agent/tool output — SGR
  color rendering (16 named colors, 256-color, truecolor), stick-to-bottom `follow` with a
  `lr-follow-change` event, `write()`/`content` streaming, `\r`/`\b`/`\t` cursor handling so progress
  bars render correctly, in-buffer `search()`/`searchNext()`/`searchPrevious()`/`clearSearch()`,
  `line-range` highlight/anchor support (`scrollToAnchor()`, `lr-highlight-activate`), and built-in
  copy/download affordances. Not a PTY — no stdin/keystroke handling or cursor-addressed full-screen
  apps.
- 52a90e5: Adds `<lr-test-results>`: a pass/fail suite summary with visible (never color-only) per-status
  counts, `aria-pressed` status filter toggles, and failure rows that auto-expand by default and can
  host a slotted `detail-{testId}` diff/code block. Row state (expansion, filter) survives a streaming
  `suites` reassignment mid-run, and a run's completion is announced through an internal live region.
- 967e785: Adds `<lr-thread-list>`: the conversation sidebar — a grouped ("Pinned / Today / Yesterday / Previous
  7 days / …"), searchable list of chat sessions built on `lr-conversation-item` and virtualized via
  `lr-virtual-list`. Data mode (`threads` array) renders rows with optional pin/archive/delete row
  actions, all controlled events (`lr-thread-pin`/`-archive`/`-delete`/`-rename`) carrying the
  _requested_ new state — no CRUD or persistence of its own. Slotted mode (host-supplied
  `lr-conversation-item`s) skips grouping/virtualization/row-actions entirely, for a host that wants
  full control over a short, unconstrained list.
- 9448c10: Add `lr-trace-tree`: a collapsible span hierarchy for one agent/LLM trace (Langfuse/LangSmith
  run-tree style) — kind icon, name, status, an inline duration bar on the shared trace time scale,
  and optional tokens/cost columns. Consumes a flat `LyraSpan[]` array (hierarchy derived from
  `parentId`); expand state survives a streaming reassignment of `spans`. The shared `LyraSpan` type
  (`components/trace-tree/span.ts`) is also consumed by the upcoming `lr-span-waterfall`, so the
  two components can render the same trace as two synchronized projections.
- bef6b0d: Adds `<lr-transcript-feed>`: a data-driven live-captions surface for an in-progress voice session —
  `entries` in (`{ id, speaker?, text, interim?, timestamp? }[]`), reconciled keyed by `id` so a same-id
  interim-to-final upgrade moves the row into the announcing `role="log"` region without a duplicate
  announcement. Ships the shared stick-to-bottom "follow" contract (`follow`/`lr-follow-change`, the
  same vocabulary `lr-terminal` uses). No dependency, no STT/diarization built in — bring your own
  transcription source and stream entries in.
- ec5fe96: Adds the `DocumentAnchorTarget` mixin (`internal/anchor-target.ts`) and its `LyraAnchorTarget`
  interface: the shared implementation of the anchor-target contract every anchor-capable lyra-ui
  viewer adopts — `highlights`/`activeHighlightId`/`anchor` properties, `scrollToAnchor()` with a
  generation-guarded retry-until-loaded loop and screen-reader announcements, and
  `lr-highlight-activate`/`lr-text-select`/`lr-anchor-result` event plumbing including
  selection->anchor emission. Internal module; no adopter yet in this release (`lr-pdf-viewer` adopts
  it next). No behavior change for any existing component.
- 44b6de7: Adds the shared `LyraAnchor`/`LyraHighlight` grounding-bridge type module
  (`@aceshooting/lyra-ui/components/document-viewer/anchors.js`): a W3C Web-Annotation-inspired
  discriminated union (`page`, `text-quote`, `fragment`, `line-range`, `cell-range`, `cfi`,
  `time-range`, `region`, `node-path`) that every anchor-capable viewer and every knowledge-grounded
  citation surface will address a passage through. Pure types plus one constant; nothing to register,
  no runtime behavior change for existing components.
- c644abd: Widens `DocumentFile` with optional `anchor`/`highlights`/`alt` fields and
  `DocumentRendererDefinition` with an optional `capabilities` declaration; `lr-document-viewer` gains
  matching `anchor`/`highlights`/`alt` properties, forwards them to the resolved renderer, and emits
  `lr-anchor-result { found }` once per applied anchor. Every addition is optional and every existing
  registration/usage is unaffected — this removes the previous limitation where even a renderer's own
  props (like pdf's `page`) couldn't be reached through the router.
- 5f92994: Adds `internal/text-highlights.ts`: a highlight paint manager for HTML-flow document viewers, using
  the CSS Custom Highlight API when available and falling back to `<mark>`-wrapping otherwise, with a
  uniform `acquireHighlightHandle()` API that never requires callers to branch on browser support
  themselves. Internal module with no public tag and no adopter yet in this release; ships ahead of the
  markdown/html-viewer/docx-viewer highlight support that will consume it. No behavior change for any
  existing component.
- b067b83: Adds `internal/text-quote.ts`: dependency-free `text-quote` anchor resolution (quote/prefix/suffix ->
  DOM `Range`, and the reverse — a selection `Range` -> a `text-quote` anchor with captured context).
  Internal module with no public tag; used by the `DocumentAnchorTarget` mixin's default selection
  handling and by `lr-pdf-viewer`'s anchor/highlight resolution. No behavior change for any existing
  component.
- bc75a1f: Adds `<lr-usage-badge>`: a compact, static resource strip for one message or run — tokens in/out,
  cost, latency — with a hover/focus tooltip breakdown (full grouped figures, plus a computed Total
  tokens row when both counts are set). Purely formatting: it computes no counts, rates, or prices,
  and every segment is independently optional. Reuses `<lr-tool-call-chip>`'s hover/focus/Escape
  tooltip contract. Distinct from `<lr-context-meter>` (occupancy of a fixed capacity) and
  `<lr-generation-status>` (a live ticking readout with a Stop button) — this is the static spend
  record shown after a message or run completes.
- f3c744b: `lr-virtual-list` gains a public `scrollToIndex(index, { align, behavior })` method: scrolls a
  specific row into view (`align: 'start' | 'end' | 'auto'`, reduced-motion-aware `behavior`) without
  the `aria-current`/"active row" side effect of the existing `active-id` property. In
  `row-height="auto"` mode, a far-off target's estimate-based offset is corrected with a single re-scroll
  once the row's real height is measured. Previously there was no way to programmatically scroll to a
  specific row at all except by driving `active-id`, which also marks that row as the current selection —
  a streaming transcript's own stick-to-bottom auto-scroll has nothing to do with "selection."
- e24ae10: Adds `<lr-voice-picker>`: a TTS voice selector mirroring `lr-model-select`'s closed-dropdown/
  free-text-combobox dual mode and form-association, with a `catalog` entry shape carrying
  `language`/`description`/`previewUrl`, and an event-first preview affordance (`lr-preview-request`,
  cancelable) that plays through one internal `<audio>` when a `previewUrl` is present and the host
  doesn't take over. No TTS SDK, no catalog fetching, no selection persistence — those stay host
  concerns.
- 37a89cb: Adds `lr-widget-renderer`'s internal type registry (`registerWidgetType()`,
  `getDefaultWidgetTypeRegistry()`) and its security-critical, DOM-free allowlist resolver
  (`resolveTree()`): unknown widget types and disallowed/mistyped props are skipped, never rendered;
  `forcedProps` always win; a child's `slot` outside its parent's allowlist renders unslotted; depth
  (32) and node-count (5000) caps are enforced. No public API surface change on its own — groundwork
  for the `<lr-widget-renderer>` element, landing in the same release.
- bcd3c2b: Adds `<lr-widget-renderer>`: renders an agent-streamed declarative JSON widget tree through an
  allowlisted `type → lyra tag` registry (`card`/`badge`/`button`/`stat`/`result-card`/`result-field`/
  `markdown`/`image` built in, plus `row`/`col`/`text` structural built-ins) — unknown types and
  disallowed/mistyped props are silently skipped, never rendered, with a deduped dev-mode warning; a
  single bubbling `lr-widget-action` event surfaces actions; streamed updates reconcile keyed by
  `id` (or structural path), so a mapped widget's own internal state survives a re-resolve.
  `registerWidgetType()` extends the default registry app-side; a per-instance `registry` property
  fully overrides it. No `innerHTML`/`unsafeHTML` path exists anywhere in the implementation.
- dc168c7: Adds `<lr-xml-viewer>`: a `DOMParser`-based collapsible XML tree view mirroring
  `lr-json-viewer`'s UX (`collapsed-depth`, `copyable`, structural-path expand state that
  survives a same-shape `xml` reassignment), with an imperative `search()`/`searchNext()`/
  `searchPrevious()`/`clearSearch()` API and `node-path` anchors (element indices plus an optional
  trailing `'@attrName'` segment for attribute-level targeting). Self-registers into the
  document-viewer registry for `application/xml`/`text/xml` and `.xml`/`.xsd`/`.xsl`/`.xslt`/`.rss`/
  `.atom` files. No XPath/XSLT evaluation, no editing, no schema validation.

### Patch Changes

- 7bbd069: Internal only: adds three new `src/internal/` modules (`slugger.ts`, `cell-range.ts`,
  `viewer-search.ts`) and five new localization keys (`viewerSearchMatchCount(Plural)`,
  `viewerSearchNoMatches`, `viewerSearchActiveMatch`, `viewerHighlightLabel`) used by upcoming
  per-viewer search/anchor/highlight support. No consumer-visible behavior change on its own.
- da8bbf0: Requires `@aceshooting/lyra-flags` `^1.4.0` (up from `^1.3.0`) as the optional flag-asset peer.
  1.4.0 is a docs/metadata-only release of the flags package (no runtime change), so this is a
  range refresh, not a behavioral requirement bump.
- 967e785: Fixes `<lr-virtual-list>`: a `groups`-supplied group marker no longer carries `role="heading"`
  `aria-level="2"`. Those markers render inside the scroll container's `role="list"`, and ARIA's `list`
  role only permits `listitem` as a direct owned child — a `heading` sibling was a critical
  `aria-required-children` violation for any consumer combining `groups` with an accessibility check
  (surfaced by `<lr-thread-list>`'s date-grouped rows). The marker is still rendered as visible,
  non-interactive text; it's just no longer exposed as a heading landmark.

## 3.7.0

### Minor Changes

- 05c9f9c: Add `appearance="link"` to `<lr-button>`: a true inline-link tier that renders as zero-chrome underlined text — no padding, border, border-radius, or `min-block-size` floor — colored from the same `--lr-button-accent` token `appearance="plain"` uses (so `variant` still selects the link color) and inheriting the surrounding font-size/weight so it flows within a sentence rather than as a button-shaped control. Previously the smallest `<lr-button>` was still a padded, rounded, 24px-tall pill with a (transparent-but-present) border and no `text-decoration`, so an inline text link had to be hand-rolled; `appearance="link"` now covers that case directly. The notable design choice: the link rules are declared after the per-`size` rules so `font: inherit` and the zero padding/border/min-height win over whatever `size` is set, and the shared `[part='base']:focus-visible` outline is deliberately left intact.
- 2ed831d: `<lr-file-icon>` gains a `size` property (bytes, formatted via the same convention as `<lr-attachment-chip>`) shown alongside its label, and exposes the raw MIME type as a `title` tooltip.
- a5482d8: Add `<lr-swatch-picker>`, a single-select picker over a small, fixed set of color swatches — the row-of-round-accent-color-buttons pattern apps hand-roll, generalized into a first-party component. It carries the WAI-ARIA APG `radiogroup` contract (`role="radiogroup"`/`role="radio"`, roving tabindex, automatic activation on click or arrow-key move, cyclic Arrow/Home/End navigation), takes an `options: { value; color; label }[]` array plus a controlled `value`, and emits `lr-change` (`detail: { value }`) only when the selection actually changes. It is distinct from `<lr-color-picker>`'s freeform native color input: this picks exactly one of N designer-chosen named colors.

  Notable design choice: the selection ring uses a dedicated `--lr-swatch-picker-selected-color` token (defaulting to `--lr-color-brand`) so it retheme independently of the focus ring, mirroring `<lr-heatmap>`'s `--lr-heatmap-selected-color`; each swatch's fill comes from its option's `color`, applied through a per-swatch custom property so a consumer's `::part(swatch)` background rule can still override it.

### Patch Changes

- f3a606f: Fix `<lr-file-icon>`'s format badge overflowing its fixed size for multi-word localized labels (e.g. "Word document") — long badge text now truncates with an ellipsis instead of spilling outside the badge.
- 64e6cb6: Document `<lr-file-icon>`'s new `size` property and `size` csspart in `llms-full.txt`, and add the explicit-MIME-vs-filename-extension precedence test called for by the original feature request's acceptance criteria.
- 0975bcd: Fix `<lr-map>` throwing an unhandled error when the underlying maplibre-gl `Map` emits an `'error'` event (e.g. a tile/style source request failing) with no listener attached — maplibre-gl's `Evented` base rethrows in that case. The error is now caught and logged via `console.error` instead of surfacing as an uncaught exception.

## 3.6.0

### Minor Changes

- 30db265: Nine new components:

  - `lr-animated-image` — a still/animated-GIF-style image that pauses on `prefers-reduced-motion`
    and exposes a play/pause toggle.
  - `lr-animation` — declarative Web Animations API wrapper for a slotted target, with named
    timing presets, `prefers-reduced-motion` handling, and `lr-start`/`lr-finish`/`lr-cancel`
    events.
  - `lr-avatar-group` — a stacked, overlapping set of avatars with a "+N" overflow indicator.
  - `lr-include` — fetches and renders external HTML/Markdown/plain-text content client-side, with
    URL validation and DOMPurify sanitization.
  - `lr-known-date` — a form-associated day/month/year input for approximate or partial dates
    (e.g. a birth date where only the year is known).
  - `lr-lightbox` — a full-screen, modal, click-to-enlarge image viewer with prev/next navigation
    across an ordered set of images, built on the same shared overlay infrastructure as
    `lr-dialog`/`lr-command-palette`.
  - `lr-qr-code` — renders a QR code from text/URL data, via the optional `qrcode` peer dependency
    (same optional-peer pattern as the chart/map bundles).
  - `lr-random-content` — displays a randomly (or sequentially) chosen subset of its slotted
    children, with optional autoplay.
  - `lr-timeline`/`lr-timeline-item` — a vertical event timeline with per-item status/icon
    markers.
  - `lr-tour` — a guided, multi-step product-tour overlay that highlights target elements in
    sequence.

### Patch Changes

- e1aca7e: Harden shared infrastructure and close cross-component consistency gaps:

  - `lr-contact-viewer` and `lr-email-viewer` now expose a proper localized `aria-label` on their
    root surface (previously had no naming mechanism at all); `lr-calendar-viewer` gets the same
    fallback chain's final localized tier.
  - `lr-stat`'s trend announcement now interpolates the percentage into one localized template
    instead of concatenating separately-localized fragments (word order safe for non-English locales).
  - Fixed a real bug in `lr-model-settings-panel`'s `decimalPlaces` helper that returned `0` instead
    of the correct precision for exponential-notation step values (e.g. `1e-7`); it now shares the
    same exponential-aware implementation as `lr-slider`/`lr-time-range` via a new
    `src/internal/numbers.ts` export instead of a diverging local copy.
  - Deduplicated five other byte-identical/near-identical helpers that had drifted into 2-5 separate
    component files each (`prefersReducedMotion`, canvas-context memoization, swatch-color
    sanitization, slotted-content detection, and a title-attribute-stripping mixin) into single
    `src/internal/` implementations.
  - Removed an unused, never-adopted RTL helper (`rtlAwareSide`/`PhysicalSide`) from
    `src/internal/rtl.ts`.
  - Added missing accessibility test coverage for `lr-icon-button` and the standalone `lr-option`
    element (previously the only two custom elements in the library with no axe check).

## 3.5.0

### Minor Changes

- 681ed1f: Broad component hardening pass across ~50 components:

  - `lr-command-palette` now uses the shared overlay infrastructure (`lr-dialog`'s
    focus-trap/Escape/backdrop/scroll-lock manager) instead of a bespoke implementation, adds
    `aria-activedescendant` tracking, and keeps the highlighted row scrolled into view.
  - `lr-table` forwards `spellcheck`/`autocapitalize`/`autocorrect` to its filter input and inline
    text-cell editor, matching the string-aware `spellcheck` converter already used by
    `lr-textarea`/`lr-model-select`.
  - `lr-token-input` and `lr-code-editor` fix `label`/`hint`/`error` slot-vs-attribute detection
    (a `[part]:empty` selector never matches since the part always contains a `<slot>`), and
    `lr-token-input` adopts the `effectiveDisabled`/`_fieldsetDisabled` pattern so a `<fieldset
disabled>` ancestor no longer permanently overwrites its own `disabled` property.
  - `lr-calendar`: month grid gets proper `role="grid"`/`role="row"`/`role="gridcell"` semantics,
    per-day `aria-label`, a sanitized event-color style (rejects `url(...)` and anything else that
    isn't real CSS color syntax), and RTL-aware nav chevrons; `firstDayOfWeek` tolerates out-of-range
    input instead of producing `Invalid Date`.
  - `lr-icon` clones custom slotted SVG content into the component's own `<svg>` so slotted
    path/circle/group children paint reliably in Chromium.
  - `lr-document-preview` simplifies its abortable-fetch generation tracking onto the shared
    `beginAbortableLoad` helper.
  - `lr-app-rail-item`'s tooltip text now ignores text incidentally living in the decorative `icon`
    slot, mirroring `lr-chip`'s `labelText` getter.
  - Smaller accessibility/consistency fixes across app-rail, attachment-chip, breadcrumb, callout,
    chart/histogram, checkbox-group, data-grid, empty, format-\*, heatmap, html-viewer,
    image-comparer, intersection/mutation/resize-observer, map, model-select, pdf-viewer,
    phone-input, progress, radio/radio-group, responsive-panel, scroller, segmented, sparkline,
    split, stat, stepper, streaming-text, switch, tool-param-form, tool-select-dialog, widget, and
    zoomable-frame, plus a new standalone `breadcrumb-item.styles.ts` module and expanded test
    coverage throughout.

## 3.4.0

### Minor Changes

- d0ee919: Add command-palette, checkbox-group, token-input, icon/icon-button, code-editor, data-grid, and
  calendar components. Harden file-input with clipboard paste, native directory selection, and
  dropped-folder rejection reporting.
- 1293f48: Hardening pass across ~70 components: document the button/spinner interaction custom-property APIs
  (`--lr-button-width`, hover-brightness, active-scale, spinner-duration) and add missing cssparts;
  `lr-breadcrumb` now reads its accessible-name override from the standard `aria-label` attribute
  (was `accessible-label`); phone-input preserves the caret through adapter reformats and ships a
  libphonenumber-js-backed adapter path with a clearer incomplete-number message; prune unused
  localization keys and size/line-height tokens; broaden test coverage across the library.

## Unreleased

### Minor Changes

- Added `<lr-command-palette>` with searchable command registration, groups, keyboard navigation,
  Escape dismissal, and a configurable `mod+k` shortcut.
- Added `<lr-checkbox-group>` and `<lr-token-input>` as form-associated composite controls with
  array values, native reset/validity behavior, localized chrome, and accessible focus/editing APIs.
- Added `<lr-icon>` and `<lr-icon-button>` as dependency-free SVG and icon-only action primitives.
- Added `<lr-code-editor>` with line numbers, tab insertion, native textarea selection APIs, and
  editing-assistance passthrough.
- Added `<lr-data-grid>` with sortable headers, roving cell focus, row selection events, loading/
  empty states, and responsive overflow.
- Added `<lr-calendar>` with responsive month and agenda views, event markers, date navigation,
  RTL-aware keyboard navigation, and date/event selection events.
- Hardened `<lr-file-input>` with clipboard paste support, optional native directory selection, and
  explicit dropped-folder rejection reporting.
- Updated the component catalog, consumer API reference, custom-elements manifest, stories, and
  accessibility/behavior coverage for the new public surface.

## 3.3.0

### Minor Changes

- 7e7cc44: Harden every remote-resource viewer against oversized, cancelled, and failed loads, and close a set of localization gaps.

  **Resource limits.** A new internal resource loader caps any remote resource a viewer fetches at 25 MB before handing it to a parser, enforced by streaming the response so the cap holds even when the server omits `Content-Length`. Parsed tabular data is additionally capped at 10,000 rows and 1,000 columns before it is retained or rendered. Exceeding either limit now surfaces the localized `documentPreviewResourceTooLarge` message instead of attempting the parse. This is a behavior change for consumers previewing documents above those thresholds — they will now see a size error where the viewer previously tried (and typically hung or crashed) on them.

  **Cancellable loads.** `LyraElement` gained internal `beginAbortableLoad()` and `scheduleAfterUpdate()` helpers. In-flight fetches are now aborted when the element disconnects or its `src` changes again, and loads are coalesced to one per update rather than firing from `willUpdate`. This fixes stale responses racing a newer `src` and work continuing after an element is removed from the DOM. A `src` assigned while an element is detached is held and replayed when it reconnects, rather than being dropped.

  **Error messages no longer leak internals.** Viewers previously rendered raw `error.message` text (fetch/parser internals, URLs) directly into the UI on failure. They now render the localized `documentPreviewFailedToLoad` message, with the underlying error still available to consumers via the `lr-render-error` event.

  Affected viewers: `lr-archive-viewer`, `lr-calendar-viewer`, `lr-contact-viewer`, `lr-csv-viewer`, `lr-dataset-viewer`, `lr-docx-viewer`, `lr-document-preview`, `lr-ebook-viewer`, `lr-email-viewer`, `lr-html-viewer`, `lr-pdf-viewer`, `lr-pptx-viewer`, `lr-spreadsheet-viewer`, `lr-svg-viewer`.

  **Localization fixes.**

  - Form-associated components rendered the required-field validation message as a hardcoded English string (`Please fill out this field.`). It now resolves through the `fieldRequired` message key, so `registerLyraLocale()` and per-element `strings` overrides apply. Note that this also changes the default English text to `This field is required.` — if you assert on `validationMessage`, update the expected string.
  - Removed a duplicate `hidePassword` member from the `LyraMessageKey` union. The key itself is unchanged and still used by `lr-input`; only the redundant second declaration is gone.

  **Component coverage contract.** A new `check-component-coverage.mjs` gate runs as part of `contract-policy` (and therefore `lint`), requiring every public tag in the manifest to be exercised by a story and a behavior test, and every component family to carry an accessibility assertion. Stories and tests were added across the library to satisfy it, and `test:coverage` now runs the full test suite rather than five hardcoded files. No public API change.

## 3.2.0

### Minor Changes

- 62c6b05: `lr-attachment-chip` gains a preview action: a new `previewSrc` property (used when `file` is
  unset; a real `File` takes precedence via a temporary blob URL) and `previewable` boolean (default
  `true`) show a new `preview-button` part whenever a file or preview source is available, emitting
  `lr-preview` (`detail: { id, name, mimeType, src }`) to open `<lr-document-viewer>` with the
  same effective MIME type. `lr-document-viewer` gains a matching `download-link` slot and
  `lr-download` event for a safe native download action. Both properties/events are additive and
  default off/no-op, so existing usages are unaffected.

## 3.1.0

### Minor Changes

- de80dc5: Adds `<lr-archive-viewer>` for listing names and human-readable sizes inside `.zip` archives via
  the optional `jszip` peer. It registers standard ZIP MIME types and a `.zip` filename fallback with
  `<lr-document-viewer>`; other archive formats remain on the generic download fallback.
- de80dc5: Adds the optional `line-numbers` display to `<lr-code-block>` and `<lr-code-block-core>`.
- 53c7c13: Add sanitized SVG and HTML viewers, plus PapaParse-backed dataset and vCard contact viewers to the document renderer registry.
- c6dd26c: Adds `<lr-document-viewer>`, a dialog-hosted, format-dispatching document viewer, plus a
  `registerDocumentRenderer()` registry for plugging in per-format renderers. Files without a
  registered renderer fall back to the existing `<lr-document-preview>` component.
- d992ee7: Adds `<lr-docx-viewer>`, rendering `.docx` Word documents as sanitized semantic HTML through the
  optional `mammoth` and `dompurify` peers. It registers the official WordprocessingML MIME type and
  falls back to matching `.docx` filenames.
- de80dc5: Adds `<lr-ebook-viewer>` using the optional `epubjs` peer and registers EPUB files with the
  document-viewer registry.
- 49f7b87: Adds `<lr-email-viewer>` for sanitized `.eml` messages via the optional `postal-mime` and
  `dompurify` peers, plus `<lr-calendar-viewer>` for `.ics` event lists via optional `ical.js`.
  Both viewers register their standard MIME types and filename-extension fallbacks with
  `<lr-document-viewer>`.
- de80dc5: Adds `getFileTypeMetadata()`, `registerFileTypeMetadata()`, and `<lr-file-icon>` for localized,
  tokenized MIME/filename format presentation.
- 68bb5e3: Adds `<lr-pdf-viewer>`, a PDF renderer built on optional `pdfjs-dist`, with pagination, zoom, selectable text, and virtualized page rendering.
- de80dc5: Adds `<lr-pptx-viewer>` using the optional `@aiden0z/pptx-renderer` peer for best-effort client-side
  PPTX rendering with a persistent fidelity notice.
- 0b6f412: Add SheetJS-backed spreadsheet and PapaParse-backed CSV document viewers with virtualized rows.

## 3.0.0

### Major Changes

- a712749: **Breaking:** the outer, externally-overridable tier of the design-token chain no longer lives in
  the previous external theme-input namespace — it moved to lyra's own `--lr-theme-*` namespace
  (for example, the brand fill input now uses `--lr-theme-color-brand-fill-loud`). Any consumer
  retheming components through the old external custom properties must rename those
  properties to `--lr-theme-*`; the two-tier override mechanism itself (set one property at any
  ancestor to retheme every component) is unchanged. This removes lyra-ui's remaining live runtime
  CSS coupling to Web Awesome.

### Minor Changes

- 66c8819: Adds an independent `--lr-theme-*` shared token layer, aligns `<lr-button>`'s medium size with
  the standard Lyra font scale, exposes its host-width and size contracts, and adds opt-in native
  per-cell semantics to `<lr-heatmap>` through `accessible-cells`.

### Patch Changes

- 11e6a03: `lr-details`/`lr-accordion-item` no longer render the localized "Details" fallback text alongside rich content slotted into `summary` when the plain-string `summary` prop is left unset. The fallback previously always rendered whenever `summary` was empty, regardless of whether a `slot="summary"` child was present — visible only when a consumer needed markup (an icon, multiple spans) in the summary rather than a plain string.
- 581f5f3: `installHappyDomFormAssociatedShims()` no longer throws a `ReferenceError` when `HTMLElement` isn't a global at all — e.g. a plain Node Vitest environment sharing one `setupFiles` entry with happy-dom/jsdom test files. It previously read `HTMLElement.prototype` unconditionally, contradicting its own documented "safe to call unconditionally from a shared setup file used across multiple test environments" contract.
- b5de65c: `lr-popover`/`lr-dropdown`/`lr-tooltip`'s `[part="popup"]` is now `position: fixed` from the start instead of only once the popup is first opened and JS positions it. Previously, while closed, the popup stayed `position: static` sized to its full slotted content, inflating the component's own inline-block host box to match -- an invisible-but-still-hit-testable area that could sit on top of unrelated page content and intercept pointer events until the trigger was first clicked.

## 2.13.0

### Minor Changes

- 80cb577: `lr-table` gains opt-in row selection (`selectionMode: 'single' | 'multiple'`, `selectedKeys`,
  `lr-selection-change`), a built-in filter field (`filterable`, `filterText`, `filter`,
  `lr-filter-change`), controlled pagination through `<lr-pagination>` (`pageSize`, `page`,
  `totalItems`, `paginationMode`, `lr-page-change`), a `loading` state with an indeterminate
  spinner, per-column double-click inline editing (`TableColumn.editable`/`editValue`/`editType`,
  `lr-cell-edit`), and row grouping (`groupBy`, `groupLabel`). All new properties default to
  today's exact behavior when left unset.
- 5628327: `lr-input` and `lr-textarea` now also emit native-style `input`/`change` events (composed,
  matching the native element's own timing) alongside the existing `lr-input`/`lr-change`
  aliases, so consumers migrating from a native `<input>`/`<textarea>` don't need to rename their
  listeners. Both components also forward `spellcheck`, `autocapitalize`, `autocorrect`,
  `inputmode`, and `enterkeyhint` to their internal native control.
- d009cd8: Adds a new "Web Awesome parity primitives" family: `lr-badge`/`lr-tag`, `lr-callout`,
  `lr-divider`, `lr-breadcrumb`/`lr-breadcrumb-item`, `lr-details`/`lr-accordion`/
  `lr-accordion-item`, `lr-button-group`, `lr-carousel`/`lr-carousel-item`,
  `lr-color-picker`, `lr-drawer`, `lr-popover`/`lr-tooltip`/`lr-dropdown`/
  `lr-dropdown-item`, `lr-radio`/`lr-radio-group`, `lr-rating`, `lr-spinner`,
  `lr-progress-bar`/`lr-progress-ring`, `lr-format-number`/`lr-format-date`/
  `lr-format-bytes`/`lr-relative-time`, `lr-image-comparer`, `lr-zoomable-frame`,
  `lr-scroller`, and headless `lr-intersection-observer`/`lr-mutation-observer`/
  `lr-resize-observer` wrappers. `lr-number-input` and `lr-time-input` join `lr-input` as
  sibling native-input-type primitives.

  These close out the remaining free-tier Web Awesome components with no prior lyra-ui equivalent —
  133 tags total, up from 97.

### Patch Changes

- 5766257: `installHappyDomFormAssociatedShims()`'s stub `ElementInternals` now implements `setValidity()` as a no-op. `AnchoredValidityController` (used by every form-associated component) calls `internals.setValidity()` on every update, not just at construction, so a consumer's happy-dom test suite installing the shim would throw the moment any shimmed component's value changed after mount.

## 2.12.0

### Minor Changes

- 42036af: `lr-table` gains expandable rows: a table-level `expandedContent?: (row) => unknown` renders a
  full-width panel beneath any row whose key is in the new consumer-owned `expandedKeys: Set<string |
number>` property, toggled via a built-in leading chevron cell and the new `lr-row-expand-toggle`
  event (`detail: { row, key }`). An optional `canExpand?: (row) => boolean` gates which rows get an
  interactive toggle at all. All three properties are additive and default to a no-op, so existing
  tables are unaffected.

- d612939: Make card headers wrap with their actions in narrow allocations, expose citation previews through
  a stable tooltip relationship, and localize the complete citation status announcement.

  Add reactive `accessibleLabel` overrides to both code-block variants and media cards so host
  `aria-label` values reach the actionable or semantic element inside shadow DOM. Media-card's
  unnamed actions now use complete, per-kind localized messages.

  Keep markdown within logical narrow allocations and make its `streaming` state hold `aria-busy`
  until the final content update.

- 159f3c9: `lr-file-input` now forwards host accessible names to its dropzone and file input, exposes an
  imperative focus target, reports explicit enabled/disabled ARIA state, and announces accepted and
  rejected file counts with correct singular and plural messages.

  `lr-export-button` now forwards host accessible names to its trigger, exposes native focus and
  blur methods, and keeps long format menus within the positioned overlay's available space.

  `lr-document-preview` now supports explicit image alternative text (including `alt=""` for
  decorative previews), aborts superseded text fetches, and documents its sizing, font, and spinner
  motion custom properties.

- 3da4f80: `lr-button` ships a default `:hover`/`:active` pointer-interaction treatment on `[part='base']`
  (`filter: brightness(--lr-button-hover-brightness)` on hover, `transform: scale(--lr-button-active-scale)`
  on active, both disabled under `prefers-reduced-motion`) -- previously it had zero hover/active CSS,
  so a mechanical `wa-button` -> `lr-button` rename silently dropped all pointer-interaction feedback.

  `lr-button` is now form-associated (`static formAssociated = true` + `attachInternals()`), so it
  participates in an ancestor `<form>.elements` the same way `wa-button` does -- a sibling text field's
  own Enter-to-submit lookup (which scans `form.elements` for a `type === 'submit'` control) now finds
  it, instead of silently failing to submit the form.

  `lr-button` gains an `appearance="accent"` value -- a loud, high-contrast filled tier equivalent to
  `wa-button`'s own runtime-default appearance, including for `variant="neutral"` (`'filled'` reads the
  ambient surface color there, matching `wa-button`'s `appearance="filled"`; `'accent'` reads a solid
  neutral fill, matching `wa-button`'s own unset-appearance default). New `--lr-button-accent-fill`/
  `-accent-on-fill` custom properties back it.

  `lr-heatmap` gains a `monthLabelText?: (jsMonth: number, year: number) => string | undefined`
  property, the month-axis analogue of the existing `weekdayLabelText` -- lets a consumer's calendar-mode
  month labels track the same locale signal (e.g. an app's own i18n store) as every other localizable
  string on the component, instead of always following `toLocaleString(undefined, ...)`'s browser/OS-
  language default. Unset (the default) reproduces today's exact locale-derived output.

- 8a1777b: `lr-skeleton` adds an `announce` switch so grouped or decorative placeholders can avoid
  duplicating live-region announcements. Pulse and sheen effects now use the shared
  `--lr-transition-ambient` motion token and remain disabled by the reduced-motion branch.
- 8e8a77f: `lr-tool-result-dialog` now forwards host `aria-label` to the internal dialog, exports its
  typed event map, localizes complete duration messages, omits non-finite durations, exposes its
  running-spin timing, and wraps footer actions in narrow layouts.

### Patch Changes

- 6ba4d1f: Localize generation metrics, graph position announcements, attachment upload context, and duration templates. Mirror JSON viewer disclosure chevrons in RTL and give map content a named semantic group with correct host-label precedence.
- b67a25e: Forward host accessible names to the semantic canvas or SVG in the chart, histogram, box-plot,
  and lite-chart families. Localize numeric summaries, mirror chart axes in RTL, refresh derived
  histogram data, improve BoxPlot theming and reduced-motion behavior, and support narrow allocations
  with long content across charts and context meters.
- 5dd8066: `lr-chat-message` now formats its default timestamp with the component's effective locale,
  uses the shared ambient-motion token for streaming feedback, and wraps crowded footer controls
  in narrow allocations.
- e95f942: Adds a complete interpolated localization message for citation status announcements so
  translations can reorder the citation index and status naturally.
- 303e701: `lr-heatmap` now localizes its built-in value label and formats legend, accessible-range, cell,
  and calendar-date values with the component's effective locale. Explicit `value-label` text remains
  verbatim.
- 87eb96a: `lr-heatmap` now mirrors its low-to-high legend ramp in right-to-left layouts, including
  consumer-provided multi-stop palettes.
- 134dba0: Adds a complete interpolated localization message for lite-chart mark announcements so
  translations can reorder series, label, value, and position naturally.
- 0260f9b: Harden `lr-app-rail`, `lr-attachment-chip`, `lr-avatar`, and `lr-chip-group`: respect the
  configured element prefix, preserve localized attachment-message word order, support image `File`
  objects in thumbnail-only mode, make spinner timing themeable, retry replacement avatar images,
  forward avatar accessible-name overrides, and collapse slot-forwarded overflow chips correctly.
- 9033a43: Forward host naming and native textarea editing APIs through `lr-chat-composer`, complete
  `lr-phone-input` selection and range-editing methods, and expose observable focus/blur contracts
  for pagination, playback, and select controls.
- acbbf00: Logical safe-area tokens now mirror the underlying physical browser insets in right-to-left
  layouts, keeping dialogs, toasts, widgets, and tool overlays clear of notches on the correct side.
- 1f93e0c: `lr-sparkline` now applies its generated or consumer-provided accessible name to the internal
  SVG that owns the image role. Generated value summaries also respect the component's effective
  locale and per-instance message overrides.
- 18003e2: `lr-tool-call-chip` now interpolates duration values through localized message templates and
  exposes coherent motion controls for its running spin and pending pulse. Its event map is also
  exported for typed listeners.
- 140f9ea: Align `lr-checkbox` with the native checkbox keyboard, focus, reset, ARIA-state, and `input`/`change` event contracts while retaining `lr-change` as a compatibility alias.
- d099ea7: Complete the combobox's native editing surface and clearable compatibility, align conversation-item event and story semantics, add accessible disabled and timing controls to copy-button, and localize and theme flag presentation.

## 2.11.0

### Minor Changes

- c0648ec: `lr-input` gains a `size: 'xs' | 's' | 'm' | 'l' | 'xl' = 'm'` property (reflected), the same scale
  `lr-select`/`lr-combobox` already use — `--lr-input-padding-block`/`-padding-inline`/
  `-font-size` swap per size, the same pattern as `lr-select`'s own size tokens. Unset (the default,
  `'m'`) reproduces today's exact sizing.

## 2.10.0

### Minor Changes

- f506542: `lr-heatmap` gains a `selectedCell` property (`{ row, col }` in matrix mode, `{ date }` in
  calendar mode) — a controlled, consumer-owned marker (mirroring `lr-lite-chart`'s
  `selectedIndex`) that draws a persistent canvas ring independent of keyboard focus, appends a
  "Selected: ..." description to the host's own `aria-label` so it stays discoverable after focus
  moves elsewhere, and appends a "(selected)" suffix to the keyboard live-region announcement. Unset
  (the default, `null`) reproduces today's exact output.
- 6f6d758: Add `lr-button`, a generic action-button primitive (`variant`/`appearance`/`size`/`loading`/`disabled`/`type`, default + `start`/`end` slots) -- the `lr-*` equivalent of a plain `wa-button`.
- 5eda04d: Add `lr-input`, a single-line plain-text input primitive (`type="text"`/`"password"`/`"email"`/`"number"`, label/hint/error chrome, form-associated validation, a built-in password-visibility toggle) -- the `lr-*` equivalent of a plain `wa-input`.
- 7c95e95: `lr-tool-result-view` gains a real `fallback="text"` mode (previously accepted as an attribute
  value but silently treated identically to `"json"`): a string `result` renders as preformatted text
  instead of being forced through `<lr-json-viewer>`'s tree view, falling back to the `"json"`
  behavior when `result` isn't a string. A new `copyable` property adds a copy-to-clipboard affordance
  to either fallback kind. Additive — unset, both fallback kinds and every existing consumer render
  byte-identical to before.

### Patch Changes

- 83fe6ba: Fix `lr-heatmap`'s `llms-full.txt` section, which was missing four real, already-shipped members
  (`cellInteractive`, `weekdayLabelText`, `colorSteps`, `refreshTheme`), and add a matching
  `focus()`/`blur()` mention to `lr-button`'s own section. Add a `pnpm run llms-freshness` lint gate
  (wired into `contract-policy`, so it runs in `lint`/CI/`publish.sh`) that fails the build if any
  custom element's public property isn't mentioned anywhere in its own `llms-full.txt` section, so
  this can't silently drift again. A small baseline of ~20 pre-existing drift items on unrelated
  components (chart family, dialog, menu, split, tree-node, widget, etc.), discovered while building
  this check, is exempted for now via a documented allowlist in the script — out of scope for this
  change, left for a follow-up cleanup.

## 2.9.0

### Minor Changes

- b4a6f5b: `lr-heatmap`'s color ramp now preserves a translucent `rgba()`/`hsla()`/hex-with-alpha color instead of silently resolving it to fully opaque. `resolveRgb()`/`hexToRgb()` return an `[r, g, b, a]` quadruple (previously `[r, g, b]`), and the ramp emits `rgba(...)` whenever an endpoint is translucent — unchanged `rgb(...)` output for opaque colors, so an existing consumer using only opaque `--lr-heatmap-scale-lo`/`-hi` values sees no difference. Lets a consumer key a ramp endpoint off a themed semi-transparent surface token (e.g. a "quiet baseline" tint) and get the intended translucent cell color instead of a stark opaque one.

## 2.8.0

### Minor Changes

- 0331bbf: `lr-table` gains a public, reflected `showAllColumns` property/`show-all-columns` attribute for its reveal-hidden-columns state, plus a `lr-columns-revealed` event fired when `[part='reveal-columns-button']` toggles it. Consumers can now read the current reveal state back (to persist it) and set an initial one (to restore a previously-persisted preference), mirroring the read-back/set-forward contract `sortKey`/`sortDir` already support. The button still toggles the state itself by default, so existing usage is unaffected.

## 2.7.0

### Minor Changes

- af61856: `lr-app-rail`'s navigation landmark (and its `role="dialog"` while the mobile overlay is open) now honors a host-level `aria-label` attribute, taking precedence over the `label` property and its localized `"Navigation"` default, mirroring `<lr-date-input>`'s `accessibleLabel` pattern. Previously a host-level `aria-label` on `<lr-app-rail>` had no effect on the accessible name computed inside its shadow DOM.
- 4ee4e76: `lr-chat-composer` forwards `spellcheck`/`autocapitalize`/`autocorrect` onto its internal `<textarea>` and re-dispatches bubbling, composed `blur`/`focus` events so a host-level listener can observe focus changes across the shadow boundary.
- 06e5fda: `lr-chip` gains a `--lr-chip-pressed-bg` custom property (falls back to `--lr-chip-bg`) so the pressed/selected background can be set independent of the resting background. A toggleable-but-unpressed chip now announces `aria-pressed="false"` instead of omitting the attribute entirely, matching the ARIA Authoring Practices convention for toggle buttons.
- a158b6b: `lr-combobox` gains a `size` property (`'xs'|'s'|'m'|'l'|'xl'`, default `'m'`) mirroring `lr-select`'s existing scale, including matched sizing for the "+N" overflow tag so it stays visually consistent with the trigger at every size. Async `ComboboxSourceRow` results can now carry a decorative `icon`, trailing `badge`, richer `accessibleLabel`, and opaque `data`; the read-only `selectedRows` getter retains the structured rows and payloads for the current selection. The new visuals are exposed through `option-icon` and `option-badge` CSS parts.
- 480d9e2: `lr-conversation-item` forwards `spellcheck`/`autocapitalize`/`autocorrect` onto its in-place rename `<input>` and re-dispatches bubbling, composed `blur`/`focus` events so a host-level listener can observe focus changes across the shadow boundary while a rename is in progress.
- 74dcaa7: `lr-date-input` forwards `spellcheck`/`autocapitalize`/`autocorrect` onto its internal `<input>` and re-dispatches bubbling, composed `blur`/`focus` events so a host-level listener can observe focus changes across the shadow boundary.
- 22f206c: `lr-dialog` now lets a host-level `aria-label` attribute win over its computed accessible name (a slotted heading, `heading`, or `label`), matching `<lr-date-input>`'s `accessibleLabel` pattern. Previously a consumer setting `aria-label` directly on `<lr-dialog>` was silently ignored in favor of the bespoke `label`/`heading` props. Additive — left unset, today's existing three-tier fallback is unchanged.
- 80b22ba: `lr-empty`'s `compact` mode gains a `--lr-empty-compact-align` custom property (defaulting to today's exact `flex-start`/`start` pair) so a consumer can combine `compact`'s denser padding with a centered heading/description layout by setting it to `center`.
- 0f21c57: `lr-export-button` accepts custom format descriptors with consumer-supplied labels, descriptions, and extension metadata. Custom formats emit `lr-export` for application handling without bundling an encoder, while a new controlled `loading` state exposes busy semantics and prevents duplicate activation during async exports.
- 3ac5e4d: `lr-gauge` gains a full-circle `type="ring"` presentation and a `--lr-gauge-fill` custom property for setting the fill stroke per instance across radial, ring, and linear gauges.
- f6b2aa5: `lr-graph` nodes gain independent accessible labels and SVG tooltip descriptions. Links gain stable ids, spoken-name/tooltip relationship-label fallbacks (not visible edge text), tooltip descriptions, directed arrowheads, per-link colors, and dash patterns; `lr-link-click` now includes the optional link id and the marker is exposed through the `arrowhead` CSS part. A host `aria-label` is forwarded to the internal semantic SVG.
- efc1182: `lr-map` now forwards a host-level `aria-label` attribute onto `[part="base"]`'s accessible name as a fallback when `label` is left unset, matching `lr-slider`/`lr-checkbox`/`lr-switch` — previously a host `aria-label` was silently dropped in favor of the localized `'map'` default.
- 085d173: `lr-mention-popover` now honors a host-level `aria-label` attribute as the accessible name for its internal `role="listbox"` popup, taking priority over the `label` property and its localized default. Previously the popup's name came only from `label`/`localize()`, so a plain `aria-label` set on `<lr-mention-popover>` itself was silently ignored — matches the same fallback already used by `lr-combobox`/`lr-table`.
- 3b59e94: `lr-menu`'s `role="menu"` popup now honors a host-level `aria-label` attribute over both the `label` prop and its localized default, matching `lr-select`/`lr-model-select`'s established `this.getAttribute('aria-label') || <computed default>` precedence. Additive — `aria-label` is unset by default, so every existing consumer (whether relying on the default `"Menu"` text or an explicit `label` prop) renders byte-identical to before.
- 653173d: `lr-model-select` gains an opt-in `hint`/`error-text` form-control chrome (matching named slots and `hint`/`error` CSS parts, mirroring `lr-select`, with `aria-describedby` wired to the rendered ids), plus `spellcheck`/`autocapitalize`/`autocorrect` passthrough and bubbling `blur`/`focus` events on the free-text mode's internal `<input>`. All additive — a bare `<lr-model-select>` with none of these set renders byte-identical to before.
- 992b0ba: Add `lr-pagination`, a controlled, localized page-navigation component with previous/next controls, a validated numeric page jump, range summaries, applied-page announcements, loading/empty handling, RTL-aware icons, five sizes, and container-responsive stacking. Enrich `TreeItem` rows with optional `icon`, `description`, and `accessibleLabel` fields plus matching structured CSS parts while preserving the existing tree keyboard model.
- dfb2f5e: Add `lr-phone-input`, a form-associated country/telephone field that keeps canonical form values in E.164 while preserving partial editable input. Numbering metadata stays opt-in through an injected adapter or the consumer-loaded `loadLibphonenumberAdapter()` helper; `libphonenumber-js` is an optional peer and international E.164 input works without a formatter.
- d88377a: `lr-switch` gains an opt-in `hint`/`error-text` form-control chrome (props + matching named `hint`/`error` slots + CSS parts), mirroring `lr-select`'s pattern for those two pieces, with `aria-describedby` wired to whichever are rendered. Left unset, neither renders and the control is unchanged. The default slot stays the control's visible, clickable label (same as `lr-checkbox`) — no separate top-of-field `label` prop was added.
- c8709cd: `lr-textarea` gains optional label/hint/error chrome, accessible-name forwarding, bounded auto-resize, editing-assistance attributes, public native-input and selection/caret APIs, synchronized `setRangeText()`, and bubbling composed focus/blur events. Existing visual and behavioral defaults remain unchanged when the new options are unused.
- fca0ffb: `lr-tool-approval-dialog`'s raw-JSON args `<textarea>` now also hardcodes `autocapitalize="off"` and `autocorrect="off"` alongside its existing `spellcheck="false"`, so a mobile browser (notably iOS Safari, which defaults textarea `autocapitalize` to `'sentences'`) can no longer auto-capitalize or auto-correct JSON key/value text while a user edits tool-call arguments, silently corrupting the JSON.
- 5b9b056: `lr-tree` now forwards a host-level `aria-label` attribute onto the internal `role="tree"` element's accessible name as a fallback when `label` is left unset, matching `lr-slider`/`lr-select` — previously a host `aria-label` was silently dropped since `role="tree"` lives on an internal element, not the host.
- 12595bd: `lr-typing-indicator`'s dots-variant stagger delays are now themeable via `--lr-typing-dot-stagger-1`/`-2` (defaulting to today's exact `600ms`/`1200ms`), so a consumer retiming `--lr-transition-ambient` can keep the stagger proportional.

## 2.6.0

### Minor Changes

- 78d4b58: `lr-chat-message` gains an `attachments-position` prop (`'before' | 'after'`, default `'after'`) so the `attachments` slot can render above the message body instead of below it, keeping DOM/visual/reading order in sync.
- a072af9: `lr-chip` gains a `--lr-chip-pressed-border` custom property so a consumer can set the pressed/selected border color independent of `--lr-chip-accent` (which also drives the label text color). Falls back to `--lr-chip-accent`, so existing consumers are unaffected.
- b56bdb2: `lr-empty` gains a `--lr-empty-compact-padding` custom property to override `compact`'s fixed uniform padding (e.g. with an asymmetric shorthand like `8px 2px`). Falls back to `var(--lr-space-xs)`, today's exact value.
- e029ac2: `lr-heatmap` calendar mode gains a `weekdayLabelText?: (jsWeekday: number) => string | undefined` hook to override the weekday-axis label text (e.g. for a consumer with its own locale/translation state independent of the browser's runtime locale).
- 6d5f9c4: Add `lr-textarea`, a bare multiline plain-text input primitive (value/rows/resize/placeholder, form-associated validation) — the `lr-*` equivalent of a plain `wa-textarea`.
- bbe8007: `lr-segmented`'s `SegmentedItem` gains an optional `icon` field, rendered before the item's label.
- e98013a: `lr-table`'s `TableColumn` gains a `headerCell` render hook (mirroring `cell`/`footer`) and `width`/`minWidth` fields. Any column defining `width` switches the table to `table-layout: fixed` so widths are authoritative.
- 993809a: `lr-widget` gains a `backdrop-inset` prop to decouple the fullscreen backdrop's inset from the panel's own `fullscreen-inset`. Falls back to `fullscreen-inset`, so existing consumers are unaffected.

### Patch Changes

- 1c78bd2: Fix `lr-poll-status`, `lr-typing-indicator`, and `lr-stream-status`'s ambient "still alive" pulse/bounce animations, which reused `--lr-transition-base` (180ms — reserved for discrete UI micro-interactions) and rendered as a fast flicker instead of a calm breathing loop. Adds a dedicated `--lr-transition-ambient` token (1.8s) for infinite looping indicators.
- e029ac2: Fix `lr-heatmap`'s `cellColor` hook silently rendering solid black when it returns a CSS custom property or other non-literal color (e.g. `color-mix(...)`) — the value is now resolved via a cached, hidden probe element before being assigned to the canvas `fillStyle`.
- 600544f: Fix `lr-skeleton` rendering as an invisible 0×0 box everywhere: `[part='base']` was a bare `<span>` (UA default `display: inline`), so its own `inline-size`/`block-size` were CSS no-ops per spec. Adds `display: block`.

## 2.5.0

### Minor Changes

- 84cefde: `lr-attachment-trigger`'s single-capability trigger `aria-label`s ("Attach files"/"Attach an
  image"/"Use camera"), its multi-capability menu's "Add attachment" label/aria-label, and its menu
  item labels ("Upload files"/"Upload a photo"/"Take a photo") now route through `this.localize()`,
  overridable via `.strings`/`registerLyraLocale()`. Default English output is unchanged when no
  override is set.
- 6bf30ea: `lr-avatar` now accepts default-slotted icon/glyph content (e.g. an inline SVG), shown in place of
  the image/initials and taking priority over both `src` and `initials` — useful for a chat UI
  distinguishing an "AI" avatar from a "user" avatar by role glyph rather than a photo or initials. Set
  `alt` alongside the icon for an accessible name, since the glyph itself is treated as decorative.
- 87890ea: `lr-checkbox`'s built-in required-field validation message ("Please check this box if you want
  to continue.") now routes through `this.localize()`, overridable via `.strings`/
  `registerLyraLocale()`. Default English output is unchanged when no override is set.
- b720eda: Fixed `lr-chip`'s opt-in `selected` toggle/pressed mode so it stays interactive after the first
  click. `[part='base']`'s `role="button"`, `tabindex`, `aria-pressed`, and click/keydown handlers
  used to be gated on the _current_ value of `selected`, so a chip that started `selected` and was
  clicked (flipping it to `false`) lost its focusable/clickable semantics on the next render — there
  was no way to click it back on. `selected` becoming `true` at any point now latches the chip into
  toggle mode for good, so it stays clickable in both directions. A chip that must be interactive
  from the outset while starting **unselected** (e.g. an initially-inactive filter chip) can opt in
  explicitly with the new `toggleable` property, since `selected`'s own default (`false`) can't be
  told apart from "never opted in" on its own.
- cbfec47: `lr-citation-badge`'s visible status words folded into its computed accessible name ("High
  confidence"/"Medium confidence"/"Low confidence"/"Verified"/"Unverified") now route through
  `this.localize()`, overridable via `.strings`/`registerLyraLocale()`. Default English output is
  unchanged when no override is set.
- dba57e9: `lr-context-meter`'s accessible summary ("{used} of {total} used" / "{used} used") now routes
  through `this.localize()`, overridable via `.strings`/`registerLyraLocale()`. Default English
  output is unchanged when no override is set.
- 7379a41: `lr-conversation-item`'s "Untitled conversation" fallback title now routes through
  `this.localize()`, overridable via `.strings`/`registerLyraLocale()`. Default English output is
  unchanged when no override is set.
- acdaa37: `lr-dock-panel`'s resize-handle and collapse-toggle `aria-label`s ("Resize panel",
  "Collapse panel"/"Expand panel") now route through `this.localize()`, overridable via
  `.strings`/`registerLyraLocale()`. Default English output is unchanged when no override is set.
- eca2ea4: `lr-document-preview`'s hardcoded English strings — the image-preview `alt` fallback
  ("Document preview"), the unsafe-URL error ("Document URL is not allowed."), the non-`Error`
  fetch-failure message ("Failed to load document."), and the empty-`error-message` fallback
  ("Something went wrong.") — now route through `this.localize()`, overridable via
  `.strings`/`registerLyraLocale()`. Its in-flight text-fetch spinner label ("Loading document…")
  is now also wired through the existing `loadingDocument` message key. Default English output is
  unchanged when no override is set.
- a3c4ebf: `lr-export-button`'s trigger button text (default "Export", also reused for the format menu's
  `aria-label`) now routes through `this.localize()` when `label` is left at its built-in default,
  overridable via `.strings`/`registerLyraLocale()` — matching `lr-attachment-chip`'s
  `removeLabel`/`retryLabel` convention. Setting the `label` attribute/property explicitly still
  overrides it directly. Default English output is unchanged when no override is set.
- df8341b: `lr-generation-status`'s stop-button `aria-label` ("Stop generating") now routes through
  `this.localize()` (sharing the existing `stopGenerating` key used elsewhere in the library), and
  the tokens segment's singular/plural noun ("token"/"tokens") is now localizable too, matching
  `lr-json-viewer`'s/`lr-word-cloud`'s existing count-noun pattern. Overridable via
  `.strings`/`registerLyraLocale()`. Default English output is unchanged when no override is set.
- 20ae3e7: `lr-graph`'s visually-hidden data-list `aria-label` ("Graph data") now routes through
  `this.localize()`, overridable via `.strings`/`registerLyraLocale()`. Default English output is
  unchanged when no override is set.
- 8c29581: `lr-segmented` gains a `label` property giving its `role="radiogroup"` root an accessible name.
  When unset, a plain `aria-label` attribute on the host element is honored as a fallback, matching
  `lr-slider`'s existing `label`/`aria-label` convention. Previously the radiogroup had no way to
  receive an accessible name at all.
- 259c0c6: Completed a full-library i18n/RTL/styling standardization pass across the remaining component
  families not yet covered by earlier rounds — `chart` (and `box-plot`/`histogram`/`lite-chart`),
  `avatar`, `code-block`, `combobox`, `date-picker`, `dialog`, `document-preview`, `export-button`,
  `file-input`, `graph`, `heatmap`, `map`, `time-range`, `tool-call-chip`, `tool-param-form`,
  `tool-result-dialog`, `tree`, `widget`, and several smaller components. Highlights:

  - Routed remaining hardcoded English strings (accessible descriptions, aria-labels, empty-state
    text) through `this.localize()`.
  - Fixed RTL gaps: `date-picker`'s previous/next chevrons now mirror under `dir="rtl"` (rotating
    the wrapping `part`, not the icon), matching the grid's own arrow-key swap.
  - `lr-avatar`: fixed a dangling `--lr-color-surface-alt` token reference, corrected its `size`
    JSDoc, and extended the accessible-name role/`aria-label` to the initials-fallback path (not
    just the icon-slot path) whenever `alt` is set.
  - `lr-export-button` now fires `lr-show`/`lr-hide` on its format menu, matching the same
    convention already used by `lr-menu`/`lr-select`/`lr-combobox`.
  - Fixed a `this.localize(key, literalFallback)` pattern that unconditionally short-circuited
    `registerLyraLocale()` lookups for the affected keys (the fallback is now omitted wherever
    `DEFAULT_STRINGS` already carries the same default).

  AGENTS.md gained a new "Internationalization (i18n), RTL, and theming" section documenting the
  resulting standard, and both READMEs now summarize it for consumers.

- 79e4390: Fixed gaps found during a full re-verification pass over previously-completed work:

  - `lr-menu`'s type-ahead navigation now excludes `hidden`/`aria-hidden` items (it already
    excluded `disabled` ones), matching the Arrow/Home/End roving-focus navigation it sits next to.
  - The root barrel (`src/lyra.ts`) now re-exports 13 component event-map types that were previously
    unreachable from the package root even though their owning classes were exported: `LyraChip`,
    `LyraChipGroup`, `LyraCitationBadge`, `LyraCopyButton`, `LyraDiffView`, `LyraFileInput`,
    `LyraHeatmap`, `LyraLiteChart`, `LyraMediaCard`, `LyraSelect`, `LyraSourceCard`, `LyraSplit`, and
    `LyraTimeRange`'s `*EventMap` types are now all importable from `@aceshooting/lyra-ui`.

- 59d4477: `lr-media-card`'s hardcoded English fallback strings — the file-chip "Untitled file" name, the
  `image`/`video` alt-text fallbacks ("Image attachment"/"Video attachment"), and the accessible
  "Open …" label (both the named and generic-kind forms) — now route through `this.localize()`,
  overridable via `.strings`/`registerLyraLocale()`. Default English output is unchanged when no
  override is set.
- ea774a8: `lr-mention-popover`'s default listbox accessible name ("Suggestions") now routes through
  `this.localize()`, overridable via `.strings`/`registerLyraLocale()` — matching the already-shared
  `noMatches` key its empty-state row uses. An explicit `label`/`empty-text` value still wins
  verbatim. Default English output is unchanged when no override is set.
- cd10606: `<lr-menu>` gains an opt-in `closeOnEscapeAnywhere` property. Escape has always closed the menu
  and refocused the trigger when it originates from a real `<lr-menu-item>`, but slotted non-item
  content (e.g. a form control slotted alongside the items) previously got full default keyboard
  behavior with no way to close the menu on Escape. Setting `closeOnEscapeAnywhere` extends that
  same Escape-closes-and-refocuses behavior to keydowns from anywhere in the list, including slotted
  non-item content. Defaults to `false`, so existing consumers are unaffected.
- 7d63af9: `lr-menu`'s `role="menu"` popup default accessible name ("Menu") now routes through
  `this.localize()`, overridable via `.strings`/`registerLyraLocale()`. An explicit `label` value
  still wins verbatim. Default English output is unchanged when no override is set.
- f232381: `lr-model-settings-panel`'s hardcoded English strings — the visible "Temperature" caption
  (also reused as the nested `lr-slider`'s accessible name) and the internal `lr-model-select`'s
  "Select a model…" placeholder — now route through `this.localize()`, overridable via
  `.strings`/`registerLyraLocale()`. Default English output is unchanged when no override is set.
- 1686322: `lr-playback`'s play/pause button and position-slider `aria-label`s ("Play"/"Pause",
  "Playback position") now route through `this.localize()`, overridable via
  `.strings`/`registerLyraLocale()`. Default English output is unchanged when no override is set.
- 0cacb4d: `lr-poll-status`'s pause/resume button aria-label, due-state countdown text ("Refreshing…"), and its
  three live-region announcements ("Paused."/"Resumed."/"Refreshing now.") now route through
  `this.localize()`, overridable via `.strings`/`registerLyraLocale()`. It also now shows a distinct
  "Paused" countdown state while `paused`, instead of freezing on whatever value it last displayed.
  Default English output is unchanged when no override is set.
- 870523f: `lr-widget` gains two new named slots, `collapse-icon` and `fullscreen-icon`, overriding the
  built-in chevron/expand-or-close glyphs on the collapse and fullscreen toggle buttons entirely
  (platform slot-fallback-content mechanism: whatever is assigned wins, otherwise the default glyph
  renders unchanged). `WidgetView`'s `label` is now optional and a new `ariaLabel` field lets a view
  toggle be icon-only while still exposing an accessible name — previously a toggle with no `label`
  had no accessible name at all.
- c2bc232: Align every component with the library's i18n/RTL/theming standard and fix the remaining
  gaps:

  - Removed several `this.localize(key, literalFallback)` call sites (`toolApprovalHeading`,
    `playback`'s play/pause/position labels, `model-settings-panel`'s temperature/model labels,
    `media-card`'s five accessible-name strings, `kbd`'s shortcut-token labels, `chat-composer`'s
    composer label) where the literal fallback silently defeated `registerLyraLocale()` translation
    for that call site.
  - Routed remaining hardcoded strings through `this.localize()`: `date-picker`'s next-month label
    and `date-input`'s validation messages, `toast-item`'s/`chip`'s/`combobox`'s remove/close
    labels (now interpolated via a `{placeholder}` instead of string concatenation), `heatmap`'s
    matrix/calendar aria-labels and "no data"/row/col fallbacks, `chart`/`box-plot`'s description
    and data-table text, `lite-chart`'s mark-position announcement, `document-preview`'s empty-state
    nouns, `json-viewer`'s copy/expand/collapse/count labels, `stat`'s trend announcement,
    `dialog`'s `confirm()` cancel button, `typing-indicator`'s default label, `tool-param-form`'s
    edge-case validation message, and `tool-result-dialog`/`tool-call-chip`'s duration seconds unit.
  - Fixed RTL gaps: `app-rail-item`'s icon tooltip now flips side under `dir="rtl"` via
    `rtlAwarePlacement()`, `chat-message`'s and `source-list`'s collapse/disclosure chevrons now
    mirror under RTL, and `lite-chart`'s roving-tabindex point navigation now swaps
    ArrowLeft/ArrowRight under RTL.

  Also compressed the shared string registry (`internal/localization.ts`): removed 21 `kbd*` base
  keys (`kbdEnter`, `kbdEscape`, `kbdTab`, etc.) that were fully superseded by their `*Word`/`*Visual`
  counterparts and had no remaining call sites anywhere in the library, reducing the packed consumer
  bundle size.

- aeef118: `lr-select`'s required-field validation message ("Please select an option.") and its
  trigger's fallback accessible name ("Select", used only when no `aria-label`, `label`, or
  `placeholder` is set) now route through `this.localize()`, overridable via
  `.strings`/`registerLyraLocale()`. Default English output is unchanged when no override is set.
- 4fb27a2: `lr-skeleton`'s default accessible name ("Loading…") now routes through `this.localize()`
  (reusing the shared `loading` key), overridable via `.strings`/`registerLyraLocale()`. An
  explicit `label` still wins verbatim. Default English output is unchanged when no override is set.
- f7b9f0e: `lr-source-list`'s fallback header text ("Sources", used only when neither `label` nor
  `label-plural` is set) now routes through `this.localize()`, overridable via
  `.strings`/`registerLyraLocale()`. Default English output is unchanged when no override is set.
- f2ea145: `lr-stepper`'s `StepItem` gains an optional `title` field, rendered as a native `title` tooltip on
  that step's button — useful for explaining why a `disabled` step is locked (e.g. "Complete Basics
  first"). Steps that omit it render no `title` attribute at all, unchanged from today.
- 9e5864a: `lr-stream-status`'s built-in stalled-message default ("Taking longer than usual…") and its
  three live-region announcements ("Connection stalled."/"Connection restored."/"No longer
  stalled.") now route through `this.localize()`, overridable via `.strings`/`registerLyraLocale()`.
  Default English output is unchanged when no override is set.
- 9174500: `lr-switch`'s built-in required-field validation message ("Please turn this on.") now routes
  through `this.localize()`, overridable via `.strings`/`registerLyraLocale()`. Default English
  output is unchanged when no override is set.
- 60084ba: `lr-thinking-panel`'s default header label ("Thinking") and its duration-display text ("Thought
  for …"/"Thinking…") now route through `this.localize()`, overridable via
  `.strings`/`registerLyraLocale()`. An explicit `label` still wins verbatim. Default English
  output is unchanged when no override is set.
- b113bda: `lr-tool-approval-dialog`'s heading text, generic tool-name fallback, args-editor accessible
  name, invalid-JSON fallback error, and its Deny/Edit/Approve button labels now route through
  `this.localize()`, overridable via `.strings`/`registerLyraLocale()`. Default English output is
  unchanged when no override is set.
- 3b1f930: `lr-tool-call-chip`'s visible status labels (Pending/Running/Success/Error/Denied, shared with
  `lr-tool-result-dialog`'s identical vocabulary) and its unnamed-tool fallback ("Tool call") now
  route through `this.localize()`, overridable via `.strings`/`registerLyraLocale()`. Default English
  output is unchanged when no override is set.
- bbaea80: `lr-tool-param-form`'s validation messages (required field, wrong type for a string/number/
  integer/boolean, enum mismatch, const mismatch, unsupported field type, malformed schema shape,
  non-serializable value) now route through `this.localize()`, overridable via
  `.strings`/`registerLyraLocale()`. Default English output is unchanged when no override is set.
- bda19ac: `lr-tool-select-dialog`'s dialog title, search placeholder, "use default tools" switch label
  and hint, category count/"Other" fallback, tools-enabled summary, no-matches message, and the
  no-tools-available empty state now route through `this.localize()`, overridable via
  `.strings`/`registerLyraLocale()`. Default English output is unchanged when no override is set.
- 220bd73: `lr-widget`'s collapse/expand, exit-fullscreen/expand-to-fullscreen, and view-toggle-group
  aria-labels, plus its fullscreen dialog's fallback accessible name, now route through
  `this.localize()`, overridable via `.strings`/`registerLyraLocale()`. The collapse/expand labels
  reuse `lr-dock-panel`'s existing `dockPanelCollapse`/`dockPanelExpand` keys. Default English
  output is unchanged when no override is set.

### Patch Changes

- 00ce49f: Fix `lr-date-picker`'s day-grid keyboard navigation to swap ArrowLeft/ArrowRight under `dir="rtl"`, matching the grid's own visual mirroring (the day cells use unset `direction`, so the browser already lays them out right-to-left). ArrowUp/ArrowDown (by week) are unaffected.
- 37e1a2f: `lr-table`'s header-cell ArrowLeft/ArrowRight roving-tabindex navigation now derives its RTL
  check through the shared `isRtl()` helper instead of a duplicated inline `getComputedStyle`
  check, and gains test coverage confirming ArrowRight/ArrowLeft already swap correctly under
  `dir="rtl"` (a native `<table>` mirrors column visual order under RTL on its own) while
  ArrowUp/ArrowDown row navigation is unaffected. No behavior change.
- 2fd3786: Fix calendar-heatmap weekday-axis labels to respect firstDayOfWeek instead of always labeling grid rows 1/3/5.

## 2.4.0

### Minor Changes

- 171bdbd: `lr-attachment-chip`'s file-size unit abbreviations ("B"/"KB"/"MB"/"GB"/"TB") now route through
  `this.localize()` when rendered, overridable via `.strings`/`registerLyraLocale()`. The exported
  `formatFileSize()` pure function gains an optional `unitLabel` resolver parameter, defaulting to the
  plain English abbreviation — every existing single-argument call is unaffected.
- 5f043ba: `lr-chart`'s data-table "Category" column header, per-row "Point N" fallback label, and "Reset
  zoom" button text now route through `this.localize()`, overridable via `.strings`/
  `registerLyraLocale()`. Default English text is unchanged.
- 5e90140: `lr-chat-composer`'s action button labels ("Send message"/"Stop generating") now route through
  `this.localize()`, overridable via `.strings`/`registerLyraLocale()`. Also adds `stoppable: boolean =
true` — when set to `false`, the button never renders as a Stop/cancel control while busy; it stays a
  disabled Send button instead, for backends with no cancellation endpoint. Default behavior is
  unchanged.
- 558e76c: `lr-chat-message`'s visible status text ("Sending…"/"Responding…"/"Failed to send") and its two
  live-region status-change announcements ("Message failed to send."/"Message complete.") now route
  through `this.localize()`, overridable via `.strings`/`registerLyraLocale()`. Default English text is
  unchanged.
- 238c8d7: `lr-chip-group`'s collapsed overflow-indicator's visible "+N" text now routes through
  `this.localize('showMoreCollapsed', ...)`, matching the aria-label it sits beside, which was already
  localized. Default English output ("+N") is unchanged.
- 0d9018f: `lr-code-block`'s collapse-toggle, copy-button, and code-region aria-labels now route entirely
  through `this.localize()` instead of concatenating a localized verb with a hardcoded English suffix
  ("code"/"to clipboard"/"Code"). Default English output is unchanged.
- a249bd6: `lr-diff-view`'s copy-button aria-label now routes entirely through `this.localize('copyDiff', ...)`
  instead of concatenating the localized "copy" verb with a hardcoded " diff" suffix. Default English
  output ("Copy diff") is unchanged.
- 58c6e59: `lr-file-input`'s drag-preview live-region announcements ("Release to add the file." / "This file
  type is not accepted.") now route through `this.localize()`, overridable via `.strings`/
  `registerLyraLocale()`. Default English text is unchanged. The post-drop `acceptedMessage`/
  `rejectedMessage` properties and the visible `label` property are unaffected (already
  consumer-overridable).
- b3e3bb6: `lr-json-viewer`'s root-node toggle/copy fallback words ("array"/"object"/"value", used only when a
  node has no key label) now route through `this.localize()`, overridable via `.strings`/
  `registerLyraLocale()`. Default English text is unchanged.
- b322e75: `lr-model-select`'s synthetic stale-value row badge ("not in catalog") now routes through
  `this.localize('notInCatalog')`, so it can be overridden via `.strings`/`registerLyraLocale()` like
  the component's other built-in message (`noMatches`). Default English text is unchanged.
- e54eeee: `lr-source-card`'s "Untitled source" fallback and its " — p. N" page-suffix format now route
  through `this.localize()`, overridable via `.strings`/`registerLyraLocale()`. Default English output
  is unchanged.
- 0576643: `lr-split` now redistributes the track space freed when a `panelConstraints` pixel bound clamps a
  panel's percentage basis down (e.g. a `maxPx` cap on a wide viewport) to sibling panels that have no
  pixel constraint of their own, instead of leaving that space unused. No behavior change for splits
  without `panelConstraints`, or where no panel is actually clamped this render.
- 97756af: `lr-table`'s `columns[].sticky` option now accepts `'start' | 'end'` in addition to the legacy
  `boolean` (`true` continues to mean `'start'`, unchanged). An `'end'`-sticky column pins to the
  inline-end edge instead — useful for a trailing actions column that would otherwise be pushed off
  a narrow viewport — via the same `inset-inline-*` logical-property approach, so RTL is unaffected.
- ffee803: `lr-tool-result-dialog`'s tool-name fallback ("Tool call"), visible status label
  ("Pending"/"Running"/"Success"/"Error"/"Denied"), and maximize/restore button aria-label now route
  through `this.localize()`, overridable via `.strings`/`registerLyraLocale()`. Default English output
  is unchanged.
- f9f57f9: `lr-word-cloud`'s default aria-label's pluralized "word"/"words" noun now routes through
  `this.localize()` too, so a registered translation of the `wordCloud` template's `{word}` slot is no
  longer stuck in English. Default output is unchanged.

## 2.3.0

### Minor Changes

- a1b2f8e: `lr-app-rail` gains `dragging` (reflected boolean, true for the duration of a pointer-driven
  resize -- not a keyboard step -- so its own `[part='base']` transition suppresses during the drag
  instead of visibly "chasing" the pointer) and `hideToggle` (suppresses the built-in mobile hamburger
  button for a consumer that owns its own external toggle wired to `open`).
- e9075b8: `lr-app-rail-item` gains an opt-in `tooltip` property: a hover/focus flyout showing the item's
  label text while `icon-only` hides it from view, using the library's existing Floating-UI-backed
  `place()` positioner -- an explicit, documented alternative to hand-rolling a `::part()`+`::after`
  tooltip composition.
- 8160548: `lr-attachment-chip`'s `compact` variant now also shrinks font-size and gap (via new
  `--lr-attachment-chip-compact-font-size`/`-compact-gap` custom properties), not just
  border/radius/padding/thumbnail-size. Also adds `thumbnailOnly`, which -- combined with `compact`
  on an image-mime chip -- hides the filename/size text entirely for a pure thumbnail density with
  no consumer-side CSS.
- 099fa8a: Add `lr-avatar`: a small, fixed-size identity marker (image, or an initials fallback) for a
  user-menu trigger or similar identity affordance -- `size`/`shape`/`tone` variants mirror
  `lr-chip`'s existing tone vocabulary for consistency.
- bf9d442: Add `lr-card`: a generic bordered content container (`appearance` variants mirroring `wa-card`,
  `header`/`media`/`footer`/`actions` slots) for the "small bordered surface with padding" idiom
  common across hero highlights and clickable grid tiles -- a real `lyra-ui` parity counterpart to
  `wa-card`, which this library otherwise mirrors 1:1.
- f9ecffd: `lr-chip` gains an opt-in `selected`/pressed interactive mode: `[part='base']` becomes
  keyboard-activatable and reflects `aria-pressed`, toggling on click/Enter/Space and emitting
  `lr-chip-select`. Not combinable with `removable` (avoids a nested-interactive a11y violation);
  today's passive-label-pill usage is unaffected since `selected` defaults to `false`.
- db24359: Add `lr-code-block-core`: a build-lean variant of `lr-code-block` for a consumer whose
  `languages` map already covers every language it renders. Unlike `languagesOnly` (a runtime flag
  on `lr-code-block` itself, which a bundler can't prove always-true and so can't tree-shake),
  `lr-code-block-core` is a genuinely separate module that never references shiki's full
  ~200-language default entry point at all -- importing it instead of `code-block.js` gives a real
  compile-time exclusion of that table from the build output.
- 83ba36c: `lr-dialog` gains `--lr-dialog-width`, unset by default -- when set, the panel actually
  stretches to that width instead of only shrink-wrapping its content capped at
  `--lr-dialog-max-width`, which was a real gotcha for anyone porting from `wa-dialog`'s
  assertive `--width` token.
- a1d7030: Add `lr-diff-view`: a real two-string line diff (LCS-aligned), rendered as interleaved
  unified-diff output -- unlike diff-flavored syntax highlighting over an already-formatted string,
  this computes the alignment itself, so a one-line change inside a longer block renders as one
  red/green pair near the change instead of every old line then every new line.
- b56abda: `lr-empty`'s `heading`/`description` gain the same slot-override-attribute treatment
  `lr-stat`'s `caption`/`sub` already have -- a consumer can now pass rich mid-sentence content
  (e.g. an inline `<code>` reference) while the plain-string attribute stays the default.
- 4324a73: `lr-graph` now renders a link whose `target` isn't a real node as a short dashed stub off the
  source's position, instead of silently dropping it -- for a wiki-style `[[link]]`/broken-reference
  visualization where "this edge exists but its endpoint doesn't" is a meaningful state, not noise.
  A dangling `source` is still dropped (no position to draw a stub from).
- 1e71d71: Rewrite `lr-heatmap`'s two weekday-axis-label tests to assert against independently fixed dates
  instead of re-deriving the implementation's own formula, which could never fail regardless of
  correctness -- the underlying `weekdayLabels()`/`firstDayOfWeek` anchoring was already correct.
  Also add `cellColor`, an optional per-cell color override function (mirroring the existing
  `cellText`/`cellInteractive` shape) that bypasses the color ramp entirely for an exact value.
- 2e74ea0: Fix `lr-lite-chart`'s `minBarHeight` z-order bug for stacked bars: a floored near-zero segment
  was being overdrawn by the segment stacked on top of it, since each segment's position was derived
  independently from cumulative value rather than from where the previous (possibly-floored) segment
  actually ended on screen. Also add `selectedIndex: number[]`, reflecting `data-selected` onto every
  bar at a given category index across all datasets, for highlighting a whole selected column.
- 00f3b37: `lr-markdown` gains `escapeHtml`, an opt-in property overriding `marked`'s `html` renderer hook
  to emit escaped text instead of parsed/sanitized markup -- for a consumer rendering arbitrary
  already-written content (transcripts, logs) where a stray angle bracket should render as visible
  text rather than a real DOM element, without giving up GFM tables/lists/etc.
- d3fbf36: Add `lr-poll-status`: a "next scheduled refresh" countdown with a built-in pause control -- a
  ticking M:SS display, a "Refreshing…" due state, and an internal live region announcing phase
  transitions, mirroring `lr-stream-status`'s own composition for a different concern (a scheduled
  interval, not transport/connection health).
- b5464bd: Add `lr-segmented`: a single-select button row with the WAI-ARIA APG `radiogroup` contract
  (role="radio", roving tabindex, automatic-activation Arrow/Home/End navigation) built in --
  "choose exactly one of N labeled options" is ubiquitous settings/filter-panel UI that otherwise
  gets hand-rolled without keyboard/ARIA semantics every time.
- 551f272: `lr-select` gains `--lr-select-trigger-height`, unset (auto) by default -- when a consumer sets
  it, the trigger resolves to exactly that height (both floor and cap) instead of only being
  floored by `--lr-select-trigger-min-height`, for pixel-matching a sibling form field in the same
  row without a blunt `::part(trigger){block-size:...}` override.
- 1fddbdc: Add `lr-stepper`: ordered multi-step wizard navigation (label + index, current/completed/
  locked/error state, click-to-jump, horizontal/vertical orientation). Fully data-driven and
  controlled -- like `lr-table`, it never mutates its own `steps` data, firing a cancelable
  `lr-step-select` event and leaving state updates to the host, so gating a jump behind an
  external validity check (e.g. "does the target step's data exist yet") is a normal listener, not a
  workaround.
- 60dbf18: `lr-table` gains two per-column hooks: `footer(rows)`, rendered in a real sticky-bottom
  `<tfoot>` (only when at least one column defines it) -- e.g. a totals row; and `cellStyle(row)`,
  applied via `styleMap` directly to the generated `<td>` -- e.g. a computed heat-tint background --
  which coexists safely with the existing sticky-column offset styling.
- 6ce5b87: Add a new `./testing` subpath exporting `installHappyDomFormAssociatedShims()` -- an opt-in,
  environment-guarded polyfill for `HTMLElement.prototype.attachInternals`, for a downstream
  consumer's own Vitest+happy-dom test suite (happy-dom has no `ElementInternals` implementation,
  and every form-associated `lr-*` component calls `attachInternals()` unconditionally in its
  constructor). Not used by this package's own tests, which already run against real browsers.
- 25254f2: `lr-widget` gains a leading `icon` slot, rich `label`/`sublabel` slot overrides (mirroring
  `lr-stat`'s `caption`/`sub` pattern), and a `views` property driving a built-in header toggle
  group plus one named slot per entry -- for a chart/table (or similar) toggle inside the same card
  chrome, so a consumer no longer has to hand-roll that shell around a bare default slot.

### Patch Changes

- 062f036: Fix `lr-attachment-trigger`'s internal hidden `<input type="file">` actually rendering as a
  visible, focusable-adjacent element in normal document flow — it now has `display: none` by
  default (and a new `hidden-input` CSS part, for the rare integration that needs to override that).
- 9094b39: Fix `lr-chart` losing a user's legend-toggled hidden-dataset state on every data-driven redraw --
  `draw()` now snapshots each dataset's `isDatasetVisible()` state before reassigning `chart.data` and
  restores it via `setDatasetVisibility()` afterward, since Chart.js's own dataset-object identity
  changes on every reactive update from a live-polling consumer.
- a413c8c: Fix `lr-chip-group`'s "+N"/"Show less" overflow toggle hardcoding English strings instead of using
  the library's own existing `localize()`/`strings` override mechanism, which every other component
  with translatable text already uses (including the identical `showMore`/`showLess` keys, already
  consumed by `lr-source-card`).
- 4010bc4: `lr-menu`'s `onListKeyDown` now ignores a keydown whose target isn't a real `<lr-menu-item>`,
  matching the same `instanceof LyraMenuItem` guard `onItemSelect`/`onListFocusIn` already use --
  previously it unconditionally intercepted Arrow/Home/End/Enter/Space/Escape/Tab from any keydown
  bubbling through `[part="list"]`, including from non-item slotted content (e.g. a custom-range
  date input), hijacking keystrokes meant for it. Note: Escape/Tab now also only close the menu when
  the event originates from a real item -- a slotted non-item control gets fully default keyboard
  behavior instead.
- a5a055f: Fix `lr-split`'s fixed-percent panels not reserving space for the auto-inserted divider between
  them, causing a deterministic `(panelCount - 1) * dividerWidth` container overflow in the default
  (uncollapsed) state. Panels now get a nonzero `flex-shrink` so they absorb the dividers' own width
  instead of the row overflowing.
- 18003f0: Fix `lr-stat`'s `[part='base']` not stretching to fill its host in a CSS Grid -- a stat tile with
  a longer `sub`/breakdown-rows line rendered visibly taller than its row-mates. `block-size: 100%` on
  `[part='base']` now matches the convention `lr-word-cloud`/`lr-context-meter` already use.
- 55c384e: Fix `lr-tabs`'s `tablist` part showing a phantom vertical scrollbar on a tablist with no
  vertically-overflowing content — `overflow-x: auto` alone can leave the y axis's computed overflow
  at `auto` too per the CSS overflow spec, which sub-pixel rounding can trip; `overflow-y: hidden` is
  now explicit, since the tablist is never meant to scroll vertically.

## 2.2.0

### Minor Changes

- ff41aba: `lr-app-rail`: add a `resizable` opt-in (drag + keyboard-steppable `[part="resizer"]` handle,
  `railWidthPx`/`minRailWidthPx`/`maxRailWidthPx`, `lr-rail-resize` event) for the `'full'` state's
  width; add `preferredMode` to manually prefer `'full'`/`'icon-only'` while the mobile breakpoint
  keeps tracking automatically; and fix the mobile toggle button's `aria-label` to use a proper
  `openNavigation` message key (consistent with the existing `closeNavigation` key) instead of
  concatenating a hardcoded `" navigation"` suffix onto a partially-localized string.
- 3b1a404: `lr-app-rail-item`: add an `active` property that reflects `aria-current="page"` onto the
  internal link/button, mirroring `lr-conversation-item`'s existing `active` pattern.
- 3b7a98b: `lr-attachment-chip`: fix the uploading progressbar/spinner's `aria-label` to actually use
  `uploadingLabel` (previously hardcoded, unlike the adjacent visible status text); add an
  `untitledLabel` override for the empty-name fallback; add a `compact` density variant.
- 49be9e4: `lr-attachment-trigger`: add a `triggerTitle` property forwarded to the internal trigger
  button(s)' native `title` (a sighted-mouse-user hover tooltip, distinct from `triggerLabel`'s
  `aria-label` role); reduce the internal `.trigger-button:hover` rule's specificity via `:where()`
  so a consumer's `::part(trigger):hover` override wins without needing `!important`.
- 4d04843: `lr-code-block`: add a `languagesOnly` opt-in that skips the default `loadShikiHighlighter()`
  call entirely, so a consumer whose `languages` map already covers every language it renders has no
  bundler-reachable path to shiki's full per-language dynamic-import table.
- 2968d7b: Add `lr-copy-button`: a standalone icon-only copy-to-clipboard button for a plain text `value`,
  with no positioning opinion of its own — for a consumer needing just the copy/checkmark-swap
  affordance without adopting `lr-code-block`'s or `lr-json-viewer`'s full content model.
- 49be9e4: `lr-dialog`: add `noLightDismiss` to opt out of backdrop-click dismissal, and make `close()`
  actually respect a `lr-dialog-close` listener's `preventDefault()` (the event is now genuinely
  `cancelable: true`) for every dismissal path — Escape, backdrop, the built-in close button, and a
  consumer's own `close()` call.
- 6958595: `lr-heatmap`: add a `cellInteractive` predicate to opt individual cells out of hit-testing and
  keyboard roving focus, and a `colorSteps` discrete-array ramp as an alternative to the 2-endpoint
  `--lr-heatmap-scale-lo`/`-hi` linear interpolation (governs both `mode`s and both `scale`
  values). Also adds test coverage confirming `firstDayOfWeek`'s calendar-mode weekday-axis labels
  are correct for a non-Sunday-first week (the underlying computation was already correct; only the
  test combining the two was missing).
- 2c6fc82: `lr-lite-chart`: add a `minBarHeight`/`min-bar-height` pixel floor for near-zero stacked
  segments, fix `scale="sqrt"` proportionality for stacked bars (previously compressed each
  segment's absolute cumulative stack position independently instead of the bar's total height
  split linearly by segment share), and add a `chartLabel`/`chart-label` override for the chart's
  auto-derived `aria-label`.
- e29b2f9: `lr-markdown`: add `part="paragraph"`, `part="list"` (both `<ul>` and `<ol>`), and
  `part="inline-code"` (bare inline codespans only, not a fenced code block's `<code>`, which
  already has its own `part="code-block"` wrapper) so a consumer's `::part()` CSS can reach plain
  text elements that previously had no themeable hook.
- 3b7a98b: `lr-split`: add a `dividerLabel` function property overriding the auto-inserted divider's
  hardcoded English `aria-label` template.

## 2.1.0

### Minor Changes

- 82a3419: `<lr-attachment-chip>`: added four label-override properties for i18n/locale — `removeLabel`/`retryLabel` (`remove-label`/`retry-label` attributes, the verb prefixed to the remove/retry buttons' `aria-label` ahead of the interpolated filename) and `uploadingLabel`/`uploadFailedLabel` (`uploading-label`/`upload-failed-label` attributes, the verb/phrase used in the visible uploading/error status text, keeping the live percentage interpolation intact for `uploadingLabel`). All four default to today's exact hardcoded English text (`'Remove'`, `'Retry'`, `'Uploading'`, `'Upload failed'`), so leaving them unset changes nothing for existing consumers.
- 82a3419: `<lr-attachment-trigger>`: added a `triggerLabel` property (`trigger-label` attribute) that overrides the single-capability trigger button's `aria-label`, which previously came unconditionally from the built-in `CAPABILITY_META` table (e.g. `'Attach files'`, `'Attach an image'`, `'Use camera'`). Lets a host localize the accessible name without forking the component. Unset (the default) preserves today's exact `CAPABILITY_META`-derived label for every capability.
- 82a3419: Add `<lr-code-block>` `languages`, a map of language id to an already-imported shiki grammar module (e.g. `import bash from 'shiki/langs/bash.mjs'`). When `language` matches a key in `languages`, highlighting for it is seeded from exactly that pre-supplied grammar via a fine-grained `createHighlighterCore()` highlighter (`code-loader.ts`'s new `loadShikiHighlighterCore()`), bypassing the default `loadShikiHighlighter()` singleton and its dynamic per-language `loadLanguage()` import entirely for that language — no loading skeleton either, since this path never waits on that singleton. shiki's main entry point (what the default path imports) bundles a dynamic `import()` per bundled language (~200 of them), since a bundler can't statically narrow which of those a `loadLanguage(lang: string)` call might request at runtime; `shiki/core`'s fine-grained API has no such table, so a consumer who pins its full, known language set this way gets a build output scoped to just those languages instead of shiki's entire bundled set. A `language` value absent from `languages` (or left unset, or when `languages` itself is unset) still falls back to the ordinary dynamic-import path unchanged — this is a partial, additive opt-in, not a replacement for it.
- 82a3419: Fixed 'confirm()''s own usage example to import from the granular subpath
  ('@aceshooting/lyra-ui/components/dialog/confirm.js') instead of the root barrel
  ('@aceshooting/lyra-ui') — following the root-barrel example as written previously pulled in the
  library's entire ~80-component side-effect-import chain into a consumer's eager bundle
  (confirmed via a real build: +79 KB gzip regression, fixed by switching to the subpath import).
  No code changed, documentation only.
- 82a3419: Add `heading`/`closable` convenience chrome and a `--lr-dialog-max-width` token to `<lr-dialog>`. `<lr-dialog>` previously required a consumer to hand-build any visible title bar (by slotting a real heading element) and any close affordance (via a footer button wired to `close()`) — `heading` now renders a visible header row with that text when no heading element is slotted (still deferring to a slotted heading, unchanged, when present), and `closable` renders a built-in close (X) button in that same header row, wired through the exact same `close()` path Escape/backdrop-dismiss already use, with reason `'close-button'`. `[part="panel"]`'s previously-hardcoded `max-inline-size: min(32rem, 100%)` is now `min(var(--lr-dialog-max-width, 32rem), 100%)`, mirroring `<lr-media-card>`'s `--lr-media-card-max-height` — the default stays exactly `32rem` when unset. All three are additive/opt-in; existing consumers see no behavior change.
- 82a3419: `<lr-heatmap>`'s calendar mode gained four additive extensions. `firstDayOfWeek` (0-6, Sunday-first default, same numbering as `CalendarCellPos.weekday`) anchors the week grid at a different weekday instead of always Sunday, threaded into `buildCalendarGrid()`'s new `firstDayOfWeek` parameter; matrix mode ignores it. `rowY` overrides the y-origin computed for each weekday row, the vertical analogue of the existing `columnX`, consulted consistently by drawing, hit-testing, and the keyboard focus ring via a new private `rowYFor()` helper mirroring `columnXFor()`'s exact dispatch-with-computed-fallback shape. The previously matrix-mode-only `cellSize`/`fitToWidth` properties now also size calendar mode's grid, replacing its hardcoded 11px cell constant when explicitly set (unset, calendar mode keeps that original 11px default). The previously matrix-mode-only `scale` property now also governs calendar mode's bucketing: `scale="sqrt"` compresses via the same square-root magnitude compression matrix mode uses instead of always calling `quartileBucket()`, so one heavy day doesn't wash out a skewed dataset; the default `"linear"` preserves today's exact quartile-only calendar behavior. All four are opt-in and no-ops when left unset/default.
- 82a3419: `<lr-lite-chart>` gained seven additive properties. `pointText` overrides the per-bar/per-point `<title>`/`aria-label` tooltip text (mirrors `lr-heatmap`'s `cellText` hook), falling back to today's exact raw-value template when unset. `roundedBars` draws bars as a rounded-top-corner path instead of a square-cornered rect (default `false` keeps the plain rect). `skipZero` omits a bar entirely — no mark, no `tabindex`, no tooltip — for a value that is exactly `0`, instead of today's zero-height-but-focusable bar (default `false` unchanged). `padLeft`/`barGapRatio` override the internal `PAD_LEFT`/`BAR_GROUP_GAP` layout constants (36px / 0.2 respectively) when set. `scale` (`'linear' | 'sqrt'`, `type="bar"` only) switches the bar-height mapping from the default linear `niceDomain` fraction to a `Math.sqrt(value / domainMax)` compression mirroring `lr-heatmap`'s matrix-mode `sqrt` scale, so a skewed dataset's smaller bars aren't washed out by one dominant value; `type="line"` ignores `scale` entirely. `hideAxis` suppresses `renderGrid()`'s gridlines and y-axis tick labels altogether (x-axis category labels are unaffected). All seven are opt-in and no-ops when left unset/`false`.
- 82a3419: `<lr-markdown>` gains four additive properties. Every rendered `<img>` now carries a `part="img"` (with a matching `[part='img'] { max-width: 100% }` base style), alongside the existing `content`/`heading`/`code-block`/`link`/`table`/`blockquote` parts — previously images went through marked's default renderer with no styling hook at all. `heading-offset` (default `0`) shifts every rendered heading's depth before emitting `<h${depth}>`, clamped to `<h1>`–`<h6>`, letting a consumer nest rendered markdown under an existing heading level without losing document outline. `link-target` (default `'_blank'`, unchanged) can now be set to `null`/`''` to omit `target`/`rel="noopener noreferrer"` entirely and open links in the same tab, instead of always forcing a new tab. `eager-load` (default `false`) skips `connectedCallback()`'s async `marked`/`dompurify` `import()` and renders synchronously whenever the shared module cache (`markdown-loader.ts`) is already warm — e.g. a second `<lr-markdown>` on the same page, or a consumer that primes `loadMarkdownDeps()` at startup — avoiding the brief plain-text fallback paint that otherwise happens on every connect, even when both peers load without error. All four are opt-in; unset, output is byte-identical to before.
- 82a3419: `<lr-menu-item>` gained a `type` property (`'normal' | 'checkbox'`, default `'normal'`) and a `checked` boolean, mirroring `wa-dropdown-item`'s identical `type="checkbox"` pattern for building things like a "Word wrap" or "Show minimap" toggle inside a `<lr-menu>`. A `type="checkbox"` item renders `role="menuitemcheckbox"` (instead of `role="menuitem"`) with `aria-checked` reflecting `checked` and a checkmark glyph shown once checked; activating it (click, or Enter/Space via a parent `<lr-menu>`'s roving-focus handling) toggles `checked` and fires a new `lr-menu-item-change` event (`detail: { value, checked }`) in addition to — not instead of — the existing `lr-menu-item-select`, so a parent menu still closes and re-fires its consolidated `lr-menu-select` exactly as before. `type="normal"` (the default, and every existing `<lr-menu-item>` in the wild) is completely unaffected: same role, same rendering, same events as prior releases.
- 82a3419: `<lr-model-select>`: added a `label` property that renders a visible `part="form-control-label"` title above the trigger/combobox, paired with it via `for`/`id`, mirroring `<lr-select>`'s own `label` exactly. Once non-empty it also takes over as the accessible-name source, with an explicit host `aria-label` still winning over it (same precedence as `lr-select`). Unset (the default), the control keeps today's exact `aria-label || placeholder || 'Model'` fallback chain unchanged.
- 82a3419: `<lr-select>`'s single-enabled-option auto-commit trigger (added 1.3.0) is now gated behind a new `autoCommitSingleOption` property, default `false`. Previously this behavior was unconditional as soon as exactly one `<lr-option>` was enabled, silently swapping the trigger's ARIA role and keyboard model on any consumer whose option list happened to narrow to one entry at runtime. Existing consumers now get the pre-1.3.0 combobox trigger unless they explicitly opt in with `auto-commit-single-option`.
- 82a3419: `<lr-split>`'s `collapseState` is now a public accessor with force/auto semantics mirroring `<lr-app-rail>`'s `mode`: it was previously derived only from the `ResizeObserver`-measured container width, but assigning a concrete `'wide'`/`'rail'`/`'floating'` value now pins it there (ignoring further measurement) until released back to automatic tracking by assigning the write-only `'auto'` sentinel, which immediately re-derives it from the current width. `lr-split-collapse-change` fires on both a forced assignment and a release-to-auto, exactly as it already did for a breakpoint crossing, and only when the effective state actually changes. The `'floating'` tier also gains a new `open` property (default `false`): previously this state always rendered its pane as an always-visible overlay card the moment the container narrowed past `float-breakpoint`; it's now a hidden-by-default drawer — the pane renders nothing (hidden, out of the accessibility tree) until a consumer sets `open`, at which point it renders with a `[part="backdrop"]` scrim, traps focus, and closes (`open = false`) on Escape or a backdrop click, mirroring `<lr-app-rail>`'s mobile overlay. `collapseState` still reflects to a `collapse-state` attribute for CSS targeting. `open` defaulting to `false` is a deliberate behavior change for the `'floating'` tier specifically (it was previously always visible); every other collapse behavior, and `collapse="none"` (the default), is unaffected.
- 82a3419: `<lr-tabs>` can now render a leading icon inside a generated tab button without changing its accessible name. Give a panel's tab an extra direct-child sibling of `<lr-tabs>` carrying `slot="<id>-icon"` (any markup — an inline SVG, an emoji span, a custom icon element) and it renders ahead of the label inside that tab's button, wrapped in a new `part="tab-icon"` `aria-hidden="true"` span so it's always excluded from the button's accessible name (which stays exactly the `label` attribute's text, as before). A tab with no matching `<id>-icon` sibling renders no icon wrapper at all, so every existing text-only `<lr-tabs>` is byte-for-byte unaffected. A named slot (rather than an `icon="<name>"` attribute keyed into this library's internal `icons.ts`) was chosen because that internal set is a small closed vocabulary of chrome glyphs for this library's own components, not a public icon registry — a slot lets a consumer supply an arbitrary, domain-specific icon instead.

## 2.0.0

### Major Changes

- 8b5f729: **Breaking:** the root `@aceshooting/lyra-ui` entry point no longer re-exports or
  side-effect-registers the optional-peer-dependent component families — `<lr-chart>`
  and its typed subclasses, `<lr-box-plot>`, `<lr-histogram>`, `<lr-map>`, and
  `<lr-graph>`. Import each of these directly from its own subpath instead (the README
  already recommends granular subpath imports as the primary pattern):

  ```js
  import "@aceshooting/lyra-ui/components/chart/chart.js";
  import "@aceshooting/lyra-ui/components/map/map.js";
  ```

  Why: the root barrel previously re-exported every component's public API from one
  `lyra.ts` file, so TypeScript had to resolve `chart.js`/`maplibre-gl`/`d3-force`'s type
  declarations even for a consumer who only imports an unrelated component (e.g.
  `LyraEmpty`) from the package root — a hard compile error for anyone who hadn't
  installed every optional peer. Splitting these families out of the root barrel means
  importing `@aceshooting/lyra-ui` (or any of its remaining members) never requires an
  optional peer's types to be resolvable.

  Every other component (including `<lr-lite-chart>`, which has zero peer
  dependencies) is unaffected — the root barrel still re-exports/registers everything
  else exactly as before.

### Minor Changes

- 144ad8f: Add a `compact` flag tier and expose three fidelity tiers via `variant`.

  `@aceshooting/lyra-flags`: the ~65 emblem flags now ship a tiny WebP raster at
  `flags/compact/<code>.webp` (~1–3 KB) alongside the standard vector and the pristine `detailed`
  original. `flagUrl(code, { variant: 'compact' | 'standard' | 'detailed' })` selects a tier,
  code-split per flag _and_ per tier so a bundled app ships only the tiers it actually uses. The
  `standard` tier was also re-derived from the pristine originals so every flag is now under 80 KB
  (no fidelity loss perceptible at card/row scale).

  `@aceshooting/lyra-ui`: `<lr-flag>` gains a `variant="compact" | "standard" | "detailed"`
  property — a tiny raster for icon-scale use (menu items, language selectors), the default
  icon-optimized vector for card/row sizes, or the pristine full-detail vector for hero display.
  The `detailed` boolean is deprecated but kept working as an alias for `variant="detailed"`.

- 2a7390d: Fix `lr-heatmap` calendar mode's month/weekday axis labels to follow the runtime locale instead of hardcoded English, and add a `columnX` override so a calendar's week columns can be pixel-aligned with an external coordinate function.
- 43864d6: Add `lr-lite-chart` `layout="scroll"` (fixed-width, horizontally-scrollable bars via `barWidth`), `maxLabels` axis-label decimation, and a `barX` coordinate override for pixel-aligning bars with a sibling `lr-heatmap`.
- 043b7b0: Move `LyraSelectSize` above `<lr-select>`'s class JSDoc block so `custom-elements.json` correctly documents `lr-select` as a custom element.
- 7bbe3d2: Add `lr-split` opt-in responsive collapse (`collapse="start"|"end"`, `rail-width`, `rail-breakpoint`, `float-breakpoint`): below `rail-breakpoint` the chosen pane clamps to a fixed rail width, below `float-breakpoint` it becomes an absolutely-positioned floating overlay, both signaled via a `data-collapse-state` attribute/dataset marker and the new `lr-split-collapse-change` event.
- f14165f: `<lr-stat>` breakdown rows (`StatRow`) gain an optional `exactValue` field, mirroring the headline value's tooltip: setting it renders a `title` tooltip and makes that row's `[part='row-value']` keyboard-focusable, independently per row.
- d62725d: `lr-table`'s `[part='reveal-columns-button']` now renders only when a `priority` column is actually hidden by the `@container` breakpoints (or `showAllColumns` force-visible mode is active), instead of whenever any column merely declares a `priority`; the new `columnsHidden` reactive property and `lr-columns-hidden-change` event expose the same real-time state to consumers.

### Patch Changes

- Updated dependencies [144ad8f]
  - @aceshooting/lyra-flags@1.3.0

## 1.3.0

### Minor Changes

- 6358479: Added a "Conversation & Agent UI" family: chat/tool-call/agent-config building blocks for
  streaming AI interfaces, plus the general-purpose primitives (dialog, tabs, checkbox, switch,
  menu, chip, JSON viewer, live region, markdown, code block) they're built from. No breaking
  changes to any existing component.

  New tags: `lr-dialog`/`confirm()`, `lr-tabs`, `lr-checkbox`, `lr-switch`,
  `lr-json-viewer`, `lr-live-region` (+ `internal/announcer.ts`'s throttled `Announcer`),
  `lr-markdown` (needs the optional peers `marked`/`dompurify`), `lr-chat-message`,
  `lr-typing-indicator`, `lr-tool-call-chip`, `lr-tool-result-view` (+ its
  `registerToolRenderer()` renderer registry), `lr-tool-result-dialog`, `lr-chat-composer`
  (form-associated), `lr-attachment-chip`, `lr-stream-status`, `lr-virtual-list`,
  `lr-conversation-item`, `lr-model-select`, `lr-slider` (form-associated),
  `lr-tool-select-dialog`, `lr-citation-badge`, `lr-source-list`/`lr-source-card`,
  `lr-app-rail`, `lr-responsive-panel`, `lr-mention-popover`, `lr-streaming-text`,
  `lr-thinking-panel`, `lr-generation-status`, `lr-code-block` (needs the optional peer
  `shiki`), `lr-tool-approval-dialog`, `lr-tool-param-form`, `lr-menu`/`lr-menu-item`,
  `lr-chip`/`lr-chip-group`, `lr-model-settings-panel`, `lr-context-meter`,
  `lr-dock-panel`, `lr-document-preview`, `lr-media-card`, `lr-attachment-trigger`,
  `lr-kbd`, `lr-result-card`/`lr-result-field`.

  Also extends `internal/rtl.ts` with `rtlAwareSide()`/`rtlAwarePlacement()` (mirrors a physical
  `left`/`right` value, or the `left`/`right` component of a Floating UI `Placement`, under RTL) —
  used by `lr-menu`'s `placement` property so an explicit `placement="left-start"` still anchors
  to the trailing edge instead of the physical left when the page is RTL.

- 6358479: `<lr-select>`: when exactly one `<lr-option>` is enabled, the trigger now auto-commits that
  option on click or Arrow Up/Down instead of opening a single-row listbox — no chevron, no popup,
  `role="button"` instead of `role="combobox"`. Avoids an unnecessary extra click for "only one
  choice available" states (e.g. a filtered picker that's converged to a single match). Multi-option
  selects are unaffected; `value`/validity defaults are unchanged. Not gated behind a new prop — this
  is the new default trigger behavior for any select with a single enabled option.

## 1.2.0

### Minor Changes

- 6e832d5: `<lr-chart>`: added `IntersectionObserver`-gated lazy redraw and content-signature memoization — a
  chart skips calling into Chart.js while scrolled off-screen (redrawing once when it re-enters the
  viewport) or when none of its content-affecting properties (`type`, `labels`, `datasets`, `legend`,
  `area`, `xLabel`, `yLabel`, `y2Label`, `beginAtZero`, `horizontal`, `stacked`, `config`) have actually
  changed since the last draw. `refreshTheme()` is unaffected and always redraws.
- 9d36af5: `<lr-combobox>`: the input's accessible name now checks a host-level `aria-label` attribute before
  falling back to `label`/`placeholder`/`"Combobox"` — previously a plain `aria-label` on
  `<lr-combobox>` was silently ignored. Matches the same fix in `<lr-select>`.
- 0b3ea6c: `<lr-flag>`: added a `detailed` boolean property that requests the pristine, full-detail source SVG
  for the minority of flags whose default rendering was recently optimized for icon scale (e.g. `es`,
  `pt`, `sv` — see the `@aceshooting/lyra-flags` changeset). A safe no-op for every other flag. Useful
  for a flag rendered larger than icon scale (e.g. a hero display) where the extra illustrative detail
  is actually visible.
- 2027e3f: `<lr-flag>`: the default accessible name (`alt`, used when `label` is unset) is now a human-readable
  region name via `Intl.DisplayNames` (e.g. `language="en"` → `"United Kingdom"`) instead of the bare
  uppercase country code (`"GB"`, previously read letter-by-letter by most screen readers).
- 49569ed: `<lr-heatmap>`: fixed `role="img"` conflicting with the canvas's own focusable, keyboard-interactive
  descendant (arrow-key roving focus, Enter/Space activation) — now `role="group"`, matching
  `lr-lite-chart`/`lr-word-cloud`'s existing pattern. Added `cellText?: (pos, value) => string`, a
  formatter hook for the per-cell hover tooltip and keyboard live-region announcement (both draw from the
  built-in English template by default; this is additive, not breaking). Also fixed calendar mode's date
  label formatting, which hardcoded the literal `'en'` locale instead of the runtime locale.
- ef74f4a: `<lr-lite-chart>`: added `tickFormat?: (value: number) => string` to customize y-axis tick label
  formatting (e.g. currency, duration) instead of the built-in nice-number formatter. Also added
  `IntersectionObserver`-gated lazy rendering and content-signature memoization — a chart skips
  recomputing its grid/marks while scrolled off-screen or when none of its content-affecting properties
  (`type`, `labels`, `datasets`, `legend`, `xLabel`, `yLabel`, `beginAtZero`, `stacked`, plot size) have
  actually changed since the last render.
- 22cf001: `<lr-select>`: added a `size` property (`xs`/`s`/`m`/`l`/`xl`, default `m`, same scale as
  `lr-toast-item`'s `size`) for compact toolbar placements that don't fit the default trigger height.
  Also, the trigger's accessible name now checks a host-level `aria-label` attribute before falling back
  to `label`/`placeholder`/`"Select"` — previously a plain `aria-label` on `<lr-select>` was silently
  ignored.
- 4bf80aa: `<lr-stat>`: added `exact-value` (shown as a hover/focus tooltip on the headline value, e.g.
  `value="$1.2K" exact-value="$1,204.37"`), a `sub` property/slot (a secondary line distinct from
  `caption`, e.g. a comparison-period label), a `prose` boolean (renders `value` as smaller/lighter text
  with `unit` hidden, for a loading/status message in place of a numeric value), and a `compact` boolean
  (tighter padding for constrained spaces — same convention as `lr-empty`'s and `lr-widget`'s
  `compact`).
- c8206f8: `<lr-widget>`: added `fullscreen-inset` (a raw CSS `inset` shorthand, e.g. `"0 0 0 240px"`, applied to
  the fullscreen panel and backdrop instead of the default `var(--lr-space-l)` on every side — for apps
  with a persistent sidebar/toolbar that should stay visible during fullscreen) and a `compact` boolean
  (tighter header/body padding), matching `lr-empty`'s existing `compact` convention.
- a768a20: `<lr-word-cloud>`: fixed the rendered `<svg>` not respecting a host-assigned height —
  `[part='base']` had no `block-size` rule, so the internal `svg { block-size: 100% }` resolved against
  an indefinite containing-block height and fell back to the spiral layout's own intrinsic size instead,
  overflowing past the host's box. `[part='base']` now constrains to `block-size: 100%`, matching the
  component's own documented `<lr-word-cloud style="height: 20rem">` usage pattern.

### Patch Changes

- Updated dependencies [da766cb]
  - @aceshooting/lyra-flags@1.2.0

## 1.1.0

### Minor Changes

- c033ec0: `@aceshooting/lyra-flags`: `flagUrl(code)` is now genuinely code-split per flag — each code is
  its own dynamically-`import()`ed chunk, so using it (directly, or via `<lr-flag
country=...>`/`<lr-flag language=...>`) only ever fetches the flags actually requested at
  runtime, not all 249. This makes `flagUrl()` `async` (**breaking**: `Promise<string | undefined>`
  instead of `string`). `FLAG_URLS` (the old synchronous, eager, all-249-at-once map) is no longer
  exported from the package root — the equivalent for a consumer that genuinely wants every flag up
  front (e.g. a flag-picker listing every country) is the new `flagUrls()` (`async`, resolves the
  full map). `FLAG_LOADERS` (the new lazy per-code map `flagUrl()` is built on) is exported directly
  for consumers that want the per-code laziness without going through `flagUrl()`.

  `@aceshooting/lyra-ui`: `<lr-flag>` transparently picks up the lazy-loading fix — no changes
  needed at call sites using `country`/`language`. Also adds a new `src` property: a pre-resolved
  flag image URL that takes precedence over `country`/`language` and skips the peer-package lookup
  (and its loading-skeleton round trip) entirely, for consumers who already have a flag's URL at
  build time (e.g. via `import frUrl from '@aceshooting/lyra-flags/flags/fr.svg?url'`).

- c033ec0: Added `<lr-lite-chart>` — a dependency-free bar/line chart (plain SVG/DOM rendering, zero peer
  dependencies) for projects whose architecture forbids a charting dependency outright. Covers
  grouped/stacked bars, multi-series lines, per-point click (`lr-point-click`, same detail shape as
  `lr-chart`'s), and hover tooltips via native SVG `<title>`. Not a full `lr-chart` replacement —
  no zoom/pan, no pie/doughnut/radar/scatter/bubble types, no horizontal/dual-y-axis, no raw-config
  passthrough. Reuses `lr-chart`'s `--lr-chart-*` theme token names for free cross-component
  theming.
- c033ec0: Added `<lr-word-cloud>` — a dependency-free SVG word/tag cloud, laid out via an outward
  Archimedean-spiral placement search (heaviest word first). Supports `linear`/`sqrt` weight-to-font
  scaling, optional `mixed` (rotated) orientation, per-word or per-`group` coloring with a themeable
  `--lr-word-cloud-color-1..8` palette, and roving-tabindex keyboard navigation matching
  `lr-heatmap`'s pattern (a single tab stop, arrow keys, Home/End, a live-region announcement).

  Also a hardening pass across the rest of the library — real bugs fixed, not just polish:

  - `lr-skeleton`: `width`/`height` properties had zero visual effect (the custom property was set
    on the wrong shadow-DOM node); now actually resizes the placeholder.
  - `lr-combobox`: setting `open` directly (bypassing `show()`) never wired up click-outside or
    fired `lr-show`/`lr-hide`; picking a row or clearing while using `source` left stale async
    results displayed; a `<lr-option selected>` appended after the first slotchange was ignored;
    two nameless `multiple` comboboxes in the same form merged their submitted values; a pending
    debounced `source` fetch could fire after the element was removed.
  - `lr-chart`: bubble-chart series got a categorical (not numeric) x-axis, collapsing every point
    onto one tick; `resetZoom()` double-emitted `lr-zoom`, briefly reporting the stale pre-reset
    `zoomed` state to `{ once: true }` listeners.
  - `lr-date-picker` / `lr-date-input`: the already-exported `clampDate()` was never actually
    wired in, so `goToDate()`/`goToToday()` could navigate to (and focus) an out-of-range date;
    locale/weekday-format/first-day-of-week wiring gained test coverage; outside-month placeholder
    cells are now `aria-hidden` only in rows that also have a real visible day.
  - `lr-tree`: mouse-driven expand/collapse/select could desync the roving-tabindex `activeId` from
    real DOM focus; arrow-key expand/collapse is now RTL-aware, matching `lr-split`/`lr-time-range`.
  - `lr-widget`: the fullscreen focus trap didn't pierce into a slotted custom element's own shadow
    root, letting focus escape to a hidden nested control.
  - `lr-toast-item`: the close button used the native `disabled` attribute, which force-blurs a
    focused element with nothing to restore it — switched to `aria-disabled`.
  - `lr-empty`: gained a live-region announcement when entering the empty state, matching
    `lr-skeleton`'s existing `role="status"` convention.
  - Accessibility, documentation, and test-coverage fixes across most other components; `llms.txt`,
    `llms-full.txt`, and both READMEs corrected for drift against the current API surface.

  No breaking changes.

### Patch Changes

- Updated dependencies [c033ec0]
  - @aceshooting/lyra-flags@1.1.0

## 1.0.1

### Patch Changes

- 436b1ce: Fix `scripts/publish.sh` to commit `CHANGELOG.md` and `custom-elements.json` with each release commit (previously only `package.json`/the lockfile were staged, leaving those generated files uncommitted after every release). Remove the redundant `.github/workflows/publish.yml` CI job, which always failed by re-publishing a version `publish.sh` had already shipped.

## 1.0.0

### Major Changes

- 99fb0e0: Added several new components

### Patch Changes

- Updated dependencies [99fb0e0]
  - @aceshooting/lyra-flags@1.0.0

All notable changes to `@aceshooting/lyra-ui` are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Release notes on GitHub (`gh release create --generate-notes`) are generated from commit
history and may be more granular than this file; this file is the curated, human-readable
summary.

## [0.1.3] baseline

Current published baseline at the time this changelog was introduced. Historical versions
prior to 0.1.3 were not backfilled into this file — see git tags (`git tag -l`) and GitHub
Releases for the full release history.

- Free, clean-room Lit 3 web-component library — an open-source companion to Web Awesome.
- Tiered component set (layout/atoms, forms, overlays, data-viz/dashboard, temporal/graph,
  map/file/flag families) — see `packages/lyra-ui/llms.txt` and `llms-full.txt` for the full
  API reference.
- `@aceshooting/lyra-flags` optional companion package for `<lr-flag>` artwork.

[0.1.3]: https://github.com/aceshooting/lyra-ui/releases/tag/0.1.3
