# @nannier/canvas

## 4.4.0

### Minor Changes

- 8442fcc: Badge: add `truncate`, which keeps the label on one line and lets the pill shrink in a tight row, ending the label with an ellipsis instead of overflowing its host. The board card's `noticeLabel` pill uses it, so a long notice no longer runs past the card's metadata row.

  Minor justification: a new public option on an existing component (`Badge truncate`). Badges without it render unchanged.

- e8f0a59: Card: add `noticeLabel` to board tickets, a quiet outline pill after the type and priority for a short notice such as an outdated workflow version.

  Minor justification: a new public option on an existing component (`Card board noticeLabel`). Existing cards render unchanged.

- 6f6409f: Chat: the default Chat returns to the v4 hand-off's look, and the Claude desktop window layout moves behind a new `assistant` prop.

  - Default (no `assistant`): the conversation card with its thread 16 in and messages 14 apart, 14 / 1.55 replies, the user bubble in the accent's soft wash at the 16 corner with its trailing bottom corner tucked to 4, 9 by 13 in, and the composer's sunken field under a hairline (the 16 corner on the field border, 6 in and 12 at the start, the draft and a 32 px round send button 8 apart, a paperclip attach action).
  - `assistant` (on Chat, ChatThread, ChatMessage and ChatComposer): the layout measured on the Claude desktop window: one centered column whose composer box is 768 wide at most, replies at 15 / 21 inset 20 from the composer and the user bubble 16, a neutral `muted` user bubble at its own 10 corner, turns that change speaker 40 apart and a turn's blocks 20 apart, a raised 44 px composer at its own 12 corner with a bare add icon and a send control that stays quiet until there is a draft, the `meta` model line, and 12 px gutters below 480 wide. Its corners are the new web shape roles `assistantBubble` (10) and `assistantComposer` (12); the hand-off's `composer` and `bubble` stay 16 and `bubbleTail` (4) returns.
  - `flush` keeps meaning only "drop the card frame"; combine it with `assistant` for a conversation that sits on its host's fill, as the Wizard `assistant` example does.

  Consumers that relied on the desktop layout Chat drew by default since 4.3 pass `assistant` to keep it. Minor justification: a new public option on an existing component (`Chat assistant`).

- e7a942a: Chat, Wizard, Dialog, Steps and Textarea can now lay out a conversation like the Claude desktop window, and the same pieces serve a planning modal and a ticket drawer.

  - Chat: `flush` drops the card frame so the conversation sits on its host's fill; consecutive activity folds into one expandable line; author lines appear only when the app names an author or provider. The Claude desktop layout itself (the 768 column, 15 / 21 replies, the 40 / 20 turn rhythm, the 44 px composer with its quiet send control and the `meta` model line) is Chat `assistant`, described in its own changeset.
  - Wizard: `assistant` gives a full-height 287 px sidebar of step rows, a 44 px top bar with the title and status, `previous` and the actions in a bar under the conversation, and an unpadded body for an `assistant flush` Chat.
  - Dialog: `toolbar` (with `banded`) makes the header a 44 px top bar; `headerActions` places actions before the close action.
  - Steps: `rows` (with `vertical`) draws each step as a 32 px sidebar row, the current one filled and done ones checked.
  - Textarea: `composer` sets a chat composer's 15 / 20 metrics.

  Minor justification: new public options on existing components (`Chat flush` and `meta`, `Wizard assistant`, `Dialog toolbar` and `headerActions`, `Steps rows`, `Textarea composer`). Without them, components keep their layout.

- 2c347bc: Add FlowEditor, a new public organism: a node-graph editor with cards on a pannable, zoomable canvas joined by directed, labelled edges. Cards move by drag or arrow keys and snap to a grid, edges are drawn from a card's port or with the toolbar's From and To selects, palette entries add nodes by press or drag, and a read-only mode shows a graph without editing it. Minor justification: a new public component (FlowEditor) with its types (FlowNode, FlowEdge, FlowPaletteItem, FlowSelection, FlowPoint, FlowEditorProps), which Argus's Workflow section builds on. The docs derivation script also learns to carry components the v4 hand-off predates.

### Patch Changes

- 0be26c1: Chat docs: the "Assistant layout" example's first activity line now carries the `content` every `ChatMessageData` requires, so the generated example typechecks against the real props and expands to what was read.
- 5e3596f: Chat: two Claude desktop metrics that still applied to the default Chat now apply only under `assistant`, so the default follows the v4 hand-off's message renderer.

  - The person's own turns carry no author line in the default Chat, even when the app names an `author`, `provider` or `providerIcon`; a reply keeps its author line when named, as before. With `assistant`, a named turn of either speaker keeps its author line, as the desktop layout drew it.
  - A folded activity line's label is 13 / 18 in the subtle ink in the default Chat (the hand-off draws no activity line, so it keeps the label activity lines were introduced with); `assistant` keeps the desktop window's 14 / 21 in the muted ink.

- 57ca3bd: Docs: FlowEditor, which the v4 hand-off predates, now has its own card on the Data page (after the Board, under Workspace) and is tagged Organism in the Library index. Components added after the hand-off are declared once in a shared registry that the docs generators and checks read.
- a79755f: FlowEditor: the iOS and Android builds now draw the web look (neither platform ships a node-graph editor, so no native shape is owed; the toolbar still uses each platform's Button and Select), and an edge's label is a true capsule drawn over the canvas instead of an SVG rectangle, in the same place, size and inks.
- 6f6e2fe: Move GeoMap's pan-and-zoom math into a shared view camera so other zoomable surfaces reuse it. GeoMap behaves exactly as before.
- 5d8cb30: Steps: a pressable `rows` step now gives the platform's press feedback, as the numbered marker does: iOS and the web dim it while pressed, and Android ripples inside the row's corner.

## 4.3.1

### Patch Changes

- c3e5cb7: Wizard moves its steps under the title as segmented bars when its panel is narrower than 520 points, instead of keeping the 170 point rail beside a squeezed body. Chat narrows its thread and composer gutters from 24 to 12 points below 480 points wide. Together they stop the composer's text box collapsing to zero width in a phone-width planning wizard.

## 4.3.0

### Minor Changes

- 0d5af5f: Chat now reads like a desktop assistant: a centered reading column, plain assistant replies, a neutral bubble for the person's own turns without an author line, and a raised rounded composer with round add and send buttons. Minor because it adds a public option: `activity` on `ChatMessageData` shows work the assistant did (a command, a tool call, a process log) as one folded line that expands to its content.

## 4.2.0

### Minor Changes

- d33952f: Every component now reports its named anatomy parts to an enclosing `AnatomyProvider`, so an Anatomy figure numbers the parts of any component, not only Button. `AnatomyPart` is exported to register an element as a named part, and a component's parts belong to it alone: kit components and content nested inside it register nothing of theirs. Outside a provider nothing changes.

  Minor justification: new public API (`AnatomyPart`, `AnatomyPartProps`) and a new capability across the kit (anatomy part reporting for every component).

- 68dcdf4: Icon glyphs gain categories. Each `ICON_NAMES` entry now carries a `category`, and
  the new `ICON_CATEGORIES` export (typed `IconCategory`) lists the fifteen categories
  in menu order, so an icon picker can group and filter the set. Minor: this adds a
  public export and a field, a new capability for consumers building icon pickers.
- 693ad6c: Add opt-in terminal transcripts and bare multiline prompts to Chat, ChatMessage, ChatThread, ChatComposer, and Markdown. Preserve semantic Markdown in monospace with flat scrolling tables, and support IME-safe hardware Enter sending, Shift+Enter newlines, and composer autofocus. Add terminal Textarea metrics and a Typography terminal face modifier that preserves reading sizes. Terminal transcripts, prompts, and CodeBlock transcript output use a modern system monospace stack independently of existing mono typography. Preserve all existing defaults.

  Minor justification: new public options (`terminal` on Chat, ChatMessage, ChatThread, ChatComposer, Markdown, Textarea and Typography) that consumers such as Argus use for terminal transcripts and prompts.

- 0c67056: Add a solid inset terminal Drawer and a bare selectable CodeBlock transcript variant for execution sidebars.

  Minor justification: new public variants (`terminal` on Drawer, `transcript` on CodeBlock) for execution sidebars.

### Patch Changes

- e5f2831: AvatarGroup stacks avatars the way the v4 hand-off does: each avatar after the first overlaps its neighbour by 10px at every size (it was 16px at the default size and 13px on small avatars, which crowded the initials); `snug` and `loose` stay looser. A stock initials disc now takes the hue the hand-off draws for the same name (its eight identity hues and hash), so a name's disc colour matches the design; Argus's reference discs keep their gradient hues.
- 22f8af8: Prepare consumer plugin metadata for OpenAI and Claude distribution, and update
  the recipe scaffold to Canvas 4.1.0 with a package-derived version label and
  optional hosted subpath configuration.
- 6437937: PieChart and RadialBarChart sit in the v4 hand-off's chart frame like every other chart (the card fill on a hairline at the card corner under the small shadow, a 16px inset, 12px compact), and take `plain` to drop it when nested in a card. RadialBarChart draws the hand-off's rings: 16 units apart from the outermost, a 10 unit stroke, scaled to its 160px disc (120px compact).
- 3fe1a49: Make the README a concise introduction with a single-package npm install command and a first component up front. Preserve detailed setup, optional dependencies, overlays, and theming in a linked usage guide that also supplies the generated API documentation.
- a55f54a: GridList lays its tiles out on the v4 hand-off's grid: three columns by default (`cols2` for two), each at least 140px wide, so a phone keeps two columns and only a very narrow grid drops to one, and a short last row keeps its columns instead of stretching. Gallery thumbnails take the hand-off's diagonal wash of the item's colour (a `blue-500` or `emerald-500` reads by its colour family, as the hand-off draws it), the gallery title sits on a 16px line, and a tappable gallery tile lifts to the medium shadow under the pointer on the web. A tappable people tile is now pressable across its whole card; it used to take presses only on its centred content.
- b8e2dec: Heatmap draws the v4 hand-off's heatmap: the chart frame (the card fill on a hairline at the chart corner under the small shadow, a 16px inset, its rows 12px apart; `plain` drops it inside a card), `label` (or `caption`) as its 500 13px title, 11px cells on a 13px pitch in seven-row columns that scroll sideways where they overflow, an empty cell in the secondary surface and a valued one in the chart-1 wash from .18 to .98, and the hand-off's less-to-more legend. A plain `calendar` takes the same cells with day inspection and no axis labels, as the hand-off draws it; `detailed` keeps its unframed calendar with month and weekday labels.
- dd363c6: Keep InputOTP's active cell and caret synchronized with an already focused input when hydrating prerendered autofocus markup.
- 491f18d: Stats takes the v4 hand-off's layout and tile: by default the metrics sit in one bordered box with a hairline between each, and `framed` separates them into tiles 12px apart. The group lays out on the hand-off's auto-fit grid (a 180px floor for three or more metrics, 220px for one or two), so a narrow card stacks its metrics. Each tile has a 16px inset, a 13px label led by the accent's swatch and the icon, a small trend line beside the figure, a delta led by its direction arrow and, for `share`, a thin bar over a legend. Tappable metrics show a chevron and a hover wash on the web. The `compact` and `large` tiles are unchanged.
- e56cdef: A titled Stats group draws its title above its bordered box, 10px over the metrics, instead of inside the box against its edge.
- 50560bc: Characterize the terminal variants the kit's design rules check: Textarea's prompt metrics now resolve through `metricsOf` (terminal, then dense), CodeBlock's `transcript` and Drawer's `terminal` are members of their recorded axes, and the Drawer terminal screen's 12 corner is a declared kit corner (`terminalScreen`) instead of a bare number. No rendered value changes.
- 4929ff8: Keep the native role scan's message-author exceptions in step with the regenerated Chat examples, and cite a commit in the audit reference test by a prefix that always holds a letter, so the test no longer fails on the 2% of SHAs that open with eight digits.

## 4.1.0

### Minor Changes

- 198c70e: Ship versioned agent-readable component references, a consumer skill for Codex and
  Claude Code, and offline documentation, setup diagnostics and recipe scaffolding
  commands in the existing Canvas package. Include settings, dashboard and booking
  recipes, and generate a sitemap for the documentation export.

### Patch Changes

- 198c70e: Clarify Expo installation, required peer versions and published-package guidance.
  Update the independent starter to the released Canvas 4 package and add a
  repeatable consumer-agent evaluation protocol.

## 4.0.0

### Major Changes

- 01dace1: Release Canvas 4.0.0 with the Canvas v4 “Light as material” design across the universal kit and its documentation.

  This release replaces the Dark Factory appearance with the vendored v4 hand-off: authored colors and focus rings, Instrument Sans and Bricolage Grotesque typography, revised component skins, floating glass surfaces on the web, and the reference interactions and animations. The five accent palettes are violet (the default), cyan, mint, rose, and amber in both light and dark schemes; the former blush palette is removed.

  The docs adopt the v4 navigation, section pages, component details, examples, and responsive chart compositions. Existing keyboard interaction, accessible names, focus management, and Reduced Motion behavior are retained.

  When upgrading from v3, update `react-native-svg` to **15.8.0 or later**. Canvas 4's chart effects use its SVG filter exports; SVG 14 is no longer supported. The React and React Native minimums remain 18 and 0.74, respectively.

  Activity feeds retain actor and target labels alongside their timestamps. Pagination keeps both navigation arrows available in narrow containers by using its compact presentation when the numbered row cannot fit.

  Wizard follows the reference's vertical stage rail, header status, and opposing footer actions. Its default panel cap is 620 px. On the web, embedded wizards render as opaque panels without a modal backdrop; live modal wizards retain their backdrop and floating material. Compact vertical Steps and compact outlined Inputs support the reference's dense workflow layout.

### Minor Changes

- bdc747f: Three looks the Canvas v4 hand-off's component page draws around its own components, carried by the kit so the docs pass a boolean instead of restyling (justification for the minor: new public options). Kbd gains `comfortable`, the page key beside its effect: the same cap padded 3 px above and below its label and 8 px at the sides, 25 px tall. Badge gains two metric steps: `token`, a code token chip (the 22 px pill with its 8 px inset under the small 11 px label in the code face, for a role, a state or an attribute name), and `tier`, a tier tag (a 24 px pill with a 10 px inset under an 11 px mono label in caps on its own line); the metric precedence is `notification` > `reference` > `navigationCount` > `count` > `tier` > `token` > `small`. DescriptionList gains `muted`, which sets each text value as a note beside its term, 400 13/19 in the muted ink on the web and Android, where the default value is the full-weight foreground data; the iOS value detail is already the secondary ink and keeps its type.
- 438b3a5: Two new capabilities from the v4 hand-off's component page, the user-visible additions
  this minor carries. DescriptionList gains `ruled` on its rows axis (it wins over
  `divided`): every row, the first included, draws a 1 px top rule in the `border` token,
  pads 9 above and below with nothing beside, and in the two-column layout the term hugs
  its content over a 120 floor with 14 to the value, the hand-off's accessibility-card
  rows, so a list under a caption reads as a table that starts below it. Typography gains
  `pagerName`, the previous and next cards' name under their label: the display face, 500
  18 on the face's own 22 line (the hand-off sets no ratio; the line is Chromium's `normal`
  for Bricolage Grotesque, measured on the hand-off page), in the foreground ink. The docs'
  accessibility cards take `ruled` and the previous/next cards take `pagerName`.
- f576583: Two new options from the v4 hand-off's component page, the user-visible additions this
  minor carries. Badge gains `band` on its metrics axis (between `tier` and `small`): a
  group band's pill, the props table's "pick one" beside an axis's title, 20 px tall with a
  7 px inset, no border and a 500 11 px label on the small pill's 14 px line, in the code
  face with `mono` and in the tone axis's fill (the accent's soft pill with none), where
  `small` is 18 tall with a 6 px inset. DescriptionList gains `compact`, its density axis:
  on a `ruled` list the rows take the hand-off's semantics-card metrics, 8 above and below
  the text, 12 to the value, the two-column term a fixed 110 column and the value at the
  top of its row (so a wrapping row of badges starts level with its term); the plain and
  divided rows have no compact step, and iOS keeps its centered 52pt Lists row with the
  fixed column and the gap. The docs' props-table bands take `band` and the accessibility
  Semantics list takes `compact`, so the Intent band is the hand-off's 41 px and the
  Semantics rows its 8 px inset.
- 01dace1: Add Board pageScroll so an enclosing page can own horizontal overflow while plain lanes keep their equal widths and minimum lane size.
- 2898f10: Extend Card with a board ticket layout: fixed clamped text slots, supplied move handle, blocked reason, active AI text shimmer with cycling dots, and optional authenticated image header. Image failures collapse the image well and reduced motion keeps progress text static.
- e7df23d: A Button's icon slots take the button's label ink. An `Icon` in `iconLeft` or `iconRight` (and the icon-only body) with no tone of its own now paints in the label's colour for the intent, emphasis, body and state the button resolves (the primary's contrast ink on a primary button, the foreground on outline and ghost, the muted ink on a quiet or filter trigger, the link ink on a link, dimmed with the label while disabled), on web, iOS and Android, as the v4 hand-off draws its glyphs in `currentColor`. The seam is a kit ink context (`src/style/ink.tsx`): Button provides its resolved label ink around its slots, and Icon's colour precedence becomes the explicit tone or `color`, then the nearest ink, then the foreground, so every Icon that passes a tone keeps it and an Icon on the page is unchanged. The docs playground and Anatomy figure stop hand-toning the demo glyphs. Minor: a new capability, icons inherit the button's ink without a per-call tone.
- 13423ca: Adds Button's `plainLink` body (justification for the minor: a new public option, a plain accent text action). It is the Canvas v4 hand-off's bare accent button, a panel's Reset and a table's show-all row: no box, no fill, no underline, a 500 13 px label in the accent text ink (12 px with `small`) that brightens to the foreground ink under the pointer on the web, while iOS and Android draw their own tint-coloured text button; it is still read as a button. Two corrections ride with it: a loading web Button now draws the hand-off's open arc (14 px, a 2 px stroke in the label's ink with the right edge open, one turn per 0.8 s on the loop primitive) in place of React Native's ring with its track, with iOS and Android keeping the platform's activity indicator through a `loader` the skin names; and `themeChoice` is the hand-off's 32 px border box around a 20 px colour disc (`<Swatch tiny ring circle>` through `iconLeft`) instead of padding around the label line, so the disc centres in the pill. A loading button's loader also takes a special body's own label ink.
- a80963b: Card takes the Canvas v4 component page's card geometry as new public booleans (justification for the minor: new component options). A Corners axis adds `calloutCorners` (14), `largeCorners` (18) and `stageCorners` (20) beside `smallCorners`, each read from the web row's callout, panel and stage corners, while iOS and Android keep their own card corner; `outlined` joins the elevation axis as the hairline on the card fill with no shadow (the Material 3 outlined card on Android); the density axis gains the page's insets, `panel` (18, a 12 px rhythm), `guide` (18 by 20, 10), `linkTile` (14 by 16, 6) and `pager` (16 by 18, 4), and a density on a sectioned card (an eyebrow, a title or a footer) now reshapes its sections' inset and rhythm instead of being ignored; and CardMedia's specimen well gains the `stage` step (320 px, an 80 by 40 inset, the anatomy figure) and the `playground` step (340 px, a 44 by 28 inset, growing beside a panel). The docs component page frames its playground, variants, anatomy, props, accessibility, platform, guidance, related and previous/next cards with them at the hand-off's radii, insets and fills, and the inherited-props note under a props table is the hand-off's one-line callout.
- 01dace1: Add detailed composition legends, ordered outlined treemap partitions, tapered funnel stages, radar spokes and a wider gauge. Match the v4 chart showcase's ring, strip and value geometry.
- 01dace1: Match the v4 hand-off's specialty chart presentations: detailed goal rows, metric breakdowns, status panels and outlined city maps; precise activity calendars and trend strips; and responsive metadata tags through Chip's small prop. Preserve chart selection, keyboard navigation, status summaries and Reduced Motion while restoring hover transitions.
- 3bdfff0: The Charts section cards' pieces (justification for the minor: new public options). The chart value flag is the inverse ink pill (the Tooltip bubble's reading, mono 11 px, the `chartFlag` corner, the dense inverse tint under glass), and the cartesian charts mark the inspected category on the axis in the card ink with a dashed `3 4` guide. New options: `Chart` `formatValue`; `ChartSeries` `dashed` (LineChart, ComposedChart, CandlestickChart overlays, with a dashed legend rule); `CandlestickChart` `legendCaption`; `ProgressRing` `stacked`; `MetricBreakdown` `hideSparkTag`; `BulletChart` per-row `success` / `warning` / `destructive`; `DepthChart` `spread`; `UptimeBar` `midLabel`. Looks taken from the hand-off: the selected bar's gradient and glow, the ring and gauge arc glows, the completed ring's check, the radial bar's first series on the outer ring, the radar's quarter rings, the range area's selection marks, the scatter plot's solid selected point, the box plot's half-width whisker caps, the sparkline's range-scaled bars, the calendar heatmap filling its width, the treemap's 2:1 tiles and near-black ink, and the geo map's stacked zoom control.
- 2ad40dc: Adds Chip's `href` and `hrefAttrs` props (justification for the minor: a new public option, a chip that is a link). A chip with `href` is one link to its destination: a real anchor on the web, so middle-click and open-in-new-tab work and assistive tech hears a link, with `hrefAttrs` forwarding `target`, `rel` and `download`; natively it pairs with an `onPress` that navigates through the app's router, as Button's `href` does. `onPress` now receives the press event, so a web app routing on the client can take the click. A hovering pointer turns a link chip's ink and edge to the accent (the Canvas v4 hand-off's component chips) instead of brightening it. While `disabled` the anchor is dropped, and `href` is ignored on a selectable chip or a ChipGroup option, which stay toggles.
- 580288f: Minor: the hand-off's token chip and icon tile. Chip `token` is the tag of a colour role (a 32 px pill in the card fill inside the hairline, a 500 12 px mono label, 6 px beside its 20 px swatch), and Chip `square` holds an `option` chip to the 30 px tile an icon-only option takes, its label the tile's 500 10 px mono line. Swatch `tiny` is the 20 px disc that chip carries, and Swatch `ring` draws the edge as a 1 px inset ring in the field border in place of the hairline.
- 0353907: CodeBlock gains `unbadged`, a new public option that keeps the header bar and `language` highlighting but draws no trailing language badge beside the copy chip, for a `filename` that already names the language (the hand-off's playground bar is "JSX · ButtonProps" and a Copy button only). Justification for the minor: a new component prop.

  Colour correction, carried in the same release: a tsx or jsx snippet now takes the hand-off's component-page highlighter (Component.dc.html `hl`) instead of the ts grammar. Only a Capitalised tag name is painted, in the accent text ink, with the brackets and slash neutral; an attribute name is cyan only before its `=`; strings stay mint and comments the subtle ink; keywords, numbers, function names, lowercase tags, bare boolean attributes and children text are the code ink. So `<Button>Save changes</Button>` reads as one label after one accent name instead of a cyan `<Button>Save` beside a white `changes`. The ts and js grammar and the other languages keep their rules.

- 01dace1: The Controls section's cards take the v4 hand-off's look (justification for the minor: new public options on existing components). New options: Button `rounded` (the pill on a text button), `dashed` (a dashed edge) and `unread` (the accent corner dot); ButtonGroup `attached` (one edged box of divided full-height cells), an item's `display` node, and the stepper's `decrementIcon` / `incrementIcon` slots; Radio `meta` (a trailing note on the title's line) and a card radio's `icon`; RadioGroup `equal` (equal-share cells in a `row`) and `inlineLabel` (the label at the start of a `row`); Switch `listed` and `divided` (a settings list's row inset and the hairline between rows); Slider `percent` and `scale`; Stepper `suffix`; Avatar `online` (the presence dot); AvatarMenu and Dropdown `inline` (the menu drawn open in flow, a specimen); BadgeGroup `compact`; Typography `italic`; DataTable `matrix` (the specimen grid) and a column's `fit` (the widest cell's width).

  Skin changes on the web: a Button label's line equals its size; the split button and the stepper take the hand-off's cells, glyphs and boxed stepper; a checkbox's and a radio's label and description take the 14 and 12 px lines; a chosen card radio takes the accent edge over the soft accent wash; the Stepper's end cells are as wide as the control is tall; the AvatarGroup "+N" count is 600 13 px; and the AvatarMenu capsule takes the hand-off's hairline edge round the popover fill. BadgeGroup's default gap is now the hand-off's 6 px (`compact`); `snug` keeps the 8.

  The generic AvatarMenu now follows its component renderer: a 32 px disc (28 px compact), the reference identity hue assignment and flat initials paint, 13 px / 11 px identity text with a 2 px gap, and no disc glow. Compact leaves only the avatar. The original account composition keeps its own metrics.

- d1c3ef4: Two new options for a page header, the user-visible capability this minor adds. Breadcrumb
  takes `subtle`, a quieter trail with the ancestors in the subtle ink and the current page at
  the regular weight. Button takes `pageLink`, a 34 px hairline link around a muted label, and
  `mono` to set that label in the 400 12 px mono face for a source path. The component detail
  page's header now draws both, as the v4 hand-off does.
- 092e9b6: Minor: three new options for a documentation page. Button `jumpLink` is a page's jump to one of its sections (a 28 px hairline pill around a muted 12 px label), and Button's page links now brighten their label to the full ink on hover. Chip `option` (and ChipGroup `option`) is the compact pick of a settings panel: 26 px tall, a 12 px label, no check. Typography `pageLead` now holds a page summary to the 72-character measure.

  Typography gains `pageBody` (15/1.6) and `pageNote` (14/1.6), a page's own prose roles held to the hand-off's 80-character measure.

  CardContent gains `dense`, `compact` and `cozy`, the tighter insets of a caption under a media well.

- f605b31: CodeBlock gains `headerless`, a new public option that highlights by `language` without drawing the language or filename header bar (justification for the minor: a new component prop). Fixes found building the v4 component detail pages: a nested Text now keeps the face of the run it sits in, so CodeBlock's highlighted tokens paint in the mono face instead of the interface face once an app registers fonts; a DescriptionList node value yields to its cell, so a wrapping Row in a `twoColumn` list wraps instead of overflowing the card; and the Select selection checkmark is decorative, kept out of the option's accessible name on web, iOS and Android.
- 2ad40dc: Adds the `embedded` boolean to Dialog, AlertDialog and Wizard (justification for the minor: a new public option, an open dialog shown as a non-modal specimen in a page). An embedded dialog keeps its look, its dialog or alertdialog role and its labelling, but it takes no focus when it opens, traps no Tab, returns no focus, sets no `aria-modal` or `accessibilityViewIsModal`, and does not cancel on Escape, native back or a backdrop press, so a page of open specimens loads at the top with the keyboard order intact. It is ignored with `trigger`, which always opens a live modal; a Wizard keeps presenting into the nearest OverlayProvider. The docs' open Dialog examples and the Overlays section's dialog, alert dialog and wizard specimens now use it.
- 5d7feb0: The Foundations section cards' pieces (justification for the minor: new public options on existing components). Swatch takes the `tile`, `bar` and `steps` forms (with `segmented`), and the `fluid`, `glow`, `centered`, `bare` and `compact` lockups, with an `ink` for a tile's name; its name and value lines sit on their faces' normal lines (16 and 14 px). Icon takes the `subtle` tone (the second ink) and the `cell` glyph tile with its web hover wash, and `muted` now paints the third ink as the hand-off's renderer does. Emblem takes the `cyan` and `secondary` tones, Card the `sample` elevation tile and `floating` (the floating glass material on the web), and Badge `dot` with no label draws the bare 8 px dot.
- e7146c7: Add Input compact density for outlined form wells with a 40px default height, 14px value, platform field corners and independent small and large sizes.
- 2a07de8: The Inputs section cards' kit pieces (justification for the minor: new public options, Autocomplete option details, `FormSection aside` and Form `hints`).

  - Autocomplete `options` take `{ label, detail }` objects beside strings: the detail sits at the row's end in the 11 px mono face in the subtle ink, and the row is announced as "label, detail". The web field ends in a 15 px chevrons-up-down 10 px from its end, and the toggle inside a pinned-focus field no longer draws a ring of its own.
  - `FormSection aside`: the heading in a rail beside the rows (the `xs` step, 48 px gutter, a 20 px display title over a 14 px description, rows 18 apart, 28 above and below, a closing rule), stacking at 512 px or narrower. Form `hints`: leading content in the actions row, which then spreads hints and buttons to its ends; after a closing aside section the actions row drops its rule.
  - Input: the web password eye shows the open eye while masked and the struck eye once revealed; a field held focused by InteractionOverride draws the web's 1.5 px accent caret after its value, blinking on the loop primitive.
  - Listbox rows at the hand-off's 18.2 / 16.2 leading; Switch title and description at 18.2 / 17.4; the PhoneInput country row's 16 px flag at a 10 px gap with the dial code in 12 px mono; the FilterPanel's Clear a plain accent text action and its web option label 400 14 px, 10 from the box.

- 0c6a050: Two docs-specimen seams. `InteractionOverride` pins hover (`hovered`), keyboard focus (`focused`) or the pressed state (`pressed`) for every kit control in its subtree, through the hover primitive, the kit Pressable's focus ring and press state, and a field's own focused look, so Button, Input, Checkbox, Switch, Pagination and the rest show the pinned state with no prop of their own; it pins the look only, leaves a disabled control at rest, and costs a context read without a provider. `AnatomyProvider` reports the rectangles of the named parts of the components inside it, relative to its own box, through `onMeasure`; Button registers its `Container`, `Label`, `Leading icon`, `Trailing icon` and `Count badge`. Minor: new public components `InteractionOverride` and `AnatomyProvider` with their props and rectangle types.
- d2da109: The Canvas v4 Layout page's cards get the kit pieces they lacked (justification for the minor: new public options on Badge, MediaObject, DataTable and Form). Badge takes `contrast` (the ink as the fill under the page colour) and `dashed` (the placeholder outline) on its tone axis, `tick` (the 20 px ruler readout) and `step` (a 32 px numbered square in the tone's fill, the solid one glowing) on its metric axis, and `keyCorners` (the key cap's 6 px corner on any size); a `band` or `tick` pill no longer leads with a tone's dot. MediaObject takes `bareIcon`, the `icon` drawn with no box, for a `<Badge step>` lead. DataTable takes `bare`, the unframed hairline table. Form takes `block`, its actions splitting the row equally. A MediaObject `body` that is a node (not a string) lays out as a block instead of inside a text run. Divider's label sits on JetBrains Mono's normal 14 px line, the hand-off's.
- 1039627: Navbar gains `brandHref` and `onBrandPress`: the brand lockup becomes one link to the app's home, a real anchor on the web (middle-click and modifier-click open a new tab), named by `brand` and drawn with the kit's focus ring. Minor: two new public props on Navbar.
- 0c6a050: Navbar takes the v4 hand-off's web look (a 56px bar, 32px capsule links on the `secondary` fill, a floating pill on the stronger hairline with the md shade) and gains three new public options: the `compact` density (the docs pill: 44px, 5px in, 2px between items), `scrolls` (the content row scrolls sideways inside the bar instead of folding the links into the narrow menu) and `hrefs` (links render as real anchors on the web while `onSelect` still fires, now with the press event). Minor: new public props `compact`, `scrolls` and `hrefs` on Navbar.
- 75989f1: The Navigation section cards' pieces (justification for the minor: new public options). Badge takes `tiny`, the 18 px pill at 400 10 px (precedence `token` > `tiny` > `small`). SidebarItem takes `badgeAccent`, a solid tiny accent tag such as "New", and Sidebar takes `account` (`SidebarAccount`: a name, an optional detail and an optional avatar source), the account card at the rail's foot. Tabs items take an `icon`, a leading glyph in the label's ink. TabBarItem takes `unread`, a dot over the glyph that also names the destination "unread". Steps takes `segmented`, equal columns each with a bar, a title and a status (precedence `progress` > `segmented` > `vertical`), and a step takes `progress`, an in-step bar on the current step. RowMenu takes `inline`, its menu drawn in place as a panel, and `inline links`, a grouped link list; its items take `shortcut`, `submenu`, `badge` and `meta` trailing hints.

  Look changes on the hand-off's renderers: a SidebarItem's `meta` reads in the third ink in mono; a `block` underline Tabs row splits into equal columns; a Tabs count badge takes its tab's label ink; Pagination's gap is a quiet text ellipsis; the web TabBar floats 12 px off its container's sides and bottom; a vertical Steps title is 500 14 px over a 12 px description in the accent text ink on the current step.

- d1e96c9: The Overlays section's kit pieces (justification for the minor: new public options on existing components). Alert takes `announcement`, the full-width product bar on the accent's gradient, and `action`, one inline link; Dialog takes `banded` (header, body and footer bands under rules) and `icon` (the accent's soft tile above the title); Drawer and ActionSheet take `embedded`, the open-in-place specimen that is a labelled region with no focus trap, no Escape or hardware back and a dim that passes touches through; Dropdown items take `submenu`, and a `selected` item draws the web's soft wash and leading check; Progress takes `status`, a short word in the percent's place; Emblem takes `soft`, the tone's soft wash tile; Card takes the `well` density (16 inset, 12 apart); a palette Swatch takes a size boolean for its disc (`small` is 32 px); an inline Popover given `top` or `bottom` draws its beak, at the end with `alignEnd`. The web looks follow the Canvas v4 Overlays page: AlertDialog and Toast float on the functional glass layer, AlertDialog's default width is the `sm` step and its confirm field carries its name as a placeholder, the Dialog close is the 28 px transparent box, the Wizard is 672 wide with segmented Steps in a banded header, the Command palette's `esc` hint is the compact flat cap and its list caps at 340, the alert and toast dismiss boxes round at 6, and the Spinner description sits on a 17.4 line.
- e599e52: Three new options close the docs playground's measured drifts against the Canvas v4 hand-off (justification for the minor: new component options). ButtonGroup gains a density axis with `compact`, the hand-off component page's platform switch: the segmented row's segments keep their size's height and take the 12 px label on a 12 px inset, on the web (iOS and Android ship the control and keep its one density; the other kinds ignore it). CodeBlock gains a density axis of `compact` and the new `playground`, the hand-off page's JSX card: the block at the card corner, its header a 40 px bar on the card fill 16 in and 8 out, its Copy a 28 px hairline button at the page's copy-button corner, and the code 16 above and below and 18 in at the default ramp. Typography gains the `knobLabel` page role, the knob panel's label row: 500 12/1 in the secondary ink, its type hint in the `muted` tone (the third ink), `mono` for the face.

  Value corrections, carried in the same release: the plain accent link's line is the face's own `normal` at its size, as the hand-off sets no ratio (16 px at 13 px, 15 px with `small`, measured on the served hand-off page), so a panel's Reset row is 15 px tall; and CardMedia's top corners now nest inside the corner the Card around it draws (a `stageCorners` card's well rounds inside the 20 px stage corner), not always the default card corner.

- b7a01ad: Two Typography page roles for the v4 component page's reference sheet (justification for the minor: new public options). `specMeta` is a props table's type or default cell, 400 12/1.55, taking the caller's `mono` face and `subtle` or `muted` ink; `specText` is its description cell and the inherited-props note, 400 13/1.55 in the secondary ink, and `code inline` beside it sizes the prose chip from it. `Collapsible` gains `reference`, the sheet's own disclosure card (a sub-component's props table): the card corner on a `border` hairline, a 46 px toggle with the 600 14 title and its description inline in the mono meta ink, and the content flush to the card so a `reference attached` table's rows meet its edge (the web and Android skins; iOS keeps its own disclosure group). A `DataTable` group band's label sits on the face's own 16 px line, as the hand-off's band measures, in place of 18.
- df530e9: Row children take `weight`, a share of the Row's free width after its gaps (CSS grid's `minmax(0, Nfr)`), for the splits twelfths cannot name (justification for the minor: a new public option). `weight={1.7}` beside `weight={1}` splits a Row 1.7 to 1 whatever either side holds, the Canvas v4 component page's anatomy figure and legend. A weighted box has no content floor, replaces `fill` and `grow` on the same box, and is the whole line once a `stacks` Row has stacked; DEV warns for a weight outside a Row, a weight that is not a positive number, and a weight combined with `span`, `fill` or `grow`.
- e016562: The Canvas v4 section pages' shared card vocabulary lands in the kit (justification for the minor: new public options on existing components, which the section cards of every docs section draw).

  - `Chip link`: the section card's link pill, 26 px tall at the full round, 10 px sides, a 500 12/12 label in the secondary ink on the popover fill inside the field-border edge, the accent ink and edge on hover; a press keeps the resting look (on the web and on iOS, which shares the web skin). Look precedence `token` > `option` > `link`. Android keeps its Material 3 assist chip.
  - Typography roles, one per value set the section cards use: `cardNote`, `codeLabel`, `inlineNote`, `tileTitle`, `paneTitle`, `gridLabel`, `tick`, `rowLabel`, `rowValue`, `headlineValue`, `bigNumber`, `unit`, `sampleLabel`, `sampleMeta` and `base`; the `natural` leading modifier, the face's own normal line at the role's size (measured in Chromium on the hand-off's faces); and the `cyan` and `mint` tones, the fixed series hues as inks solved to 4.5:1 whatever the palette.
  - `Badge cyan` and `Badge mint`: the fixed series hues as soft pills (the hue's 16% wash under its solved ink), after the status tones in precedence.
  - `Card sunken`: the `surface-sunken` well under the hairline with no shadow, on the elevation axis after `flat`; the Material 3 filled card on Android.
  - `Kbd small` (the 22 px cap) and `Kbd tiny` (the 20 px cap), a size axis over the 24 px cap, and `Kbd flat`, the hint cap's one-pixel edge in the secondary ink.
  - `Input shortcut`: a keyboard shortcut hint at the field's end, drawn as `<Kbd small flat>` (`tiny` on a small field), 8 px from the end and 10 px from the value (8 on a small field), on the framed field and the toolbar search alike.

- 25cb216: Adds the steps the Canvas v4 section pages' showcase frame is built from (justification for the minor: new public options). Card takes two corner roles beside the component page's, `showcaseCorners` (20 on the web, the hand-off's section page card) and `heroCorners` (24 on the web, its section hero's stage), the native skins keeping their own card corner on them as on the component page's steps; a `roomy` density (a 24 px inset with 20 px between flat children, the section card's, on every platform); and `heroShadow`, the medium step of the shadow ladder (the hand-off's `--sh-md`) between the resting card and the raised one. Row, Column and Grid take `ample` (40), the step between a section page's hero and its body and between the hero's halves, under `broad` (48), and Row and Column `rowsPanel` (28), the vertical gap of a wrapped or stacked row apart from its main gap.
- 2ad40dc: Adds Sidebar's `accessibilityLabel` prop (justification for the minor: a new public option, a name for the sidebar's navigation landmark). A sidebar is a navigation landmark on every platform, and a page holding two of them (a sidebar beside the app's top navigation, or two sidebars) needs a distinct name on each; the label names the landmark in every frame (flush, bordered, floating, embedded and the header/footer shell), in solid and glass alike, and labels the narrow `responsive` drawer. GlassSurface takes the same optional `accessibilityLabel` to name the landmark its `role` declares. Omitted, both render exactly as before.
- 0c6a050: Sidebar takes the Canvas v4 rail on the web (232 px sunken column, 36 px rows at a 9 px corner, 30 px at 8 when `compact`, the accent's soft wash on the active row, a mono eyebrow over each group; the 3 px edge bar is gone) and the soft accent pill on Android. Minor because it adds a new public capability: the `markers` boolean (a trailing accent dot on the active row, a hairline dot on rows of the current place), the item `marked` field, the section `current` and `count` fields, a `floating` frame with its own lift, and header/footer slots that stack as plain blocks in an `embedded` rail.
- 01dace1: Add capsule ButtonGroup tracks with a sliding selection thumb and a compact Switch chip for toolbar settings. Restore the web Switch thumb's spring transition. Both controls respect Reduce Motion.
- 0c6a050: Swatch: the palette picker takes the v4 palettes. `violet`, `cyan`, `mint`, `rose` and `amber` (ThemeProvider's precedence) render the hand-off navbar's 16 px hue disc, a radio named for its palette, ringed when `selected` and growing on web hover; `blush` and `dark` are gone with the Dark Factory palettes (part of the 4.0.0 redesign, released separately). Minor justification: new public picker props for the five v4 palettes.
- 6ce2783: Four new Typography capabilities, the user-visible additions this minor carries, all
  from the v4 hand-off's component page. `code inline` is the prose code chip, a word of
  code inside a sentence: regular weight at .88 of the reading role passed beside it
  (`lead`, `small`, `pageBody`, `pageNote`, `pageLead`, `sectionLead`; body when none), a
  5 radius, 1 by 5 padding, the surface fill and a hairline rim, the hand-off's inline
  `<code>` from its Markdown helper; `<Typography code>` is unchanged. `pageLabel` is the
  page's own label (a panel's "Props", a rail's group header, a table's header cell, a
  previous/next eyebrow): 500 11/1 mono tracked .08em in caps, in the subtle ink, where the
  `eyebrow` stays the type table's accent-ink label. `guideTitle` (600 16/1.3, a level-3
  heading) and `guideBody` (400 13.5/1.6 in the secondary ink) are the Usage guidance
  card's title and paragraph. The docs' inline Markdown sets every code span on the chip at
  its paragraph's size and the accessibility cards, the knob panel and the previous/next
  cards take the page label.
- f87df2d: The v4 component page's Anatomy figure. `AnatomyProvider` takes `zoomed`, which draws the specimen at the hand-off's anatomy zoom (1.6, a component that hugs its content): the provider fills its stage, lays its inner box out at the stage width over the zoom and scales it back up about its centre, reserves the scaled height so the stage grows with the figure, and maps every part rectangle into the space the specimen is seen in before reporting it; `onMeasure` also receives the provider's own box as its parent laid it out, so a consumer places markers against one source. Badge takes `marker`, the figure's numbered disc: a 22 px pill with no inset, its number 600 11 px in the code face on the accent, ringed by the stage well's fill and shadowed. Minor: the new `zoomed` option and box report on `AnatomyProvider`, and the new `marker` metric on Badge.
- 0c6a050: Three backward-compatible capabilities for the v4 component detail page. New public API: `CardMedia` takes `children` and stages them centered in a sunken specimen well (an inner fill under glass), sized by the new `compact` and `tall` booleans; `DescriptionList` items take a ReactNode `term` and `value` beside the existing strings; `DataTable` takes `groups` (titled bands with an optional adornment and count above each group's rows, exported as `DataTableGroup`) and per-column `grow` weights and `minWidth` floors that mix with fixed widths and pan the table once the floors exceed its width.
- e9a44b5: Adds `ChipGroup`, a pick-one set of chips (justification for the minor: a new public component). Its `<Chip value="…">` options form one WAI-ARIA radiogroup: radio chips with `aria-checked`, one roving tab stop (the chosen chip, or the first enabled one), the arrow keys moving and selecting with wrap, Home and End, and disabled chips skipped, through the shared roving-focus hook RadioGroup and Tabs use. It follows RadioGroup's contract: `value` / `defaultValue` / `onChange`, `disabled`, a `label` heading with a `description`, plus `accessibilityLabel` to name a group whose heading is already on the page. `Chip` gains the `value` prop that makes it an option.

  Chip, Kbd, Radio, Slider, Stepper and Avatar take the Canvas v4 hand-off's look. Chip is the 28px outlined pill at weight 500: a palette hue is its 16% oklch wash inside the same hue at 30% under an ink held to 4.5:1, `primary` the solid accent, `secondary` the higher raised fill, a selectable chip at rest the bare pill in the field border and selected the accent wash inside an accent edge, with a mono count and an 18px remove disc; Material 3's 32dp chip keeps its shape on Android. Kbd is the 24px mono keycap with a 2px bottom edge; a combo sits as caps with no "+", a sequence reads "then" between caps. Radio draws the hand-off's thick-ring mark on the web and keeps the iOS checkmark list and the Material 3 radio, with the hand-off's gaps. Slider is the 4/6/8px raised track with a glowing accent fill and a white thumb that takes an 8px accent halo under the pointer or a press. Stepper is one bordered 10px-corner box with a mono value and its description under the control. Avatar's sizes are 20/28/40/56 (`tiny` was 24 and `large` 48), `rounded` corners are 28% of the disc, initials sit at weight 600 over the identity hue's soft wash with an ink held to 4.5:1, and the stock `glow` is a page gap inside the accent glow; Argus's reference sizes keep their gradient.

- c7a0d40: v4 Layout and Foundations skins. Minor: a plain `Swatch` now takes `selected`, the hand-off's selection ring (2px page gap, 2px accent) with "selected" in its accessible name. Swatch takes the hand-off's 32/48/72 chip and 13px name over 11px mono value lines in the subtle ink; Emblem the 28/40/56 tile at 28% corners with an 18% tone wash, a 35% tone ring and a 600 display monogram; Divider's label the 500 11px uppercase mono caption in the subtle ink; DashboardGrid cells 12px apart (8 compact) with a dashed accent ring in customize mode; a `primary` Icon paints the accent's text ink.
- 0c6a050: Three backward-compatible capabilities for the v4 component detail page. New public API: a `chapter` gap step (64) on Row, Column and Grid; a Typography `level` that sets the announced heading level without changing the look; and `CodeBlock inline copy`, an inline pill that copies its code.
- 3b99535: Closes the component page's small geometry drifts against the v4 hand-off, measured element by element on the served hand-off and the shipped page. New public options (the justification for the minor): Badge `disc`, a legend's 22 px numbered disc in the tone axis's fill with no ring or shadow, and Badge `tier`, the 24 px tier pill beside a page title; Typography `pagerLabel`, `legendLabel` and `tileCaption`, three page roles the hand-off sets with no line ratio, each on its face's own measured normal line (14, 18 and 16 px). Value corrections: the Breadcrumb's crumbs sit on the hand-off's 16 px normal line instead of 18, so a page header no longer runs 2 px tall; a Row span cell and a Grid cell keep their width to a hundredth of a pixel instead of a whole-pixel floor, so tiles and pager cards sit where the hand-off's tracks do; CardMedia's `stage` well is 318 inside its hairlines, the hand-off's 320 border box.
- 075c58a: Divider `inset` and Navbar `brandDivider`: new public options. A `vertical inset` Divider is the hand-off's short 1 x 18 px separator centred in its row with 4 px on each side, and `brandDivider` draws one between a Navbar's brand and its links. Divider's emphasis now follows the hand-off (the `border` hairline by default, `field-border` when `strong`, the hairline at 60% when `soft`), and the web ButtonGroup segmented control takes the hand-off's look: a 10 px `secondary` track with a 3 px inset, 7 px segments, a flat `card` thumb on the sm shadow, 500 labels, 30 / 36 / 44 px tall, 8 px padding for an icon alone; under glass the group takes the control layer and its selection puck the segment's corners.
- 4aec6eb: Minor, for two new public options:

  - `plain` on Chart, LineChart, AreaChart, ComposedChart, RangeAreaChart, Histogram,
    BoxPlot, ScatterPlot, WaterfallChart, Treemap, FunnelChart, BulletChart, RadarChart,
    DepthChart, CandlestickChart and GeoMap strips the chart's own card surface so it can sit
    inside a card that already supplies one, as BarList, ServiceHealthList, MetricBreakdown
    and Stats already allow. It is the v4 hand-off's chart frame `plain` mode.
  - `apart` (32) on Row, Column and Grid: the gap between the side-by-side panels of one
    composition, the hand-off's widest grid gap. It ranks under `chapter` and over `page`.

- f126d5f: Three backward-compatible capabilities on `DataTable`, for the v4 component detail page's props table (the hand-off's Component.dc.html lines 233-241). New public API: `reference` draws the reference sheet, the column headings as the page's small uppercase letter-spaced mono label in the subtle ink on a header row with no fill, 12 px above and below, rows 10 px above and below with a rule above each, the columns 16 px apart and 16 px in from the frame's sides, and the frame at the panel corner on the `border` hairline over the card fill (the web and Android skins; iOS keeps its own table); `alignTop` holds every cell at the top of its row, with the selection box and the row actions centered on the first line of cell text; and `footer` takes a ReactNode row drawn inside the table's frame under the last row and the pagination bar, 44 px tall with its content centered, separated by one hairline. The docs' props table uses all three and moves its "Show all N props" control into the footer.
- c7a0d40: Adds `DropZone`'s `bordered` prop (justification for the minor: a new public option): a bordered zone is the Canvas v4 hand-off's dashed drop well with its 11px mono label, lit in the accent wash and edge while an item hovers it; without it a zone keeps the bare overlay it had.

  The Surfaces & data components take the Canvas v4 hand-off's look. Stats is the 16-corner card tile, 18 in (12 compact), its 600 28/20/36 figure in the display face tracked -0.02em over a 500 12 subtle label and a mono delta; the plain surface sets the tiles edge to edge in one card behind hairline rules. DataTable is framed by default at 14 (`attached` drops the frame) with a 500 12 muted header band on the popover fill, 400 13/18 cells and the accent sort arrow; Android takes that table with its 48dp targets and the ripple, iOS keeps its SwiftUI Table. StackedList frames at 14 under the small shade with 16 by 12 rows, a 500 14 name and a mono meta. GridList's gallery tile is a 14-corner card with a 110 thumbnail over its caption. MediaObject leads with the 40px icon box at 11 on the accent wash. Feed draws 400 13/19 lines with a 600 actor and a 12 subtle timestamp beside its node on a field-border connector: an item's glyph on a 28px secondary disc, otherwise a 10px dot (the first in the accent) where it showed the actor's initials, and the avatar lead's 28px avatar on the same connector rows where it had hairline-ruled rows; its activity and trace times are the 12 mono caption. DescriptionList is the inset card, 16 by 20, rows 12 above and below with a 500 13 muted term beside a 400 14 value under a 500 16 title, and Android draws that list; iOS keeps its grouped value rows. ActionPanel is a 16-corner panel 18 in with a destructive wash; Accordion is the 16 card 18 across; Collapsible a 14 card with a trailing chevron; CodeBlock a 14 card with a 36px mono header; Carousel has 32px arrows and accent pill dots; Video and QRCode sit in a 14 frame; Board lanes are sunken 14 wells holding 10-corner cards; Chat is a framed thread with soft 16 bubbles tailed at 4 and a sunken composer well; Calendar has 38px cells with a ringed today. Neither iOS nor Android ships a control for the stats, feed, lists, media object, panels or board, so their native skins draw the hand-off's parts in SF Pro and Roboto with each platform's press feedback.

- 08b3ded: Five new capabilities from the v4 hand-off's component page, the user-visible additions
  this minor carries. Typography gains three page roles, each in the interface face and the
  foreground ink: `variantTitle` (a variant card's title over its note and snippet, 500
  14/1.3, Component.dc.html line 185), `linkName` (a link tile's name beside its arrow, 600
  14 on the face's own 18 line, line 297; the hand-off sets no ratio, and the line is
  Chromium's `normal` for Instrument Sans measured on the hand-off page) and
  `subsectionTitle` (a section's own sub-heading over a grid of tiles, 600 16 on the face's
  own 20 line measured the same way, line 296; a level-3 heading). CodeBlock gains two
  members on its density axis, after `compact` and `playground`: `snippet` (the page's own
  snippet, line 283: the code-card corner with the code 16 above and below and 18 in at the
  default 13 / 1.7 ramp) and `guide` (a card's snippet, line 288: a 10 px corner, the web
  row's new `cardSnippet` role, with the code 12 above and below and 14 in at 12 / 1.65).

  The docs' component page takes them: a variant card's title is the `variantTitle`, the
  Usage section's block is a `snippet` and each guidance card's block a `guide`, the
  guidance card's title and paragraphs are the `guideTitle` and `guideBody` roles (its
  inline Markdown and lists accept `guideBody`), the Tokens section's "Related components"
  is the `subsectionTitle` 32 below the chips and 16 over its grid, and each related tile
  draws its name as the `linkName` over its room as the caption in its own subtle ink. The
  Button docs' "Icon ink per intent" row wraps, so its six buttons fold inside a narrow card
  instead of running past its edge.

- e7146c7: Restore Wizard's fixed step sidebar, header status and opposing footer actions from the v4 hand-off. Add compact vertical Steps and Dialog header/sidebar slots while preserving live modal focus, controlled navigation and independent body scrolling. Embedded Wizard panels retain their opaque in-page appearance without a modal veil.

### Patch Changes

- 01dace1: Allow board cards to open active AI work through an independently accessible progress action while retaining passive progress and reduced-motion behavior.
- 01dace1: Remove the empty board-card row between its status and work type while preserving aligned content and AI progress.
- 55d9b11: Show board-card metadata as values without visible field prefixes, use red text for High and Critical priorities, and combine blockers into readable status text such as “Blocked by Discovery”. Keep field context in accessibility labels and preserve card geometry.
- 01dace1: Match the v4 Charts room's Cartesian specimens to their authored drawing proportions, marks, legends and initial readouts while preserving normal chart defaults and real inspected values.
- 01dace1: Match the v4 hand-off's literal accent and status inks, hairline control boundaries and translucent focus rings across all five palettes and both schemes. Keep keyboard and focus behavior while using the authored color values.
- 2ad40dc: Fixes Form's `twoColumn` flow to be the two-column grid it documents. Above its 512px threshold it laid out as many 200px-floored cells as fit on a line (five across a wide card) instead of two; it now places exactly two equal columns 16px apart, keeps an odd last field at half width, and starts a new line after a FormSection, which takes the whole row. At or under the threshold it switched to a wrapping column in which a FormSection's `flexBasis: 100%` became a height basis, wrapping the fields into off-screen columns that hugged their content; it now stacks one full-width column, every field filling it, with nothing scrolling sideways.
- 5f4d03b: A tappable GridList people tile that carries action buttons is a clickable container rather than a button, so its actions are no longer buttons nested inside a button (invalid on the web and unreachable as separate controls to assistive technology).
- 2503ce3: A Grid in a container whose width is a fraction of a pixel (a cell of another grid) keeps all its columns on one row: the cells now split the measured width less the half pixel its whole-px rounding may add, where three cells cut from a rounded-up 325 overflowed a 324.66 px row and the last one wrapped.
- 0c6a050: Image: `repeat` tiles a bundled asset at its point size on every platform and draws the density variant that matches the screen, instead of react-native-web sizing the tile from the bitmap's pixels.
- 711ae41: Run GitHub validation and deployment only for manually requested releases, keeping the complete release checks without starting CI on routine commits or pull requests.

  Allow release preparation to finish a cold documentation build and consumer verification before its job times out.

- 01dace1: Render board ticket type then priority together in compact theme-aware Badge pills below the status, preserving the fixed metadata row and accessible field names.
- cf5e79a: Give release browser validation enough time to complete slow runner setup and the full interaction suite without dropping checks.
- 1039627: Sidebar: a row takes `meta`, quiet trailing metadata set in the mono face after any `badge` (an ordinal, a shortcut or a short status) instead of a pill. The docs' Templates list numbers its rows with it.
- 0c6a050: Escape hides an open Tooltip without moving focus, and dismisses the Toast that holds focus (returning focus to where it came from), through the kit's shared Escape owner; on iOS both answer the accessibility escape.
- 3dc41fe: Card's linked-reference modes take the v4 hand-off's look: `referenceInline` is a 36 px
  bordered pill (the open glyph, the key, the summary, then the status), `referenceStacked` a
  bordered box up to 320 wide with the key, summary and status stacked, `referenceTile` a
  180 by 120 tile with the key at the top and the summary at the bottom, and
  `referenceDetail` a box up to 360 wide with the source chip beside the key. Every mode sits
  on the card surface under a hairline with the small resting shadow, the key in the
  accent's mono ink. The props are unchanged.
- 24f69e7: Chart draws the hand-off's bars: vertical columns stand on the shared cartesian frame (the quarter grid, y labels at 0, half and the top, at most 28-unit columns and 14-unit clustered bars 2 apart with a 3-unit corner) and horizontal rows take its label column, bar travel and trailing value. The cartesian frame takes the hand-off's value axis (0 to the nice bound above 102% of the data, gridlines at its quarters), its axis numbers (whole below 10,000, then "12.5k"), and on the bar, line, area and composed charts its 360 by 170 proportion up to the standard plot height; `compact` now tightens only the frame, as the hand-off's does.
- c7a0d40: The charts take the v4 hand-off's look (its chart renderers in `cv-kit-charts.js`), on every platform. The chart frame pads 16 (12 compact) with a 500 14 title; axis labels are JetBrains Mono in the subtle ink, the y axis labels every other tick, and the grid is a 2 4 dotted rule with a solid zero line and no vertical lines; legends draw 8 px rounded squares in 12 px text. Bars round every corner at 3 and fill 46% (grouped) or 42% of the band; lines stroke 2.2 for the lead series and 2 for the rest, with 3 px dots and a .35 fade; areas wash at .28 and .3. Pie and donut, gauge (a radius 80 semicircle with a 0 / label / 100 scale), progress ring (168, a 14 stroke, the label beside it), radial bars, radar (field-border rings, no spokes), treemap (tiles mixed 70% over the card, with an ink that clears 4.5:1), heatmap (five chart-1 levels, 2.5 corners), candlestick, box plot, waterfall (no connectors), scatter (r 4 at .7), histogram, range area and depth chart follow the hand-off's numbers. FunnelChart draws centered rows (label, bar at its share of the first stage, mono value) in place of trapezoids. BarList draws each row as a proportional tone wash behind its label; MetricBreakdown stacks label, display headline and rate beside its trend strip over mono meter rows; StackedBar is an 8 px pill with 2 px between segments and an inline legend; UptimeBar takes a status dot header and 30 px pills; ServiceHealthList rows carry a 14-period inline strip and a status-toned mono detail, also when `compact`. No public props change.
- e9a44b5: The Inputs take the Canvas v4 hand-off's look (no new public props). Textarea: the sunken well with the hover edge and the focus halo, a 1.55 line, four rows by default, the 11 px mono count; iOS keeps its card box with a tabular "N/max" count, Android its M3 filled field at a 16 dp inset with the count in the supporting-text row. Select and Autocomplete: the 32/40/48 sunken trigger, the 34 px menu rows with a trailing accent check, the soft-wash match highlight. Listbox: one look on every platform, the selected row on the accent wash with a trailing check (multi-select no longer shows a Checkbox). InputOTP: separate 36/44/52 sunken cells with the accent halo on the active one. PhoneInput: the country chip, dial code and mono number inside the field. Field, Form and FilterPanel: the 13 px muted label, the subtle message with a leading alert glyph on error, the ruled actions row with a ghost Cancel, the ruled FormSection, and the 280 px filter panel with mono eyebrows, plain counts and a "Clear N" link.
- e9a44b5: Tabs, TabBar, Breadcrumb, Steps and RowMenu take the Canvas v4 hand-off's look. Tabs: the web underline row (24 apart, a 2px glowing accent bar, 14px labels), the segmented pills track with a raised card thumb, and the accent-washed vertical rail on the web (iOS draws its segmented control, a separate changeset). TabBar: the floating 62px capsule with an accent wash behind the active item (Android keeps its 80dp bar with the 64x32 pill). Breadcrumb: 13px trail with a chevron icon separator in the subtle ink and an 8px gap. Steps: 28px markers (filled done with a check icon, a ringed and haloed current, a hairline upcoming), 2px connectors and a glowing progress fill. RowMenu on the web: a 32px edged trigger and a 220 wide card menu with 34px rows. No public API changes, so a patch.
- 1039627: Three fixes. A workspace Dialog no longer caps its height at zero in the server render (the window height is read once the page has hydrated), so a server-rendered page no longer reports a hydration mismatch. A vertical Chart reserves room for its value labels above the plot, so the tallest bar's label stays inside the chart. The floating overlays (the scrim, an anchored overlay's card and its phone sheet) take the web's floating glass, as the hand-off's `--gl` surfaces do.
- e9a44b5: v4 overlay and feedback skins: Dialog, AlertDialog, Drawer, ActionSheet, Popover, Tooltip, Dropdown, Command, Wizard, Toast, Alert, EmptyState, Progress, Spinner and Skeleton take the Canvas v4 hand-off's look on the web (the field-border hairline and large shadow on every floating panel, the display face on dialog and popover titles, mono section labels and shortcuts in Dropdown and Command), with the native skins in the hand-off's colours where they keep a platform shape. Look changes with no new prop or component, so a patch: Alert draws a tone glyph by default, Toast gains its sparkle mark and card face, AlertDialog shows a tone tile above its title with full-width actions, Popover draws a beak pointing at its anchor on the web, and Command shows an esc keycap in its search row and mono shortcuts on its rows. Behaviour notes: Dropdown `compact` is now the hand-off's 30 px rows, segmented Progress fills its parent, Spinner defaults to the primary ink, EmptyState is always padded, the Drawer side panel defaults to 300 px, the Wizard back action is a ghost button, and the Command footer reads "move / open / close".
- 2926b77: An open Select on the web shows the kit's keyboard ring when it has keyboard focus (its edge is already the ring while open, so the halo alone was too faint a cue), and a grouped Input value (PhoneInput's number included) shrinks to its share of a narrow field instead of overflowing it and resizing when its face changes on a keystroke.
- e9a44b5: Phase 4 integration fixes: the `rounded` Avatar square reads its own corner roles, the Chip remove disc is a pill, the Radio card reads the tile corner, the iOS Toast is the web skin, the Textarea count uses tabular figures through the helper, the Skeleton avatar placeholder follows Avatar's discs, the Command palette's active-row shortcut clears 4.5:1, an Accordion `activity` string trace renders as text, and AlertDialog drops its unused Android text-button branch.
- 6b3c2d0: The web toolbar search takes the type the v4 hand-off renders: a 400 16px value and an untracked uppercase prompt.
- 35dfa71: A `soft` Select keeps its ring-coloured edge under keyboard focus and while its list is open: the soft look used to clear every edge, the state edge included, so a soft field showed no focus ring at all. A copyable inline CodeBlock (`inline copy`) no longer runs past the bounds its parent gives it: a long line wraps inside the pill instead of pushing a phone-width page sideways.
- 1039627: A stacked `Row stacks` gives every span cell the whole line, as Bootstrap's `col-12` does. A Row's alignment (`alignStart`, `alignCenter`) is its cross axis; once the Row stacked into a column it turned into a horizontal hug, so span cells shrank to their content instead of filling the line.
- e9a44b5: Tabs on iOS take the v4 hand-off's iOS row: UISegmentedControl, the HIG's control for switching between closely related subviews. Underline and pills draw a capsule track inset 3 with capsule segments (13pt labels, 600 selected) and a raised thumb (white in light, a lifted gray in dark); vertical is a 220pt grouped rail of 15pt rows at a 10pt corner. The web menus share one v4 recipe: Dropdown and RowMenu draw the hand-off's panel (the `card` fill, the md shadow, a 4px inset), 8px menu-row corners, 400 14px labels, the mono eyebrow and shortcut, and the `secondary` hover wash, and the Dropdown sits 6px under its trigger; Select's and Autocomplete's web option rows and RowMenu's trigger round at 8 too, and Popover's compact card takes the same panel. The hand-off's corners land elsewhere: Command's panel at 18 and its rows at 9, the Alert callout at 14, the AlertDialog at 24 on the web and 14 on iOS, and the EmptyState at 18 with its glyph tile at 16 on every platform. Select, Autocomplete and Input share one focus-halo colour helper. Look changes with no public API, so a patch.
- 35dfa71: An `embedded` Wizard now renders in place in the page's flow, the way Dialog's contained form does: no portal, no scrim over the window, and the compact spacing of an inline panel. It used to keep the modal presentation and cover the whole page with its scrim even when embedded in a docs card or an app screen.

## 3.12.1

### Patch Changes

- e636e71: Tooltip floats over the page instead of inserting itself into layout. Its bubble used to render in flow as a flex sibling of its trigger, so hovering a table row's action grew the cell and shifted the row. The bubble is now the kit's anchored card (AnchoredOverlay, pinned: no backdrop, not modal, no focus move), portaled into the nearest OverlayProvider, centered on its trigger at the chosen side, flipping and sliding to stay inside the visible band; with no provider it floats on an absolute anchor. Moving the pointer onto the bubble keeps it up (a 100 ms grace spans the gap). AnchoredOverlay's `centered` now also centers a content-sized card by the width it rendered at, and centers a beside card on its trigger's height. No API change.

## 3.12.0

### Minor Changes

- 10a29bf: GlassSurface is public again, with its `GlassSurfaceProps` and `GlassLayer` types: the glass host every kit surface renders through, for an app's own frame. 3.11.0 removed it on the word that nothing consumed it, but Argus frames its whole app shell with `<GlassSurface frosted>`, and its `frosted` material exists for exactly that frame, so every signed-in Argus screen failed to render ("Element type is invalid ... Check the render method of `AppShell`"). Design-language item 7 keeps shells out of the kit and Canvas never hand-paints glass, so an app frame has no other way to take the material. The integration page documents it under App frames, and the docs' own frame imports it from the package.

  Minor justification: a public component returns to the entry (`GlassSurface`, with `GlassSurfaceProps` and `GlassLayer`), restoring the frosted app frame for consumers on 3.11.x. Existing components are unchanged.

- 6c2207b: Remove the remaining deprecated props and CSS custom properties. Alert, Chip and Toast (and `toast()`'s options) drop `error`: use `destructive`. DashboardGrid drops `widgets`: use `items`. The Carousel skin drops `dotHitSlop`, ChatQuestionAnswerData drops `skipped`, and the overlay placement drops `preferSide` for `besideEnd`. The web hand-off drops `--orb-indigo`, `--orb-violet`, `--orb-cyan`, `--glass-lens`, `--glass-illumination` and `--surface-backdrop`, and glass mode no longer sets the body's `background-image`, which left an app's own page background alone only while that alias was `none`.

  Minor by the owner's decision of 2026-10-23: nothing consumes these names, so their removal ships without a major, as the deprecated JS aliases did.

### Patch Changes

- 1434f59: Internal cleanup with no API or visual change: comments that still described the removed Chromium glass lens, the moving tab thumb or the retired deprecated aliases now describe the kit as it is, and the style hub drops its unused `useWindowDimensions` pass-through.

## 3.11.1

### Patch Changes

- 04ae9b7: The audit's turn capture keeps a foundation's after phase on every component its before phase photographed, so a fix that stops a component rendering through the foundation still records that component's after pictures (the owner's R-3 decision). Audit tooling only; nothing in the package changes.
- dc0e870: The local CI runner (`bun run ci:local`) gives each checkout its own action cache and takes free ports for act's servers: a cache one worktree saved restored into another at the wrong path (setup-bun then found no bun), and two checkouts' runs at once collided on act's fixed artifact port. Tooling only; nothing in the package changes.
- 8392c15: The repository's instructions (CLAUDE.md and AGENTS.md) name every file the tuning gate enforces, adding the iOS glass tints (`src/style/glass-surface/ios-tints.ts`) and the docs page look (`docs/src/shell/page-look.ts`), and the audit records the owner's decisions after the Foundations restructure. Repository docs only; nothing in the package changes.

## 3.11.0

### Minor Changes

- 3455b16: Add AppFrame and Page, so an app no longer hand-rolls its screen frame. `AppFrame` is the app's root frame: it mounts the safe-area context and the app-root overlay host, pads all four safe-area edges over the theme background, renders an optional `header` in a `role="banner"` region and lets its body (a router's Stack) fill the height left. `Page` is one scrolling screen: a `role="main"` scroller that fills its parent, keeps taps while the keyboard is up, and caps and centers its content between page gutters at a step of the width scale (`widest` by default, any `MeasureProps` step, `start` or `fluid`), with its own overlay host, or a plain content box with `inheritOverlays` under an enclosing viewport host.

  Minor justification: two new public components (`AppFrame`, `Page`, with `AppFrameProps` and `PageProps`) for app scaffolding: the safe area, the theme background, a banner header, and a scrolling page with a page measure and an overlay host. Existing components are unchanged.

- 260f4c6: Icon: new `ICON_NAMES` export listing every glyph (its prop `key` and typed `label`) in gallery order, for icon pickers and catalogs.

  Minor justification: a new public utility export (`ICON_NAMES`); nothing existing changes.

- 15a01f5: Navbar: new `onMenu` prop. An app with more destinations than a bar can hold passes it, and the bar leads with a menu button at every width that calls it, so the app opens its own navigation (such as a Drawer of grouped sections) in place of the `links` row and its narrow menu.

  Minor justification: a new public option on an existing component (`NavbarProps.onMenu`); existing bars are unchanged.

- 855249f: Remove every deprecated alias and every helper the package published by accident: 131 names leave the public entry, among them AnchoredOverlay, Entrance, GlassSurface, GlassPane, FloatingLabel, Portal, LoopView, RippleClip, the overlay, focus, ripple and touch-target hooks, `shadow` and `customShadow`, the Riskora type ladder (`fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`), `HUE_WASH`, `brandColors`, `tabularNums`, and the `StyleSheet` and `useWindowDimensions` pass-throughs. `useOverlayHost` and its `OverlayHost` type stay public, documented with OverlayProvider. The docs' /foundation reference page, which listed the deprecated names, is gone.

  Minor by the owner's decision of 2026-10-23: nothing consumes these names, so their removal is shipped without a major. Use the components that render this machinery themselves (Popover, Dialog, Card, Typography's `tabular`, Card's appearances) and React Native's own `StyleSheet` and `useWindowDimensions`. The deprecated `styles/*` custom properties are unchanged.

### Patch Changes

- f7523ca: Carousel: land on `defaultIndex` on Android every time. Before the viewport measures, native now lays the slides' content out as wide as every slide will be, so the scroll to the starting slide can no longer be capped by a content view that has not yet mounted its new width.
- 6398fa3: Command: the scrolling results list carries `${testID}-results` when the palette has a `testID`, so a test can scroll the list itself.
- 4ea4b2c: Internal cleanup with no API or visual change: the kit drops unused imports and locals, and Chat drops six Markdown styles left behind when Markdown moved to its own style module. The unused `react-test-renderer` and `@eslint/js` dev dependencies are gone.
- 27cae29: Input: on Android a page now scrolls when a swipe starts on a filled field. The filled field's bottom band is M3's 8dp with its active indicator inside it, so the value's line fits the field exactly instead of overflowing by 2dp, which made the field scrollable and kept the swipe.
- 12d0d39: Native smoke: the journeys run on phones and tablets (an iPhone and an iPad simulator, a phone and a tablet emulator), and the flows receive the form factor for their form-factor branches.
- 95fdf40: Native smoke: compile the Android test APK before the emulator boots, and bring the materials page's mode line and drawer trigger on screen before the journey checks them.
- 6d8b4a7: Native smoke: run each appearance's journey in one Maestro session, swiping the Carousel from a measured label so its endpoints stay inside the card.
- 7470561: Navbar: at phone widths the collapsed menu button leads the bar, ahead of the logo and brand, where Material 3's top app bar puts its navigation icon.
- 2f0fc8a: Docs and smoke app: example photos resolve to their exported asset URLs on the web, and the smoke app's Metro cache is keyed on the candidate identity.
- 8681635: Smoke app: the root layout is a Canvas AppFrame (the Navbar in its banner, the router Stack as its body) and every screen a Canvas Page, so the app hand-rolls no frame; the menu Drawer uses its own `navigation` body instead of a ScrollView. Page content now caps at the `widest` step (1152) instead of 1120.
- 14bd101: Native smoke: the smoke build of the starter is named "Canvas Smoke Test", in its app name and header.
- 701d7c8: Native smoke: the verification app is now its own Expo app in `smoke/` ("Canvas Smoke Test", `com.nannier.canvas.smoke`), and `examples/starter` is a plain example again.
- 1bd6d74: Native smoke: the smoke app's menu groups every documented component under Atoms, Molecules, Organisms and Charts, each with a screen of its documented examples, and the journey renders every component on the device against the sealed package.
- 0370a9e: CI: a smoke-web job prepares the smoke app with the sealed candidate, exports it for the web and runs it at desktop, tablet and phone sizes.
- 70b7bd9: Smoke app: a web export of the prepared candidate runs under Playwright at desktop, tablet and phone sizes, checking the candidate identity, the menu, the catalog of every component and each component's examples.

## 3.10.0

### Minor Changes

- 685208e: Canvas now ships its Android capture module itself. The native backdrop capture and frost that lived in the separate, never-published `@nannier/canvas-blur` package is part of `@nannier/canvas`: Expo autolinks it from the package root (`android/`, `expo-module.config.json`), so an Expo SDK 57 Android build frosts glass overlays and native Modals on Android 12 and later with nothing extra to install, after a native rebuild. Minor: this adds a user-visible capability, real Android frost from Canvas alone, and a newly declared optional peer, `expo-modules-core`, which every Expo app already provides.

  The module is built and verified with Expo SDK 57 and React Native 0.86. This release assumes, as the owner decided on 2026-10-22, that no Canvas 3.x Android app on an older SDK exists (3.9.3 is the only 3.x release under `@nannier`). An app on an older SDK, or one that does not want the module, leaves it out of its native build with `"expo": { "autolinking": { "exclude": ["@nannier/canvas"] } }` in its `package.json`; surfaces then use `expo-blur`'s frost or their complete solid appearance, as before. Web and iOS are unchanged.

  Release tooling now handles Canvas as its one package: a changeset for any other package fails release preparation before a version is consumed, the sealed tarball must carry the module's sources and none of its tests or build output, and the native smoke run checks that Expo autolinks the module from the installed Canvas package and runs its instrumentation tests there.

### Patch Changes

- 61631bb: Repository tooling only: `bun run ci:local` runs the CI validation workflow locally through act in Linux runner containers (check, the four e2e shards and the starter) before a push, and CONTRIBUTING documents it. Nothing in the published package changes.
- e7fd9a9: Repository guidance only: `CLAUDE.md` and `AGENTS.md` gain a "One package" rule (Canvas ships as exactly one npm package, its own native modules included, and never splits a component, renderer or native module into a separate package), and the platform reference, audit notes and material inventory describe the Android capture module as part of Canvas. Nothing in the published package changes.

## 3.9.3

### Patch Changes

- bd64dd2: The Deploy workflow proves npm will issue a trusted-publishing token for every package a release candidate publishes before it accepts the candidate, using the same OIDC exchange `npm publish` performs. Accepting pushes the version commit, so a trusted publisher npm cannot match used to consume a version (3.9.0 to 3.9.2 were versioned and never published); now the job fails first, with npm's own message. Repository release tooling only; nothing in the package changes.

## 3.9.2

### Patch Changes

- c19845b: The release script runs `npm publish` captured again, as every publish before 3.9.1 did (stdin closed, no terminal): inheriting the job's stdio for 3.9.1 stalled npm past the publish job's ten minute limit. npm's output is now printed without its tarball listing, so the error and details fit in the job log instead of being cut at one pipe buffer, the call is bounded at six minutes so a stall still reaches the failure path, and the workflow's npm debug-log step runs when the publish fails or is cancelled. 3.9.1 was versioned in git and did not reach npm. Repository release tooling only; nothing in the package changes.

## 3.9.1

### Patch Changes

- c870d5e: The release script streams `npm publish` output straight to the job log instead of capturing it and printing it again inside an uncaught error, where it was cut at one pipe buffer (64 KiB): the tarball listing alone is larger, so every failed publish from 3.5.0 to 3.9.0 lost npm's own error. On a failed publish the Deploy workflow also prints the authentication lines (OIDC exchange, HTTP, errors) from npm's debug log. 3.9.0, the first attempt through Trusted Publishing, was versioned in git but did not reach npm. Repository release tooling only; nothing in the package changes.

## 3.9.0

### Minor Changes

- 836982f: Add controlled Chat, ChatThread, ChatMessage, and ChatComposer components with an independently scrolling conversation, anchored multiline draft and actions, safe basic markdown, author/provider labels, and pending, streaming, error, and delivery notice states. Preserve native Canvas controls and follow the conversation tail only while the reader remains at the bottom.
- d03d3e8: Add a viewport-bounded workspace layout for Dialog forms, keeping the header and footer fixed around a flexing body.
- 0f62068: Add application-owned message action buttons to Chat, ChatThread, and ChatMessage, with per-platform buttons and disabled, pending, and streaming guards. Add sendDisabled to block sending independently while keeping message actions and draft editing available.
- a3d5036: Add provider mark slots and application-owned file attachment controls to Chat. Support attachment-only messages, removable staged files, upload state, and accessible saved-file actions while keeping the composer anchored. Polish message typography and a unified rounded composer, add provider-aware thinking and streaming dots that honor Reduce Motion.
- 268c875: Add controlled single- and multiple-choice question forms to Chat, ChatThread, and ChatMessage, with platform Radio and Checkbox controls, custom answers, complete-answer submission, and read-only saved receipts.
- 72c66b5: Show Chat questions one at a time with an actual question counter, saved position, a neutral Back button, automatic radio advance, and Next for custom answers. Reserve Submit for multiple-choice and final questions, and require a real answer to every question before sending the complete batch after explicit final confirmation. Preserve inactive question history and historical skipped receipts, and permit retry after transient failures without duplicate activations while busy. Let people switch a single-choice question to multiple answers with the existing Switch control, preserving and saving that explicit override without changing the source question.
- d019d3e: Add Button attention for a gentle accent halo pulse without changing its dimensions or label. Honor reduced motion and suppress the cue while disabled or loading.
- a033775: Add optional dragging to plain Board lanes while preserving custom card content. Supply a drag-handle slot, full-height drop targets, keyboard moves, and a disabled state for pending saves.
- 5cd39ce: Add Textarea embedded mode for a parent-owned input surface and focus frame. Keep the editor transparent and borderless across material changes while preserving draft and native input identity.

### Patch Changes

- c848c02: Support existing trigger refs and compact application popovers, preserving Argus filter-panel geometry, scrolling, and anchored behavior on narrow windows.
- 8660dbd: An anchored card's fitted edges land on whole pixels, starting at or past its trigger's edge: a trigger standing at a fractional offset (text set at a fractional line height above it) handed the card a fractional height, and its scrollport then clipped the last whole-pixel row by that fraction (the Command's last result stood 0.047px short behind its footer). Button's docs put the section note ("The variant only changes how a button looks") back under the Variants heading, where it shows for every variant, instead of inside the Notification action example that the October insertions had pushed it into. The browser suites follow the docs as they are: Button's guidance sections include Workflow actions, Badge's rail begins with Notification count, the Listbox's indicator box is the first hidden descendant, the Calendar's 354-stop keyboard sweep takes the slow budget on CI, and the end-to-end job limit is 35 minutes.
- a6aa6bc: Preserve application layout through independent wrapped-row gaps, curated header/footer/control insets, and zero minimum width for fill stacks.
- 86dd620: Preserve the Argus account trigger in AvatarMenu with its original disc, glow, identity typography, hover behavior and compact role menu.
- 92a151a: Preserve Argus activity and trace rows in Feed, with original time gutters, inline category labels, colored markers and optional halos.
- c683b6a: Extend existing Avatar with exact Argus identity geometry, stage hues, white initials, inverse treatment, account glow and conic sheen rings. Preserve the existing 9px inline-avatar annotation through a narrowly scoped reading-role exception.
- 9077cf3: Extend Badge with dense status and count metrics, intent washes, inverse labels, and linked reference keys, preserving Argus pill anatomy.
- 284baab: Preserve the original Argus sidebar brand mark in Avatar with exact dimensions, corners, gradient, initial and shadow.
- a844c57: Preserve original Argus featured and promotional cards through Card-owned gradient artwork, responsive anatomy, authored palette roles, and the existing hover lift.
- 7b1e31b: Complete Argus Button parity with inverse and quiet outlined actions, consistent workflow action widths, and comfortable featured-action surrounds. Keep existing button APIs and native skins intact.
- fa42bdc: Match Argus small and default button label line heights exactly while preserving the existing pill shape, spacing, native controls, and interaction behavior. Keep Pagination label metrics aligned with Button.
- 43a6a16: Preserve Argus inverse-button hover washes and optional reference-link hover opacity.
- cca5d77: Extend existing Card densities, gap-free and borderless anatomy, surface and elevation choices, accessible naming and hover reporting to preserve Argus card appearance.
- a357d75: Preserve Argus clickable-card interaction anatomy with nested controls and steady pressed opacity.
- c064661: Preserve dense Argus filter labels and hover fills, and add trailing counts to the existing Chip component.
- f935be3: Allow filter chips to preserve the Argus steady pressed appearance.
- ba7d735: Match EmptyState web typography to Argus and add its borderless elevated card presentation.
- 7aa3cad: Match the existing Icon glyph set to Argus reference paths and add semantic stroke weights so app icons retain their original appearance.
- 40b1374: Extend Input with the exact dense Argus field eyebrow without changing existing field defaults.

  Retain the platform focus outline when requested and honor explicit accessible field names.

- 2accdb8: Match web Input to Argus fields: 40-pixel default height, 13-pixel semibold value, 12-pixel horizontal inset, and muted tracked uppercase labels at the kit's 10-pixel reading floor. Preserve native field skins.
- 341d061: Preserve original Argus human-gate panels, version rows, metric notes and signal markers in Card and Badge.
- fb2456b: Extend Progress with discrete segmented meters matching Argus eval-pass indicators.
- b9acd0e: Characterize the Argus appearance axes, reconcile shape tokens and generated docs, and extend linked-badge and theme-swatch native hit areas while keeping visible geometry unchanged.
- 03db445: Preserve Argus run disclosure headers, trace indentation and chevron motion through Accordion activity mode and Icon disclosure state.
- 07c9c1b: Extend Typography headings with component-owned section counts, metadata and trailing actions, preserving the Argus section-heading anatomy.
- 239e5f3: Align web Select with Argus field frames and Input density, using a 40-pixel default trigger and muted tracked uppercase labels. Preserve native platform controls.
- 4b010d4: Match web Sidebar rows, section labels, icons, active markers and counts to the Argus reference. Add embedded navigation groups for app-owned rail frames.
- 9b166e0: Extend existing Stats with value-first rail and dashboard tiles, exact dense text metrics and supporting descriptions matching Argus.
- 96182d1: Extend the existing Swatch with accessible theme-picker samples preserving Argus gradients, dimensions and selection rings.
- 55a904b: Extend existing Textarea with dense Argus paragraph metrics, exact field eyebrows, explicit accessible naming and read-only behavior.
- 61c06ea: Extend the existing Typography with Argus reading roles, exact leading and tracking, extra-bold weight, stage-hue ink, branded-surface text, truncation and accessible naming. Preserve the owner's existing 9.5-pixel web eyebrows as explicitly requested.
- 1d32816: The component audit's web capture runs Chromium in its new headless mode. The old headless shell it used leaves a backdrop blur over scrolled content unrendered, so every web glass photograph the audit took showed the kit's frost without its blur, and the glass contrast read off them was not the material's (found in the GlassSurface turn). The capture now refuses to start in a browser that still drops the blur: a frosted panel over scrolled text must leave at most a fifth of the bare text's sharpness (`e2e/audit/backdrop-check.ts`, measured at about 0.6 in the old shell and 0.03 in the new mode). Repository tooling only; nothing in the package changes.
- c369f30: The audit tooling's test of the Foundations tier reads GlassSurface's export as the deprecated alias it now is. Repository tooling only; nothing in the package changes.
- 75f33fd: The web frost paint order's evidence row names only the shipped pages as its surface; the harness it was also judged on is noted in the comparison. Repository docs only; nothing in the package changes.
- 588a4ba: The component audit records GlassSurface's web after capture at GS-5 and GS-6: the turn's variant and state runs, the Switch and Checkbox runs taken beside them now that those two no longer render through GlassSurface, and what changed against the previous after capture (glass cells of the marks' consumers and the blurred scrims; in solid only the docs prose). Repository docs only; nothing in the package changes.
- f9d0b95: The component audit records GlassSurface's web after capture retaken at the verifier's fixes (the size gate, PhoneInput's country segment test, the Switch page's and GlassSurface's wording): every variant and interaction state of the 78 components that render through it. Repository records only; nothing in the package changes.
- a458449: The component audit records GlassSurface's web after capture: every variant and interaction state of the 78 components that render through it, at the turn's fixes. Repository records only; nothing in the package changes.
- 1ade9f7: The component audit retakes the GlassSurface turn's web before capture with the fixed capture (Chromium's new headless mode, which renders the frost's blur): a variant run (8748 of 8748 cells) and an interaction-state run (1944 cells, the same 54 press states not reached), at the turn's pre-fix commit, so the turn's before and after compare like for like. The first web before runs stay recorded; they show the glass without its blur. Repository tooling and docs only; nothing in the package changes.
- 07650e7: The component audit records the web before capture of the GlassSurface foundation's turn in `audit/turns/glass-surface.md`: the variant run (8748 of 8748 cells) and the interaction-state run (1944 of 1944 cells, 54 press states not reached: Autocomplete's 6, ButtonGroup's 3, Command's 12, Dropdown's 6, FilterPanel's 12, Heatmap's 6, Switch's 3 and Video's 6, the same 54 cells GlassPane's before capture left not reached) across the 78 components in its Capture through list, taken while the devices were busy, with the device runs to follow. Repository tooling and docs only; nothing in the package changes.
- 3bbb03f: The component audit records the owner's follow-up GlassSurface decisions in `audit/DECISIONS.md`: the web field addon's faint glass fill is accepted (GS-4), Radio, Checkbox and Switch will draw their solid look under glass (GS-5), and the modal scrim will take Dark Factory's 2 px blur under glass on the web (GS-6). The blind judges' verdicts on the control boundaries, the ink fills and the mint frost are in `tools/native/liquid-motion.md`, with rows for Input and Drawer from them. Repository docs only; nothing in the package changes.
- eaa83f5: The component audit records the owner's GlassSurface decisions in `audit/DECISIONS.md`: a selection control's identifying ring stays at 3:1 under glass (GS-1), an inner fill that carries text is capped for legibility like a wash (GS-2), and the glass washes' reference card asks for solid's ink at 4.5:1 or better rather than solid's exact ratios (GS-3). Repository docs only; nothing in the package changes.
- e91cbc5: The component audit records GlassSurface's device root causes (the iOS anchored-card glass, the tall frost's grey tiles, the hoisted material), the device checks and the triage of the glass inks into their components' checklists. Repository docs only; nothing in the package changes.
- d36147a: The tuning evidence log records the GlassSurface turn's runs: the selection controls' kept boundaries (GS-1), the capped fills that carry text (GS-2), the closing row for the glass inks under the owner's GS-3 reading of the `solid-state-washes` card, and the web frost in the mint palette on the shipped Drawer and Dropdown. The card gains the solid boundary of an unchecked Radio, an empty Checkbox and an off Switch as its fifth item. Repository docs only; nothing in the package changes.
- 9533d31: The component audit records the glass texts the retaken capture reads as fail-likely, unchanged by GlassSurface's fixes. Repository docs only; nothing in the package changes.
- 9312708: The component audit records GlassSurface's findings with their fixes and the rows its review found for other turns: Radio's glass card edge, Command's palette under the page's text on iOS, and ThemeProvider's first frame under Reduce Transparency. Repository records only; nothing in the package changes.
- 40f143b: The tuning evidence log records the web frost's paint order against Dark Factory's frost: a harness run on /testing/materials and a shipped run on the docs Dialog, Card and search pages, before and after the tint moved over the blur, in blush, mint and dark. The `df-frost` reference card now names, from Dark Factory's source, that its frost is one element whose fill paints over its own blurred backdrop, and the audit checklists' facts count the new paint-order test. Repository records only; nothing in the package changes.
- 6e29384: The GlassSurface audit records the web frost's paint order as a fixed finding (glass-surface-web-frost-self-sample-1): what the frost sampled, the corner blocks it drew on the docs Dialog, the fix, its test, the before and after runs and the web after capture that followed, with two leads left for the coordinator (the native frost hosts' order and the modal scrim's blur over its own dim). The turn record lists the new web after runs. Repository records only; nothing in the package changes.
- 517d337: The component audit records the owner's GS-5 decision in GlassSurface's turn: the harness and shipped-surface runs that read Radio, Checkbox and Switch drawing solid's marks under glass in blush, mint and dark, the `solid-state-washes` card's item 5 rewritten for it, the control-boundary and small-pane rows closed by it, and Radio's checked-indicator row fixed. Repository docs only; nothing in the package changes.
- 3deb43b: The component audit records the owner's GS-6 decision in GlassSurface's turn: the harness and shipped-surface runs that read the modal scrims' 2 px blur under glass on the web against Dark Factory's `df-frost` card in blush, mint and dark, the card's Canvas note, the fixed scrim row, and a deferred row for the CSS hand-off's missing counterpart. Repository docs only; nothing in the package changes.
- b856b47: The component audit records the owner's GS-7 and GS-8 decisions (iOS Liquid Glass takes its tint as the glass's own `tintColor`, with the light tints retuned against Apple's light menu; the iOS dark control tint is retuned so a dark field keeps its body) and the blind judges' verdicts on the scrim blur, the web frost's paint order, and the native frosts' paint order with the held scrim. Repository docs only; nothing in the package changes.
- 8cb0d76: The component audit records the blind judges' verdicts on GlassSurface's iOS menu blur (with Apple's own menu at rest in light added to the `ios-native-menu` reference) and on Radio, Checkbox and Switch drawing their solid look under glass (GS-5). Repository docs only; nothing in the package changes.
- 864520b: The component audit records the device before capture of the GlassSurface foundation's turn in `audit/turns/glass-surface.md`: an iOS run and an Android run, 2916 of 2916 cells each across the 78 components that render through GlassSurface (6 unstable on each, the animated cells). Repository tooling and docs only; nothing in the package changes.
- 533d555: The GlassSurface audit records the native frost's paint order (glass-surface-native-frost-self-sample-1) and the modal scrim's blur over its own dim (glass-surface-scrim-self-sample-1) as fixed findings: what each sampled of itself, the device proof, the fixes, their tests, the before and after runs and the web after capture that followed, with the leads left for the coordinator. The tuning evidence log records the iOS proof, the shipped iOS Card and DataTable pages, the Android FilterPanel drawer, the iOS text fields and the web Dialog and search scrims before and after, each awaiting the coordinator's blind judge, and the `df-frost` card's Canvas note names both orders. The turn record lists the new web after runs. Repository records only; nothing in the package changes.
- aeca4d4: GlassSurface's audit records cite each of the turn's commits as it lands on main, keep the page look's three GlassSurface rows, and the GS-2 tests pin the fill caps the page look's darker muted inks now give (the full soft strength on the light web field, 0.04 on the dark control puck). Repository docs and tests only; nothing in the package changes.
- 1eff1df: The component audit records GlassSurface's repair round on the devices: the border curve fixed and read through lldb on the iPhone simulator, the rim's first frame fixed with its device recording, GlassPane's hoisted pane fixed with its counts before and after, GS-5's marks matching solid on iOS and inside an Android overlay, a Popover's phone sheet and pinned card blurring under Liquid Glass, and the verifier's lows. The Entrance and border-curve release notes now say what was verified, name the iOS 27 runtime they were measured on, and that React Native draws the rim's inset shadow along circular arcs. Repository docs and release notes only; nothing in the package changes.
- 8b31965: The component audit records a second shipped-surface run of the modal scrim's 2 px blur under glass on the web, on the docs search, whose Dialog scrim covers the whole window, in blush, mint and dark against solid, awaiting the blind judge; and GlassSurface's checklist gains the rows for the fresh verifier's lows: the single rim capped at a band and read in every commit, and the corrected size record, scrim docs, measurement runtime and cell count. Repository docs only; nothing in the package changes.
- 26eeb41: The component audit records a verifier's corrections on GlassSurface's checklist rows. Repository docs only; nothing in the package changes.
- 97cc9b6: The component audit's GlassSurface record states the rule behind its count of the cells that changed against the previous after capture (any pixel more than 8 of 255 apart) and corrects FilterPanel's to 15 of 18 glass cells, naming the 42 chart, Avatar and Tooltip cells that differ by anti-aliasing alone; the GlassPane changeset that stops asking for interactive Liquid Glass names the iOS 27 simulator its press was measured on. Repository docs only; nothing in the package changes.
- 2d71918: Preserve Argus notification pills through brief single-message Toast presentation, configurable lifetime and safe-area offsets.
- 7e78a92: Button's attention glow reads its red through `PALETTE_RED_500`, a single-sourced constant in the tokens module, instead of `palette["red-500"]`, which retained the whole ~500-entry palette record in every Button and StackedList bundle (about 2KB gzip each; the hue is unchanged). The size budgets are re-measured for the loop runtime the heartbeat legitimately carries: the Button ceiling moves to 16,384B and StackedList's to 70,656B, with the figures and rationale recorded in scripts/check-size.ts.
- 6171a59: ButtonGroup is now the same size in solid and glass, so a page no longer reflows when the surface mode changes (the owner's GP-1 decision: a control is the same size in glass and solid). Under glass the group swapped its skin's track and cells for a shared geometry, a padded pill track with borderless cells and its own label weights: on the web a segmented Day, Week, Month group was 8.3 px narrower and 2 px shorter in glass and the spaced peers, the stepper and the docs' own Solid and Glass toggle changed size too; on iOS the stepper narrowed; on Android an overlay's segmented group grew 4 px taller. Every kind now keeps its skin's own box under glass: a resting border becomes padding, the stepper keeps its shared-border overlaps, the labels keep their type and weight (the web's 700), and the group's material covers the track's or cell's whole footprint on its own corners. Only the paint changes, as before: the selected segment rides its glass puck and the labels take the glass colours. Solid mode is unchanged.
- b9e441e: The carousel slide-identity test takes its `HTMLElement.prototype.scroll` override off again by property descriptor instead of assigning the saved function back, which left an own `scroll` on the prototype that shadowed the anchored-overlay suite's `Element.prototype.scroll` spy whenever CI's file order ran the carousel file first (Deploy runs 39610128823 and 39611676187: the option list's entry reveal "saw no scroll"). Test only; nothing in the package changes.
- df2879a: Keep Chat checkbox option names stable when selected, including custom answers. Hide decorative checkbox check and mixed marks from assistive technology so checked state is announced through the control's semantic state.
- a587eba: Place Chat's Select multiple switch at the bottom-right beside the question navigation buttons, wrapping to a right-aligned row only when the available width requires it.
- cd190df: Frame the active chat question with the existing selected Card treatment: a soft
  theme tint, clear primary border and compact inset. Preserve question spacing,
  controls and unframed saved receipts/history across all platform entries.
- ffa4135: Keep Chat pinned to the latest content while the reader follows the bottom, including streamed text, question-height changes, and viewport resizing. Pause when the reader scrolls up, resume on returning to the bottom, and coalesce non-animated adjustments after layout without treating content-growth scroll events as reader movement.
- 808405c: Register the Chat and Wizard organisms, Markdown's platform builds and Button's `attention` variant in the kit's own registries: the audit inventory and checklists, the state recipe table, the platform reference catalog, the Foundations consumer facts (Button's attention glow and Chat's thinking dots now animate through LoopView), the skins smoke cases, the shape and type role tables, the docs' platform-skin tables and the native-role scanner's data allowance for a message's author `role`. Wizard gains the standard per-platform skin structure (layout only, aliased across platforms) and the attention glow copies its host's corner keys through `materialCorners` instead of a bare radius. Nothing renders differently; this is what the validation gates require before the kit can release.
- 2e9bfc7: Support compact project and role menus through existing Dropdown, including external anchors, selected choices, descriptions, counts, section headings, notes, and bounded scrolling.
- a63fd99: Add compact workspace dialogs that hug short conversations and grow to a safe-area-bounded viewport maximum, keeping independently scrolling threads and fixed controls.
- 26eed9c: Preserve compact application dropdown fields and text-only option menus through the existing Select, including selected action ink, pointer feedback, and independent menu bounds.
- 3513bbb: The Deploy workflow publishes to npm through Trusted Publishing: the publish job holds `id-token: write`, exchanges the GitHub OIDC token for a short-lived npm credential bound to this repository and workflow (configured on npmjs.com for `@nannier/canvas`), reads no NPM_TOKEN, and asserts npm 11.5.1 or later before the accept push so a too-old npm cannot consume a version. Every publish since the 2026-09-27 move back to `@nannier` had failed with npm EOTP. Repository workflow only; nothing in the package changes.
- 15c285e: Add a fixed workspace header content slot to Dialog.
- 0344680: An anchored card's iOS Liquid Glass now renders its blur. While an anchored card (a Dropdown menu, a Select, Autocomplete or RowMenu option list, a Popover) waits for its placement, Entrance held it at opacity 0; expo-glass-effect installs no glass under an ancestor that transparent and installs it later, once the card shows, and in an anchored card UIKit never realized that late glass, so its dense and functional Liquid Glass drew no blur and the page behind read sharp through the card's fill (found on the iPhone simulator's iOS 27 runtime: the code block's Copy button behind the Dropdown docs' Actions menu, the variant picker behind Select's Country list). Entrance now holds the card out of view instead, moved up out of the window at full opacity, still untouchable and hidden from VoiceOver, TalkBack and the web's accessibility tree, so the glass installs as the card first lays out, as every glass in the page does. The card's look once shown, its placement and its timing are unchanged on every platform. Verified on the iPhone simulator's iOS 27 runtime in light and dark glass for the Dropdown menu, Select's option list, a Popover's phone sheet and a Popover card pinned open; Autocomplete's and RowMenu's lists share the same hold and were not opened on the device.
- a0ffcb6: Keep the web ButtonGroup’s exact Argus segment typography and track dimensions, with steady press feedback and explicit leading alignment.
- cae186f: Preserve Argus grid measurements with natural card heights, whole-row cells, shadow stacking, separate row rhythm, and intermediate shared spacing steps.
- 4802dc1: A glass surface's material now stays inside its own surface on iOS and Android. Under glass a surface's own fill and border colours give way to the material, and React Native's renderer then removed the surface's view as a box that draws nothing and handed its children to the nearest ancestor view, where the material's `zIndex: -1` ordered it beneath every earlier sibling there: a glass card placed over an image or text drew its material under them, the text on top at full contrast (on the docs' Select page 11 of 39 materials sat in the page's content view instead of their cards). The surface's view is now kept wherever its material renders (`collapsable={false}`), so the material paints beneath that surface's content only. Nothing changes on the web or in solid mode.
- bb03220: Under glass in the browser, a field's prefix or suffix addon and PhoneInput's country segment now paint their fill only as dense as keeps the text on it at 4.5:1 over the field (the owner's GS-2 decision, the error and selection washes' rule extended to a fill that carries text). Drawn through the browser's dark control material (the docs' iOS preview of a field), the addon's `$` read 3.93 to 3.99:1; it now reads at least 4.6:1 there. In the light palettes the addon's fill becomes a faint trace (the text measured about 4.5:1 on it before, with no headroom), and on the web's dark field it keeps its full strength, which already held. iOS and Android keep the full fill: their frost paints a darker body than the tints predict, and on the iOS device the addon's text measures 4.76:1 in dark. Solid mode is unchanged.
- 583016e: The glass washes and the ink fill share one legibility search internally. A refactor; nothing renders differently.
- 7873ad9: On iOS and Android a control's glass now stays behind its own content and in front of what lies before it. Under glass a control that paints its surface through a GlassPane clears its own fill and border, and where nothing else made it a native view (a Badge, a Kbd, a chart frame, the wrapper beside a text field), the native renderer removed it and handed its pane to the nearest ancestor view, where the pane's order behind its host's content sorted it beneath everything earlier there: a frost could draw under the text or the surface it sat on. Every pane host is now a stacking context of its own under glass (`isolation: "isolate"`, which paints nothing and keeps the host a native view, as a glass surface's own host already was), through `paneStyle` and, for a wrapper that holds a pane without it, the kit's internal `paneHost`. Solid mode is unchanged. An app that composes the deprecated `paneStyle` gets the same isolation under glass.
- 97cc9b6: GlassPane no longer asks iOS 26 for interactive Liquid Glass. A pane passes every tap through to the control it paints behind (`pointerEvents="none"`), so React Native disables user interaction on it and its glass never received the touch Apple's interactive glass responds to: on the iOS 27 simulator a pressed glass Button showed only its label's dim, the same as before this change. The Button, Chip, Select, Calendar day, Carousel arrow, Navbar link, Sidebar row, TabBar tab, Tabs trigger and Stepper button panes stop requesting it, and GlassPane stops forwarding it, which also stops the native view reinstalling its glass effect each time a control is enabled or disabled; the resting look is unchanged and the control's own dim carries the press. `GlassPaneProps.interactive` stays as a deprecated no-op, so code that passes it still compiles. GlassSurface's `interactive` is documented to act only on a surface that itself receives the touch.
- a40c147: A tall frosted surface on iOS no longer draws flat dark tiles over its content. The native frost's specular rim, React Native's inset `boxShadow`, is drawn by iOS as one Core Animation shadow over the whole surface, and Core Animation renders no such shadow past 8192 px on a side: a frost taller than about 2730 pt at 3x (a long DataTable, the docs' Props tables at 4158 pt) showed grey rectangles over its rows that moved with them as the page scrolled. The rim is now drawn in bands of at most 4096 px, each the rim clipped to its band, so the bands together draw the same edge; a surface shorter than a band keeps its single rim. The rim's look is unchanged, on Android too, which draws its frost's rim the same way.
- 6557cb6: On iOS's and Android's frost the specular rim no longer draws a stray line across a tall surface for its first frame. The rim mounted as one rim capped at a band's height and split into bands only once the surface's first layout arrived, so for one frame a surface taller than a band (a docs Props table) drew the capped rim's bottom edge across itself. The rim now reads the surface's height in the commit that mounts it, which React Native's New Architecture lays out before the frame is drawn, so the first frame already holds the whole rim or its bands; where no height can be read it waits for the first layout rather than guess. Solid mode and the web, which draws no specular rim, are unchanged.
- ba39465: On iOS's and Android's frost a surface that grows past two bands of its specular rim before its next layout no longer flashes Core Animation's flat dark tiles. The single rim spanned whatever its surface became, so a frosted surface measured short (a DataTable showing a few rows) that grew taller than about 2730 pt at 3x in one update (the same table given a long page) asked iOS for a shadow past its 8192 px limit for a frame or two, until its new layout re-banded the rim. Every shadow view of the rim is now bounded by its own style, the single rim capped at one band (4096 px), so no frame asks for an oversized shadow whatever the surface does. The rim also reads its surface's height in every update it renders in, not only the one that mounts it, so a surface that grows with its owner's re-render draws its bands from that update's first frame. A surface no taller than a band keeps its whole single rim, unchanged; solid mode and the web, which draws no specular rim, are unchanged.
- f455e53: Under glass on the web a modal's scrim now also blurs the page it dims, by Dark Factory's 2 px (the owner's GS-6 decision, `blur(2px)` under its tinted dim, as Dark Factory's overlay scrim draws it): the Dialog's and the AlertDialog's backdrop, the Drawer's (and so the Sidebar's and the FilterPanel's drawers), the ActionSheet's and the phone sheet a Popover becomes at the phone form factor. The scrim's colour is unchanged, and the blur fades in with the dim. On a Dialog and an AlertDialog the blur is a layer of its own beneath the card, so the card's frost still samples the page. Solid mode keeps its promise that nothing samples the backdrop: solid, Reduce Transparency, Increase Contrast and a browser without a backdrop filter render the plain dim with unchanged markup, and the iOS and Android scrims do not change. The Dialog and AlertDialog pages no longer promise a blurred backdrop in every mode.
- 5625177: Under glass a Radio, a Checkbox and a Switch now draw their solid look on every platform, with no glass pane and no rim (the owner's GS-5 decision): they are small marks inside content, not surfaces, and neither Apple, Material 3 nor Dark Factory draws them in glass. A checked box, track or ring is its solid brand fill again (a checked Radio is its primary ring around a primary dot, where glass drew a filled brand disc with an inverse dot), an unchecked ring and box keep their solid outline and an off track its solid fill, with no grey frost inside them, and on iOS 26 the Switch knob is no longer a Liquid Glass puck. A switch row now dims while pressed and when disabled as it does in solid mode. On Android an overlay or a bridged Modal no longer mounts a capture frost for each mark, and a mark's borderless ripple there comes forward through the surface it sits in. The same holds wherever the Checkbox indicator renders: a Listbox's multiple selection, the FilterPanel's option rows and DataTable's selection column. A Radio `card` option is unchanged, a content pane with its selection wash. Solid mode is unchanged, and so is Increase Contrast. This supersedes what the pending release notes say of these three controls under glass (the full-footprint, ripple and not-interactive notes). The unreleased GS-1 additions for this case go with it: `GlassPaneProps.boundary` and `boundaryFill`, `GlassSurface`'s `boundaryFill` and `paneStyle`'s `"boundary"` value never shipped.
- e650f26: GlassSurface's documentation scopes its Android sentence to the install that resolves it. Documentation only; nothing renders differently.
- 8ca269c: The test suite now renders GlassSurface's Android material paths (canvas-blur's capture frost in an overlay and expo-blur's frost, with stand-ins for the native views), which before ran only on a device, and holds the specular rim the Android host draws: a tall surface's rim in bands from its first render and a short one's whole. The comments that named iOS 26 for measurements taken on the iPhone simulator's iOS 27 runtime (the rim's band limit, GlassPane's inert interactive glass) now name iOS 27. Repository tests and comments only; nothing in the package changes.
- fc262e1: GlassSurface's documentation now says that `@nannier/canvas-blur`'s capture frost is Android 12 and later: on Android 11 and earlier, where it captures nothing, a surface follows expo-blur's release as it does without the package. Documentation only; nothing renders differently.
- 132d109: On iOS a glass surface's material now keeps its skin's continuous corner. A skin with iOS's continuous corner curve (a Card, the stacked and description lists, Feeds, Stats, a MediaObject, an EmptyState, an Accordion, a Collapsible, the Drawer sheet) kept it on its own view while the material under it took the corner radii alone, so under glass the material's clip, the frost and Liquid Glass drew the circular corner where solid mode draws the continuous one. The material now takes the host's `borderCurve` with its radii on every layer, so its clip, its frost and Liquid Glass keep the continuous corner; the frost's specular rim, an inset shadow React Native draws along circular arcs whatever the curve, lies inside that clip. The web, which has no corner curve, and solid mode are unchanged.
- c38a9c6: On Android a borderless ripple inside a frosted glass surface now shows. Android projects a borderless ripple onto the nearest ancestor's background, which it draws beneath that ancestor's children, so inside a surface whose frost renders (an overlay or a bridged Modal; in the page Android's glass resolves solid) the press of a control the surface does not own drew beneath the frost and showed nothing: a Tooltip's icon or text trigger, a Breadcrumb link, a pressable Avatar disc, a Carousel dot, Calendar's month chevrons, TabBar's inactive tabs, or an app's own icon button placed in a Card, an Alert, a Popover, a Dialog or a Drawer. Every glass surface now tells the content inside it whether its material renders (a static Card, Dialog and the rest through their shared host; a pressable Card, an Alert and a bordered MediaObject around the content they hold), and the kit's Pressable draws a borderless ripple in the foreground there, as the controls that own their pane already do. Solid mode, Android's in-page glass, iOS and the web are unchanged, and the content is never remounted when the material comes and goes.
- 8de850d: GlassSurface's hosts' summary comment names the boundary fill. A source comment only; nothing renders differently.
- be7d5d1: `GlassSurface` is a deprecated alias (audit K12-2): a kit internal that was published by accident. Nothing is removed and nothing changes at run time: it keeps working until a major, its `@deprecated` text says to use the kit component that renders its own material (a Card for a content pane, a Dialog, Popover or Drawer for a floating one), and the generated /foundation reference lists it in its Glass material section. It carries a JSDoc summary that says what it paints on the web, iOS and Android, that it renders the plain skin where its material resolves solid (with the contrasting border under Increase Contrast), how its material covers the surface's footprint, and what it adds to the accessibility tree: one root node carrying the role, test id and escape handler it is given, its material hidden from screen readers. Its props' documentation now matches the code: `brand` and `tint` say what they paint on each path and what wins, `sheer` that it acts on the content layer alone, `stateBorder` that the material sits flush inside the border, and `style` that it is the kit skin's own box.
- ca3b976: The internal frost no longer carries a code path for expo-blur 57's blur targets that nothing reached. On Android the kit's frost samples a capture plane only through `@nannier/canvas-blur`, which renders its own frost wherever a plane exists, and a surface that needs a plane and has none resolves its complete solid skin before any expo-blur view mounts, so the expo-blur view was never handed a target. It keeps the legacy blur method on expo-blur releases before 57, as before, and passes no method prop from 57. Nothing changes on screen on any platform.
- 580b676: Under glass every kit surface's material now covers its whole solid footprint, as the controls' panes already do (the owner's GP-1 decision). GlassSurface, the host every glass surface renders through, kept its skin's border widths as a transparent inset and laid its material inside them, so a bordered surface drew its material one border width in from its solid edge: a static Card was 1 px smaller than a pressable one, and so were a CodeBlock, a framed StackedList, a Feed, a bordered MediaObject, a Stats card, a GridList tile, the bordered Accordion, Collapsible, DescriptionList and EmptyState, every Calendar, a DataTable, a Board column, a Carousel slide, the Navbar, Sidebar and TabBar, a Command palette, the Dialog, AlertDialog, Drawer and Popover cards, the Dropdown, Select, RowMenu and Command option lists, and the Slider's knob. The host now trades a resting border for padding of the same width, so nothing moves and the material reaches the edge, and a kept state border (a selected Card, the pressed Slider knob's halo) sits flush over the material on concentric corners. CodeBlock's floating copy chip now sits where solid puts it (it was 1 px further in under glass), and Calendar's inline day peek keeps its solid place. Solid mode is unchanged.
- e2b2b1c: The size gate's ceiling for a named `Input` import grows from 39,936 to 43,008 bytes gzip, with the measured reason recorded beside it. Over the GlassSurface audit turn every field came to carry what that turn gave the material host and the controls on it: on iOS and Android the native frost's specular rim, drawn in bands under Core Animation's shadow limit (about 1,200 bytes minified, the larger part of the growth there); the borderless Android ripple brought in front of a frost; a field addon's fill capped for its text under glass in the browser; and the full-footprint geometry GlassBox shares with GlassPane with the registered pointer-events styles, which net out to within 60 bytes. Measured at the turn's end the import is 39,477 / 40,423 / 40,072 bytes gzip on the web, iOS and Android, 317 to 698 bytes over where it started; the first measurement went 143 bytes over the old ceiling on iOS, and the new one leaves 6.4 percent of headroom there. The wash helpers that turn added to now share one legibility search, with the same answers. Nothing changes at run time.
- 0823424: On iOS the dark scheme's control-layer glass tint is now white 0.05 where the native set (`glassByScheme`, which Android keeps) says 0.12, the owner's GS-8 decision. Since the iOS frost paints a surface's tint over its blur, the 0.12 tint painted at full strength: a dark text field's body read #373740 on the iPhone simulator where it had read #272730, and the legibility cap left its prefix and suffix addons no fill. At 0.05 the body reads #272730 again and the addon keeps a faint fill with its text at 4.5:1 or better. Every iOS dark control-layer surface that paints the layer's own tint takes the sheerer one: the text fields, Stepper and InputOTP on the frost, and Liquid Glass's plain control pucks such as a secondary or outline Button or the Select trigger (a brand fill or a toned wash keeps its own colour). The light scheme, Android, the web and solid mode are unchanged. iOS's tints now live in their own table, `src/style/glass-surface/ios-tints.ts`, a tuning-harness tunable.
- cc944c7: The test suite now renders GlassSurface's iOS material paths (Liquid Glass and the frost, with stand-ins for the native views), which before ran only on a device. Repository tests only; nothing in the package changes.
- 84774de: On iOS's frost and Android's expo-blur frost a glass surface's tint now paints over the native blur, as it already did on the web and over Android's canvas-blur capture frost, and as Dark Factory's GlassSurface paints its fill over its BlurView. The tint used to paint beneath the BlurView, so the frost sampled the surface's own tint (against the materials law that no material samples itself) and dyed it with its light or dark veil: on the iPhone simulator a fill under the frost read up to 53 levels a channel off the same fill composited on the frost (a red diagnostic tint; up to 10 on the dark Card's content tint), where over it the fill lands within half a level of the composite. The tints, the blur intensities, the specular rim and its bands, the footprint, the clip, the corners and their curve, solid mode, Reduce Transparency, Increase Contrast and iOS 26's Liquid Glass are unchanged. On iOS a dark field's body now reads #373740 where it read #272730, so a field's prefix or suffix addon is capped for its ink on every platform, as the web's already was (the owner's GS-2 decision): its `$` reads 4.91:1 with no addon fill in dark, where the full-strength addon would have read 3.83.
- 0f66f25: The Switch thumb, the Slider knob, ButtonGroup's segment and split glass and TabBar's icon pill no longer ask iOS 26 for interactive Liquid Glass. Each passes its taps through to the control it paints for (`pointerEvents="none"`), and GlassSurface's material layer passes every touch through to its content anyway, so the glass never received the touch Apple's interactive glass answers (the same was confirmed inert on a device for GlassPane, GP-9). Asking only made the native view reinstall its glass effect each time a segment was selected or a control changed state. The resting look is unchanged and each control's own press feedback carries the press. GlassSurface keeps its `interactive` prop, now documented as reaching only a glass view that receives the touch itself.
- 94618dc: A glass surface's `pointerEvents` of `box-none` or `box-only` now holds on the web. GlassSurface wrote the value as an inline style, which React Native Web passes straight to CSS, where `box-none` and `box-only` are not values, so the browser dropped them and the surface took every tap; it now resolves the four values through registered styles, which React Native Web compiles to the box and its children as React Native means them. Every kit caller passes `none`, which already worked, so nothing in the kit changes on screen; iOS and Android are unchanged.
- 181ee09: Under glass on the web the Dialog's and AlertDialog's scrim now paints its dim and its 2 px blur as one layer beneath the card, as Dark Factory's scrim is one element whose background paints over its own blurred backdrop. The blur used to lie on a layer of its own under a role node that wore the dim, so it sampled that dim (against the materials law that no material samples itself), which showed as a light fringe about 3 CSS px wide at a contained scrim's rounded corners, up to 26 levels a channel off the solid scrim in blush and mint (the docs' inline Dialog); after it, the corners differ from solid by no more than the blur's own softening of the page, 3 to 4 levels. The scrim's colour, the 2 px blur, the card's frost sampling the dimmed page, the dismiss target, the accessibility hiding, solid mode's markup, the full-window scrims (which showed nothing) and the native scrims are unchanged.
- 7be349f: On the web a glass surface's tint now paints over its backdrop blur, the way Dark Factory's frost paints its background over its own filtered backdrop. The tint used to paint as a layer beneath the blur, so the blur sampled the surface's own tint (against the materials law that no material samples itself), and at a rounded corner the wedges outside the tint's curve, where the page showed unfilled, bled into the blur as faint hard-edged blocks, most visible at the top corners of a Dialog over its scrim in blush and mint. Every layer's tint, a wash, a brand colour, a sheer pane's opacity, the 24 px blur, the hairline rim, the shape, the clip and the footprint are unchanged; inside a surface the colour is the same, and only its edges and corners differ. Solid mode, Reduce Transparency, Increase Contrast and the iOS and Android materials are unchanged.
- 1fa02d6: The repository's instructions (CLAUDE.md and AGENTS.md) now name the modal scrim's blur in the glass law: the scrim is no surface, and under glass on the web it blurs what it dims by Dark Factory's 2 px, while solid mode and every native scrim keep the plain dim (the owner's GS-6 decision). Repository docs only; nothing in the package changes.
- 3469ea9: The repository's guidance (CLAUDE.md and AGENTS.md) replaces the GS-1 clause of the glass law with the owner's GS-5 one: Radio, Checkbox and Switch leave the control layer's list, and the law says they are small marks inside content, not surfaces, that draw their solid look under glass on every platform with no pane and no rim, while a Radio card tile is a content pane. Guidance only; nothing in the package changes.
- 6af59db: Hand-off parity: drop the `Drawer.footer` open-gap record, which the kit closed when `DrawerProps` gained `footer`, and regenerate HANDOFF-PARITY.md so `check-parity:check` passes again. Repository tooling only; nothing in the package changes.
- b6a16a3: Make Button attention swell outward and return in a heartbeat rhythm, with a softer second beat and a pause. Preserve fixed inner geometry and reduced-motion fallback.
- 475f95b: Preserve Argus inline identity actions, tracked reference links, compact external icons and theme choices through Button variants.
- c43dde2: Preserve Argus inline text actions through Button, with exact label sizes, weight, icon gutter, hover opacity, and system-browser navigation.
- 664d2b0: The GlassSurface audit turn and the Argus component adoption land together: the frame frost's web paint reads the surface's own `tint`, the static Reference Card frosts like the pressable one (its pane host keeps its own stacking context and its fill drops under glass), TextInput's summary sits on its declaration again, the theming page describes the frame frost without presenting the deprecated GlassSurface alias, and the size ceilings take the two lines' combined growth (the whole kit 248KB, Button 13,568B), each with its measurement.
- 6f8ce38: Preserve Argus linked reference rows, stacked summaries, compact tiles and detail panels in the existing Card family.
- a7a1116: A test holds PhoneInput's country segment to its capped glass fill (GS-2). Tests only; nothing renders differently.
- b6c1153: Preserve Argus work lanes in Board with original labels, counters, scrolling bleed, custom domain cards and dashed empty slots.
- d3103f1: Preserve Argus frame and modal-veil frost through GlassSurface, independently of descendant control materials, while retaining accessibility and capability fallbacks.
- 39e82bb: Use red for the Button attention glow, preserving its fixed inner edge and outward fade.
- 6b12a06: Add Wizard, a reusable modal workflow with fixed step navigation, a bounded scrolling body, named previous-stage navigation, centered status and application actions. Compose existing platform Dialog, Steps and Button controls to preserve their appearance and accessibility.
- f159a68: Preserve original Argus project and filter pills in Button with exact insets, label metrics, field fill, border and scope dot.
- ece863b: The ActionSheet and Popover pages now say what their scrims do under glass on the web, as the Dialog, AlertDialog and Drawer pages already did: the ActionSheet's dimmed scrim, and the one under the sheet a Popover becomes at phone width, also blur the page they dim, by the 2 px Dark Factory's scrim carries. Documentation only; nothing in the package's code changes.
- aa2a5af: Share Badge notification anatomy privately with Button so icon actions do not import unrelated badge families. Keep the same rendered count appearance.
- 13d3c49: Extract Chat's safe Markdown renderer for document use, sharing one grammar for heading hierarchy, template tables and static checklists. Retain platform controls and responsive tables without executing HTML, opening URLs or loading remote images.
- 5109605: Let Button shrink within a constrained row and wrap its label without changing its normal size or platform appearance.
- 9d49b6e: Keep Wizard navigation and primary actions on one row. Center status between the actions on wide layouts and above them on narrow layouts, with flexible side regions instead of fixed thirds.
- 2228b86: The size gate's record of why the named `Input` import's ceiling is 43,008 bytes gzip now says what the growth consists of at the end of the GlassSurface audit turn, re-measured per commit: it no longer counts an off Switch's track painted over the frost, which the turn removed before release, and it names the native frost's specular rim, which most of the growth on iOS and Android is. The ceiling is unchanged. Repository tooling only; nothing in the package changes.
- c4227b3: Preserve Argus application dialogs through the existing Dialog’s form anatomy, including header, close action, bounded scrolling, footer, and lightly frosted backdrop.
- c00f40e: Preserve Argus side panels through Drawer’s pinned header/footer, scrolling body, square web panel, lightly frosted dim, and immediate presentation options.
- 6d45374: Switch's documentation says what the glass off track keeps on each platform. Documentation only; nothing renders differently.
- db08caa: Switch's shell comment says where the off boundary paints under glass. A source comment only; nothing renders differently.
- bc9a0c8: Keep plain Board columns evenly sized within their container instead of allowing long custom cards to expand every lane. Preserve horizontal scrolling at the minimum lane width on narrow screens.
- 6accae1: Add toolbar search anatomy to Input while retaining the Argus magnifier, tracked empty prompt, editor dimensions, and text behavior.
- 9130a21: Preserve compact Argus icon controls through existing Button variants and Badge notification counts, including exact web dimensions, corners, and press feedback.
- 86b45f0: Visual baselines minted on Linux (Visual baselines run 39610199070) for the docs as they are: new pages for Chat and Wizard, and the 71 previews the October look changes moved (field anatomy heights and eyebrow labels, Button label weight, the sidebar's accent and badge, the docs sidebar listing Chat). Repository baselines only; nothing in the package changes.

## 3.8.0

### Minor Changes

- 8d178f8: A page's own blur plane on Android: `useGlassBlurPlane` makes a plane, `GlassBlurPlane` makes the view it wraps (a page backdrop) the captured plane through `@nannier/canvas-blur`, and `GlassBlurScope` offers it to the glass surfaces inside it, which frost it where they would otherwise paint their solid skin (an in-page surface has no safe plane of its own). On iOS and the web, and on Android without the integration, both render their children only.

  Minor, justified: new public API (a hook and two components) for a capability the kit did not offer, a safe in-page blur plane on Android.

  Also fixed: a glass surface's box keeps its native view while it paints a material, so on iOS and Android the material (at zIndex -1 inside the box) can no longer be hoisted by view flattening beneath the box's earlier siblings, where it disappeared.

- 4d7e5d8: Navbar takes a `bare` surface: no fill, no material and no hairline, so the bar sits clear on whatever is behind it. It is the scroll-edge appearance of iOS's navigation bar and Material 3's top app bar: pass it while nothing has scrolled under the bar and drop it once content does, so the bar's own surface returns to keep its labels legible. A framed bar (`bordered`, `floating`) keeps its frame over `bare`.

  Minor, justified: a new public option on an existing component (the Navbar's surface axis gains `bare`). The docs' top bar uses it on Dark Factory's frosted frame, clear at rest as Dark Factory's own top bar is, frosted once the page scrolls under it.

### Patch Changes

- ae93009: Re-mint the visual baselines for the docs' page look: the preview cards sit inside Dark Factory's frame (narrower by the frame's inset) with the re-solved muted ink, and the token stylesheet shows the new muted values. Minted on Linux by the Visual baselines workflow (run 38526733071) and reviewed. Baselines only; nothing in the package changes.
- 029ea4c: The docs' frame runs a hairline past the window's bottom edge, so its frost rim no longer draws a bright line there, and its corner wedges are drawn at the frame's own size, which stops the Android docs app from crashing on a 174 MB bitmap. Docs only; nothing in the package changes.
- 613e7ee: The docs frame their pages in Dark Factory's frosted shell: an inset panel fixed to the viewport, 12 / 16 px from the window on phones, 20 / 28 on tablets and 28 / 56 on desktops (capped at 1340 wide), with its 22 px top corners, palette shadow and corner wedges, around the sidebar, the top bar and the page scroller, on the web, iOS and Android. The frame does not clip, since a rounded clip around the frosted chrome made Chromium drop tiles; the chrome that reaches its corners rounds itself. Docs and tooling only; nothing in the package changes.
- b9e743d: Legibility over Dark Factory's page look. The muted ink is solved to 4.5:1 on the frosted shell over the page gradient and its orbs as well as on every surface, so it darkens slightly in the light palettes (blush #696b8d to #606183, mint #5f7083 to #5a6b7e) and lightens slightly in dark (#a3a5c6 to #a6a8c9). A brand-tinted glass fill (a primary Button, a checked control under glass) is solved over the frame's beds as well as the page token (`frameBeds`, derived from Dark Factory with the tokens), so in dark it is a few percent denser and its ink keeps 4.5:1 there. The iOS ActionSheet's secondary header label is foreground at 0.89 instead of 0.85, which keeps it at 4.5:1 on the glass sheet.
- 95912d6: Record the page look's open differences in the audit: on GlassSurface, the iOS dark frost's own dark tint, Android's top-lit frost rim and whiter light fill, and a faint ring outside the iOS frame; on Sidebar, the web rail's lighter glass hairline and wider compact rail than Dark Factory's. Each is left for its audit turn by the owner's 2026-10-14 decision. Records only; nothing in the package changes.
- 6ea6fb3: The visual-baselines workflow gets 35 minutes instead of 20: its mints already took 16.5 to 18.3 minutes, and the first mint after the docs took Dark Factory's page look ran past the limit. CI only; nothing in the package changes.

## 3.7.4

### Patch Changes

- fe60a64: The audit's commit-reference check and its tests address the repository they are given even inside a git hook: git exports GIT_DIR to a pre-push run from a worktree, and the child git calls followed it instead of their working directory, so the pre-push battery failed three tests. Repository tooling only; nothing in the package changes.
- f2e94c9: The docs paint Dark Factory's page look behind the shell: the 165 degree pastel gradient and its two soft orbs, in blush, mint and dark, on the web, iOS and Android, drawn by one react-native-svg backdrop whose orbs are radial gradients sampled from the exact blurred ellipse. The e2e paint check reads the page look's gradient stops. Docs and tooling only; nothing in the package changes.
- ab18317: Re-record the Dark Factory page-look reference for the docs: every palette at every width, headed, with scrolled frames and the orbs' geometry, and record the owner's decisions for the docs page look.

## 3.7.3

### Patch Changes

- 0fb577d: The component audit's records cite commits that are on main. Every turn so far merged by cherry-pick, which gave its commits new SHAs on main, so the checklists, the decisions log and the audit README cited 71 turn-branch commits (142 times) that main never contains; each now names its commit on main (matched by subject). `audit:checklists:check` now fails on a commit an audit record cites that HEAD's history does not hold (`tools/audit/commit-refs.ts`, with tests), and the audit README's process says a turn merges by rebasing onto main and fast-forwarding. Repository tooling and docs only; nothing in the package changes.
- 8f93003: The component audit records GlassPane's web after capture at the turn's final code in `audit/turns/glass-pane.md` (variants 8064 of 8064 cells, interaction states 1740 of 1740 with the same 54 press states not reached) and brings GP-2's finding up to the wash's refinement: the glass-only text failures the before capture showed fall from 186 to 12, the twelve being the dark iOS control puck's muted prefix deferred to GlassSurface's turn (GP-11). Repository tooling and docs only; nothing in the package changes.
- 701dbd3: The component audit records GlassPane's web after capture at the GP-2b fix in `audit/turns/glass-pane.md` (variants 8064 of 8064 cells, interaction states 1740 of 1740 with the same 54 press states not reached): the web rows of the selected Card and the checked Radio card now show solid's 5 percent tint under glass. Repository audit records only; nothing in the package changes.
- 7dabbfb: The component audit records GlassPane's web after capture at the GP-5 and GP-9 fixes in `audit/turns/glass-pane.md` (variants 8064 of 8064 cells, interaction states 1740 of 1740 with the same 54 press states not reached): the web pictures are unchanged by them, as react-native-web drops `android_ripple` and the web material has no interactive glass. Repository audit records only; nothing in the package changes.
- eb74682: The component audit records GlassPane's web after capture at the code its fix's verification left (`audit/turns/glass-pane.md`): variants 8064 of 8064 cells, every one byte-identical to the previous after capture, and interaction states 1740 of 1740 with the same 54 press states not reached, 1682 of the 1686 captured identical and the other four the Tooltip trigger's hover wash caught at another point of its fade, in solid as in glass. The verification's changes are an Increase Contrast path the capture does not photograph, held by tests, and a refactor that returns the same styles. Repository tooling and docs only; nothing in the package changes.
- 2df9b74: The component audit records GlassPane's first web after capture in `audit/turns/glass-pane.md`: across the 73 components in its Capture through list, a variant run (8064 of 8064 cells) and an interaction-state run (1740 of 1740 cells, the same 54 press states not reached as the before capture), whose analysis found an errored field's addon prefix still under 4.5:1 in dark glass and led to the wash's refinement. Repository tooling and docs only; nothing in the package changes.
- d6d62e7: The component audit records the web before capture of the GlassPane foundation's turn in `audit/turns/glass-pane.md`: the variant run (8064 of 8064 cells) and the interaction-state run (1740 of 1740 cells, 54 press states not reached: Autocomplete's 6, ButtonGroup's 3, Command's 12, Dropdown's 6, FilterPanel's 12, Heatmap's 6, Switch's 3 and Video's 6, the same 54 cells FoundationReference's before capture left not reached) across the 73 components in its Capture through list, taken while the devices were busy, with the device runs to follow. Repository tooling and docs only; nothing in the package changes.
- 873d252: The component audit's GlassPane checklist states its first specific check without a status: the check that a pane renders nothing where its host resolves solid now names the Android re-capture of chip, badge, kbd, switch, input, card, alert, checkbox, radio and steps as its evidence instead of calling it pending, which audit/README.md records as settled (its tick is the status). The generated facts of the checklists whose components the new GlassPane ripple test renders count that test. Repository tooling and audit records only; nothing in the package changes.
- 379e367: The component audit's GlassPane records cite this turn's commits as they land on main: the checklist's fix commits, the GlassSurface rows and the tuning log's revisions now name the rebased commits instead of the turn branch's earlier ones. Repository docs only; nothing in the package changes.
- d2fb2ca: The component audit records, from GlassPane's turn, that a resting control whose boundary is its border (an unchecked Radio ring or Checkbox box, an off Switch) measures about 1.2:1 against the page under glass on the web, below WCAG 1.4.11's 3:1 where solid's border measures 3.4:1, deferred to GlassSurface's turn (GP-12, glass-surface-control-boundary-1). Repository docs only; nothing in the package changes.
- d34a0be: The component audit records the owner's GlassPane decisions in `audit/DECISIONS.md`: under glass a control's material covers its whole solid footprint (GP-1), a tinted wash is capped at the alpha that keeps its text at 4.5:1 (GP-2), and the native flow parser's tests share one 30 s budget. Repository docs only; nothing in the package changes.
- 2559f2e: The component audit records GlassPane's findings (GP-1 to GP-13) on `audit/foundation/glass-pane.md`, with the rubric and specific-check items the turn verified: the full-footprint geometry (GP-1), the Increase Contrast ring widths and state colours (GP-3) and the four deprecations (GP-4, with GP-8 and GP-10) fixed; the legible state washes (GP-2) landed, with the coordinator's blind judge of the shipped surface still open; the web focus ring's placement on a host whose border became padding (GP-13) open for the owner; the Android ripple and iOS interactive-glass device checks (GP-5, GP-9) open for the coordinator; the code pill's material deferred to Typography's turn, the GlassSurface hosts' footprint and the dark native control puck's muted ink to GlassSurface's turn, and Carousel's Android initial slide recorded on its existing row. The audit README's Native spike notes the Android re-capture that settled the pane's solid fallback. Repository docs only; nothing in the package changes.
- 1d5444a: The tuning harness's evidence log (`tools/native/liquid-motion.md`) records the glass selection wash at solid's own 5 percent tint: harness run 4 on /testing/materials and the shipped /components/radio/card, /components/card/selectable, /components/input/error, /components/phone-input/error and /components/field/amount pages in blush, mint and dark glass, awaiting the blind judge. The `solid-state-washes` reference card gains the selected Card in solid (two strips under `tools/native/reference/solid-state-washes/`, registered in `.tuning-harness.json`). Repository records only; nothing in the package changes.
- c2f97a3: The component audit records the owner's GP-2b decision in `audit/DECISIONS.md` (under glass the selection wash matches solid's 5 percent tint in every look, so dark keeps no visible selection wash), updates GlassPane's GP-2 finding with it, and records the blind judge's differences outside the wash where they belong: radio-glass-checked-indicator-1 on Radio's checklist, and on GlassSurface's the unchecked radio card edges (glass-surface-control-boundary-1) and the frost body's ink (glass-surface-frost-body-ink-1). Repository audit records only; nothing in the package changes.
- 2bc9f39: The component audit records GlassPane's GP-5 (an Android press drawn over the pane) and GP-9 (no interactive Liquid Glass on a pane) as fixed with the coordinator's device evidence, defers GP-13 (the web focus ring one CSS px out under glass) to Pressable's turn with its root cause, the kit's ring leaving the browser's `outline: auto` style, and records two findings for GlassSurface's turn: its own `interactive` surfaces under `pointerEvents="none"`, and a borderless ripple drawn beneath a frosted surface the control does not own. The GP-2 decision gains its outcome note. Repository audit records only; nothing in the package changes.
- a93fac4: The component audit records GlassPane's device after capture (an iOS run and an Android run, 2688 of 2688 cells each: every solid card and every Android glass card unchanged but the animated Progress cells and one Carousel cell, the known default-index intermittent, and 764 iOS glass cards changed by the full footprint and the washes) and the coordinator's two blind judges of the glass state washes in `tools/native/liquid-motion.md`. GP-2 closes on the second: the washes reproduce solid's, and the judge's remaining differences, all outside the wash, are recorded for GlassSurface's and Radio's turns. Repository tooling and docs only; nothing in the package changes.
- dcaff32: The component audit records the device before capture of the GlassPane foundation's turn in `audit/turns/glass-pane.md`: an iOS run and an Android run, 2688 of 2688 cells each across the 73 components that render through GlassPane (6 unstable on each, the animated cells). Repository tooling and docs only; nothing in the package changes.
- ad77183: The component audit signs the GlassPane foundation off on the web, iOS and Android (`audit/foundation/glass-pane.md`): its after captures across the 73 components that render through it, the Android ripple and iOS press checked on the devices, and the glass washes judged blind. Repository tooling and docs only; nothing in the package changes.
- 573cf80: `GlassPane`, `GlassPaneProps`, `paneStyle` and `PANE_SIBLING_INPUT` are deprecated aliases (audit K12-2): kit internals that were published by accident. Nothing is removed and the deprecation itself changes nothing at run time: each keeps working until a major, its `@deprecated` text says to use the kit component that paints the material (a Card for a content pane, a Button, Chip or Input for a control) or that it has no replacement, and the generated /foundation reference lists all four in a new Glass material section. GlassPane and GlassPaneProps carry JSDoc summaries that state the pane's geometry and its accessibility; `layer` states its own default (`control`) and `static` says it means frost in place of Liquid Glass. `PANE_SIBLING_INPUT` is documented as the no-op it is. The theming and glass pattern pages say "the kit's shared material rendering" instead of naming the internals.
- 8fab20a: Under glass a control's material now covers its whole solid footprint, as Dark Factory's glass does (the owner's GP-1 decision). GlassPane, the material a kit component paints behind its own content, copied its host's border widths onto itself inside a host that kept its own transparent border, so every bordered pane host drew a smaller surface under glass than in solid: a Chip, Badge, Kbd, Checkbox box, Radio ring, Steps disc, Select trigger, Alert, Card and the rest sat two border widths in (a 20 px radio ring painted a 12 px disc), and a kept focus or open border showed a see-through gap before the material's rim. The pane now takes only its host's corner radii and `borderCurve`; under glass `paneStyle` turns a resting border into padding of the same width, so nothing moves and the material reaches the edge, and a state border (a focused or errored field, an open Select, Android's current step, the neutral Alert's edge, an Avatar's separator ring) stays a border with the material flush inside it on concentric corners. Absolutely placed content keeps its solid place (the Android Switch thumb, the Android Select and Autocomplete floating labels), and every pane host on the web, iOS and Android skins keeps its solid insets, held by the new `test/glass-pane.test.tsx`. Solid mode is unchanged. GlassSurface's own hosts (the Tabs track, ButtonGroup) are aligned in GlassSurface's turn.

  Two deprecated aliases gain an optional prop for this: `GlassPaneProps.stateBorder`, which sits the pane flush inside a border its host keeps (pair it with `paneStyle`'s third argument), and `FloatingLabel`'s `edges`, how far a field's padding box reaches over its border, which the label's bounds step back from. `paneStyle`'s glass result for a resting host now carries padding where the border was: an app that composes it and sets a padding after it replaces that padding, and its content sits one border width further out under glass than in solid; hand `paneStyle` the whole box instead, as the kit's components do. On the web a focused host whose border became padding draws Chromium's `outline: auto` focus ring one CSS px further out than in solid (audit GP-13, open).

- c409bf2: GlassPane no longer asks iOS 26 for interactive Liquid Glass. A pane passes every tap through to the control it paints behind (`pointerEvents="none"`), so React Native disables user interaction on it and its glass never received the touch Apple's interactive glass responds to: on the iOS 26 simulator a pressed glass Button showed only its label's dim, the same as before this change. The Button, Chip, Select, Calendar day, Carousel arrow, Navbar link, Sidebar row, TabBar tab, Tabs trigger and Stepper button panes stop requesting it, and GlassPane stops forwarding it, which also stops the native view reinstalling its glass effect each time a control is enabled or disabled; the resting look is unchanged and the control's own dim carries the press. `GlassPaneProps.interactive` stays as a deprecated no-op, so code that passes it still compiles. GlassSurface's `interactive` is documented to act only on a surface that itself receives the touch.
- 2fedd75: On Android a pressed control shows its ripple again where its glass material renders. A ripple is the Pressable's background, which Android draws before the view's children, and the GlassPane that paints a control's surface under glass is one of those children: wherever its frost rendered (an overlay outlet, a bridged Modal, the Material 3 bottom sheet a Popover becomes on a phone), it covered the press feedback, and a borderless ripple, projected onto an ancestor's background, landed beneath it too. The press now draws in the foreground wherever the pane renders, over the material and the label in the ripple's own translucent colour, as Compose draws Material 3's state layer: a Button, a tappable Chip, the Select trigger, a pressable Card, MediaObject and Stats card, the current Pagination page, a Radio and its card, the Checkbox box, the Stepper's buttons, the selected Calendar day, the Carousel arrows, the active Navbar link, Sidebar row and TabBar tab, the selected Tabs pill and rail item, a pressable Steps disc, a pressable Avatar tile and ButtonGroup's spaced peers, as well as an Alert's dismiss button and an Input's clear and reveal buttons, whose borderless ripples sat beneath their host's pane. Solid mode, Reduce Transparency, Increase Contrast and Android's in-page glass, which resolves solid, keep the ripple exactly as before; iOS and the web have no ripple.
- 2c411cc: Under glass a selected Card and a checked Radio card now paint the same soft primary tint as in solid, 5 percent, in blush, mint and dark (the owner's GP-2b decision). This is a rendered change: the glass selection wash was capped only for legibility, which left it about a point heavier than solid's tint in the light palettes and about 15 percent in dark, where the card read visibly violet and its title and description fell from 11.2:1 and 5.5:1 to 9.5:1 and 4.7:1. The wash's ceiling is now the solid recipe's own tint, read from one constant that Card and Radio's card chrome share (they each wrote the 5 percent literal before), and it is still capped where the card's text would lose 4.5:1. Solid mode renders exactly as before.
- 702acca: The glass error wash holds its ink on a field's addon too. An errored field's prefix or suffix addon, and PhoneInput's country segment, paint a soft inner fill over the error wash under the same muted ink, which the cap did not count: an errored field's `$` prefix measured 3.55:1 in dark glass. The cap now holds the ink on that fill as well, and is solved with a little headroom over 4.5:1, since the material samples a backdrop a little lighter or darker than the page token the cap is solved on. The error wash is now none in the light palettes and next to none in dark, so an errored field under glass reads as in solid, its own body with the error border and the message. The selected card's wash is set separately, to solid's own 5 percent tint in every look.
- 55b434b: The glass state washes keep their text legible (the owner's GP-2 decision). Under glass an errored Input, Textarea or PhoneInput and a checked Radio card or selected Card painted a destructive or primary wash at 18 or 22 percent in place of the pane's own body, which dropped the muted placeholder, prefix and description on it to about 3.5:1. Each wash now lies over the pane's own body and is capped at the densest alpha at which the same inks as in solid mode, muted and foreground, keep 4.5:1 over the page and over a content pane, in blush, mint and dark: the state is carried as in solid by the error border and by the selected card's primary border, which a selected Card and a checked Radio card now keep over the material. (A later change in this release sets the selection wash to solid's own 5 percent tint in every look and leaves the error wash none.) The values live in one table, `src/style/glass-surface/glass-wash.ts`, a tuning-harness tunable.
- 39c5221: Two of this release's glass wash notes no longer describe a dark selection wash that a later change in the same release removed, and the /testing/materials State washes fixture comment says what the washes now are: no error wash in any palette, and solid's own 5 percent selection tint. Wording only; nothing renders differently.
- 0252e28: The repository guidance (CLAUDE.md and AGENTS.md, identically) now says what `paneStyle` does under glass since the owner's GP-1 decision: it drops the node's own fill and trades a resting border for padding of the same width, so the pane covers the node's whole solid footprint while a state border stays a border over it. Repository guidance only; nothing in the package changes.
- 8044005: CLAUDE.md and AGENTS.md name every file the tuning gate holds: the glass state-wash table (`src/style/glass-surface/glass-wash.ts`) and solid's selection tint (`src/style/selected-card.ts`) join the motion, web frost and identity hue tables, as `.tuning-harness.json` already lists them (the owner approved the wording on 2026-10-13). Repository guidance only; nothing in the package changes.
- 8fab20a: Increase Contrast no longer thins a ring. The contrasting border a resting surface takes under Increase Contrast was a 1 px foreground hairline on every surface, so a 2 px Radio ring, the Android Switch's unchecked track and the Steps discs thinned to 1 px; it now keeps each side's own width where that is wider than the hairline, in foreground, and restates a side's own colour in foreground too. A border that shows a state keeps every colour its style names, so Android's current step keeps its 2 dp primary ring instead of losing it to the foreground hairline, a Material 3 underline field (the Android Input, Textarea, Select and Autocomplete) keeps its error, focus or open colour on its 2 dp active indicator, and a selected Card and a checked Radio card, which now hand their primary border to `paneStyle` as a state border in solid too, keep it instead of taking the foreground hairline.
- a632b77: The Input page says truthfully how an errored field reads in each mode: solid paints the iOS field's error wash, and under glass, where a wash would cost the field's ink its contrast over the material, the iOS field takes none (the web's a trace in dark only), so the error border, the tinted glyph and a Field's message carry the state. Docs only; nothing in the package's behaviour changes.
- 2fedd75: The audit's state-recipe test follows Button's `disabled` attribute to its new line in `button.shared.tsx` (209, two lines down after GP-5's ripple comment). Repository tooling tests only; nothing in the package changes.

## 3.7.2

### Patch Changes

- 70222ae: The native flow parser's regression tests all carry one 30 s budget. Each starts the pinned Maestro's JVM, which takes 4 to 9 s on a CI runner, and five of them ran under bun's 5 s default when CI runs the file on its own, so one timed out on the iOS smoke (the owner authorized the budget, 2026-10-12). Repository tooling only; nothing in the package changes.

## 3.7.1

### Patch Changes

- 5457963: The component audit records the web after capture of the GlassModalBlurTarget foundation's turn in `audit/turns/glass-modal-blur-target.md`, across the 4 components in its Capture through list (ActionSheet, Drawer, FilterPanel and Sidebar), with the device runs to follow: a variant run (252 of 252 cells) and an interaction-state run (234 of 234 cells, the same 12 press states the before capture left not reached). Every picture and every probe matches the before capture byte for byte (the probes once the local server's port in their urls is set aside), as the turn's changes leave the web rendering of these components unchanged. Repository tooling and docs only; nothing in the package changes.
- da5570d: The component audit records the web before capture of the GlassModalBlurTarget foundation's turn in `audit/turns/glass-modal-blur-target.md`: the variant run (252 of 252 cells) and the interaction-state run (234 of 234 cells, 12 press states not reached: FilterPanel's press on its option checkbox, the same 12 cells FoundationReference's before capture left not reached) across the 4 components in its Capture through list (ActionSheet, Drawer, FilterPanel and Sidebar), taken while the devices were busy, with the device runs to follow. Repository tooling and docs only; nothing in the package changes.
- f20a18e: The component audit records three findings the GlassModalBlurTarget turn found in other components for their own turns: ActionSheet's header and a drawer-mode FilterPanel's chrome resolve their material around their Modal, so on Android they keep the solid recipe inside a frosted sheet or drawer (`audit/components/action-sheet.md`, `audit/components/filter-panel.md`), and GlassSurface's frost keeps an unreachable expo-blur `blurTarget` branch (`audit/foundation/glass-surface.md`). Repository docs only; nothing in the package changes.
- 5457963: The component audit records GlassModalBlurTarget's findings (GMBT-1 to GMBT-7) on `audit/foundation/glass-modal-blur-target.md`, with the rubric and specific-check items the turn verified: the Modal blur bridge documented on Integration, the solid fallback for a refused plane (the owner's GMBT-2 decision, with `@nannier/canvas-blur`'s new event), the Android page-only sampling documented (GMBT-3), the materials smoke reading the capture with the Drawer and an ActionSheet open on the Android emulator, the contract JSDoc and comments, the tests and design rules, and two consumer findings deferred to ActionSheet's and FilterPanel's turns. Repository docs only; nothing in the package changes.
- da09204: The component audit records the device after capture of the GlassModalBlurTarget foundation's turn in `audit/turns/glass-modal-blur-target.md`: an iOS run and an Android run, 84 of 84 cells each across ActionSheet, Drawer, FilterPanel and Sidebar. Every card is pixel-identical to the before capture, and so is every recorded accessibility node, as the turn changes no rendering of these components' closed triggers. Repository tooling and docs only; nothing in the package changes.
- 49b9f71: The component audit signs the GlassModalBlurTarget foundation off on the web, iOS and Android (`audit/foundation/glass-modal-blur-target.md`): its after captures match the before captures across ActionSheet, Drawer, FilterPanel and Sidebar, the Android Modal frost and the misplaced bridge's solid fallback are proven on the emulator, and /integration's Native modals section was read on all three platforms. Repository tooling and docs only; nothing in the package changes.
- 12bfcab: The Android capture plane's host and consumer comments describe the `@nannier/canvas-blur` capture host the plane has been since 0.1 rather than expo-blur's `BlurTargetView`, and say who samples it (the provider's outlet, and a Modal's surfaces through `GlassModalBlurTarget`). Comments and test headers only; nothing in the package changes.
- a86ba01: Two design rules hold the Android capture plane out of its own window (`test/design-rules-source.test.ts`): the window-level plane is read only by `OverlayProvider` and `GlassModalBlurTarget`, and every React Native `Modal` in the kit puts its content under `GlassModalBlurTarget`, so its glass surfaces can frost the page on Android. Both were comments until now; each check carries a case it must catch. Tests only; nothing in the package changes.
- cef469a: `GlassModalBlurTarget` falls back to the complete solid skin when Android refuses its plane. Placed anywhere in the main window (around page content, or in a react-native-screens or expo-router modal screen, which shares the main window on Android), the bridge handed its surfaces the capture plane they sit inside, which `@nannier/canvas-blur` can never sample: the surfaces resolved frost and painted a tint with no blur. The Android glass host now forwards the frost's new `onTargetRejected` report (canvas-blur 0.2) to the nearest bridge, which stops publishing that plane, so every surface and pane host under it resolves its solid skin through the shared material resolution and releases its capture demand (the owner's GMBT-2 decision). The verdict is per bridge and per plane and starts trusting, so a bridge inside a React Native Modal, which never contains the plane, frosts from its first frame as before; with an older canvas-blur nothing changes. Its JSDoc now states the contract: what it provides, that it renders no view, the Android preconditions, where it belongs, what it samples and the no-op platforms. `test/glass-modal-blur-target.test.tsx` holds the verdict and the ActionSheet and Drawer paths through the bridge.
- 4a4e248: Follow-ups from the GlassModalBlurTarget verification. The Integration page's Modal example and the README's custom-Modal recipe pass `accessibilityViewIsModal`, as the Accessibility note beside them says and as Drawer and ActionSheet do. The Native modals bullets set the package and component names as code. The bridge's JSDoc and the page say that a misplaced bridge shows a tint with no blur until the native frost reports the refusal, then its solid skin. A stale comment in the Android capture host no longer names expo-blur's BlurTargetView, and the materials fixture's misplaced-bridge card says it keeps its solid skin on Android only, which is where that holds.
- 4a6314f: The Integration page documents overlay hosting and the Modal blur bridge (audit K12-5). A new Overlay hosting section presents `OverlayProvider` at the app's root, filling the window, and on Android with `@nannier/canvas-blur` the capture plane its page becomes. Its Native modals part presents `GlassModalBlurTarget` for an app hosting its own React Native `Modal`, with a kit-only example in Drawer's order (the bridge, then `OverlayProvider separateWindow`, then the content), the Android preconditions, the no-op platforms, the rule that it belongs only inside a Modal (in the main window its surfaces keep their solid skin), what an Android Modal's frost samples (the outermost provider's page only, not the toasts, a Modal beneath or its own scrim, while iOS and the web blur the whole stack; the owner's GMBT-3 decision), and an accessibility note. The README's custom-Modal recipe and Android blur paragraph name the bridge, which they had left out, so an app that followed them got solid Modal panels on Android. The public API manifest documents `GlassModalBlurTarget` and `OverlayProvider` there and the materials inventory gives both the route; `OverlayProviderProps` and `OverlayViewportInsets` stay staged for OverlayProvider's turn. Docs only; nothing in the package changes.
- 514849f: The native materials smoke opens the material Drawer and a new material ActionSheet in glass on Android and reads the capture counters while each is open (a capture host active and a frost view retained, through `GlassModalBlurTarget`) and again after each closes (idle, still in glass), and Back dismisses the sheet. The fixture gains the ActionSheet, an inspector inside the drawer and a card under a `GlassModalBlurTarget` placed in the page, whose refused plane must leave no frost mounted. Repository tooling only; nothing in the package changes.

## 3.7.0

### Minor Changes

- bab5132: Typography tabular figures for numbers read down a column or updated in place: Typography gains a `tabular` boolean, its own single-member numerals axis, that gives every digit the same width on iOS, Android and the web, so a column of figures lines up and a live metric or counter holds still as its digits change, the way Dark Factory sets its figures. It combines with every role, tone, weight, leading and decoration, and prose keeps the face's own proportional figures. With it in place, `tabularNums` is a deprecated alias (the owner's K12-2 decision) that keeps working until a major and points to `<Typography tabular>`.

### Patch Changes

- 66641e9: `check:api` now fails when any docs page but the Foundation reference, or DESIGN.md or README.md, presents a deprecated alias or a kit-internal helper as API: imported, called, rendered or typed in a snippet, or set alone as inline code. The kit's docs no longer teach a helper the kit retired. Repository tooling only; nothing in the package changes.
- 886b8b7: The public API gate (`check:api`) ends the staged docs migration route by route instead of all at once when the `/foundation` reference appears: `tools/api/manifest.ts` `PLANNED_ROUTES` gives every planned route the audit turn that lands its docs, that turn closes it, and nothing may be staged for a closed route or left open with nothing staged. Every deprecated alias's home is the foundation reference, which must list each one once it exists. Repository tooling only; nothing in the package changes.
- a9254e7: The component audit records the web after capture of the FoundationReference foundation's turn in `audit/turns/foundation-reference.md`, across the 98 components in its Capture through list, taken while the devices were busy, with the device runs to follow: a variant run (10710 of 10710 cells, the 10692 of the before capture and Typography's new Tabular example in 18) and an interaction-state run (2010 of 2010 cells, the same 54 press states the before capture left not reached) after the turn's fixes, and the same pair again after the corrections a verifier's re-read called for, which touch only the Foundation reference page and the native menu's glyph rasters. The second variant run matches the first: every picture byte for byte, and every DOM probe once the local server's port in its url is set aside. Against the before capture every variant picture is byte-identical but 27 Typography phone cards (nine examples, each in the blush, mint and dark solid looks), which differ by at most one level of 255 on 5 to 12 pixels at the card's bottom edge; every variant probe and every web flag is the same, and the 18 Tabular cells carry no flag. Every state picture and probe is the same across the three state runs but Tooltip's hover-opened ones, which every run flags hover-unstable: the capture's resting pointer has to follow the trigger once or twice, so whether the trigger shows its hover wash, and the contrast the probe reads on it, changes from run to run (two pictures differ from the before capture, four from the first after run). Repository tooling and docs only; nothing in the package changes.
- 6e402a7: The component audit records the web before capture of the FoundationReference foundation's turn in `audit/turns/foundation-reference.md`: the variant run (10692 of 10692 cells) and the interaction-state run (2010 of 2010 cells, 54 press states not reached: the 45 of Autocomplete, ButtonGroup, Command, Dropdown, FilterPanel and Video that earlier turns' before captures also left not reached, and Heatmap's 6 and Switch's 3 on glass, neither component in an earlier turn's Capture through list) across the 98 components in its Capture through list, taken while the devices were busy, with the device runs to follow. Repository tooling and docs only; nothing in the package changes.
- f770644: The component audit records FoundationReference's findings (FR-1 to FR-13) on `audit/foundation/foundation-reference.md`, with the rubric and specific-check items the turn verified, closes the deferred AO-9, BO-11, EN-6 and FL-10 with the Foundation reference page, and records the roving-focus fix on Radio's and Tabs' checklists. Repository tooling and docs only; nothing in the package changes.
- 468c75f: The component audit signs the FoundationReference foundation off on the web, iOS and Android: the native after runs match the before runs on every card but the animated Spinner and Progress examples and Carousel's default-index example, an intermittent Android bug recorded for Carousel's turn that the turn's changes do not touch. Repository tooling and docs only; nothing in the package changes.
- bed396f: The component audit's tooling records only the runs a turn's capture step made itself (a turn's web capture in the same checkout no longer lands in another step's record), and its native capture keeps TalkBack's compressed accessibility tree beside the full Android dump. Repository tooling only; nothing in the package changes.
- 87cb43b: A controlled component whose parent clears its value to `undefined` keeps showing the last value the parent gave, as React's own controlled input does, instead of jumping back to the value it mounted with (a cleared Listbox or Calendar selection showed its first selection again). It stays controlled, and switching a component between controlled and uncontrolled after it mounts now warns once in development, either way.
- f537ba4: Every deprecated alias the kit declares says what it was in its JSDoc summary, the hover an editor shows (React Native's own `StyleSheet` and `useWindowDimensions` stay React Native's to document): the 24 that had none (among them `FOCUS_RESET`, `MONO_FONT`, `isRTL`, the ripple and touch-target helpers, `useRovingFocus`, `useHardwareBack`, `useWheel` and their option types), `brandColors` and `HUE_WASH`. `check:api` now requires a summary on every deprecated alias, value or type, beside its `@deprecated` text. Documentation only; no runtime change.
- 6c5c5af: The kit's own helpers that were public by accident are deprecated aliases (the owner's K12-2 decision, with OD5 for React Native's `StyleSheet` pass-through): `activeIndicator`, `ActiveIndicatorOptions`, `CodeToken`, `containerStyle`, `DASHBOARD_COLUMNS`, `DashboardTier`, `devWarn`, `effectiveSpan`, `enableAndroidLayoutAnimations`, `FLUID`, `FOCUS_RESET`, `fontStyle`, `GESTURE_SURFACE`, `gridCellWidth`, `gridColumns`, `holdThen`, `innerFill`, `InnerFillRole`, `InnerFillStrength`, `inverseDenseTint`, `isGlass`, `isRTL`, `keyframes`, `Measure`, `measureOf`, `MONO_FONT`, `moveWidget`, `normalizeWheelDelta`, `orderedWidgets`, `resetDevWarnings`, `RovingFocusOptions`, `RovingItemProps`, `StyleSheet`, `supportsNativeDriver`, `syntaxColor`, `thereAndBack`, `tokenize`, `TokenKind`, `useControllableState`, `useDialogFocus`, `useEscapeKey`, `useHardwareBack`, `usePopoverFocus`, `useRovingFocus`, `useWheel`, `WheelGesture` and `withInnerFill`. Every one keeps working until a major, and its `@deprecated` text names the kit component, prop or token to use (`Container`, `Grid`, `CodeBlock`, `DashboardGrid`, `Text`, `Typography`, `useTheme`, `Dialog`, `Popover`) or says that nothing replaces it. The package exports the same names as before.
- 7d0ea73: The docs gain the Foundation reference (`/foundation`, under Guides on the web and on iOS and Android, and in search under every deprecated name): every deprecated name in Canvas, grouped by the part of the kit it comes from, with what it was and what to use instead, linked to the replacement's page where one component, prop or token replaces it. Its rows are generated from the public API manifest and the source, so the page cannot drift from either, and it carries the contracts the retired machinery kept: how an anchored card behaves for every menu and Popover, how to test one without layout, the field label's contract, and server rendering against the window. React Native's `StyleSheet` pass-through's deprecation text no longer cites an audit ID.
- cdfc003: `docs:gen` writes the `/foundation` reference's rows (`docs/src/core/foundation-reference.ts`) from `tools/api/manifest.ts` and the source: every deprecated alias with its declaration's summary, its `@deprecated` text and its replacement's docs route, and `docs:gen --check` fails when they drift. `check:api` reads those rows as part of the page and now requires every deprecated alias's `@deprecated` text to say what to use instead and to name its replacement when it has one. Repository tooling only; nothing in the package changes.
- 30841a2: The arrow keys of a RadioGroup no longer select a disabled Radio: its arrows, Home and End walk past a disabled option in their direction of travel, and a disabled option is never the group's tab stop, so a group whose selected option is disabled still has one (the first enabled option), as a browser's own radio group does. Tabs keeps the same rule, now from the shared roving-focus behaviour, and a disabled active tab hands the tab stop to the first enabled one instead of leaving the row unreachable by Tab.

  For code still on the deprecated `useRovingFocus`, its `RovingFocusOptions` gains an optional `isDisabled(index)` that carries this rule: the item it names is skipped by the arrows, Home and End and is never the web tab stop. The change is additive. A call without it moves as before, except in two cases: an active index outside the group now makes the first item both the tab stop and the start of an arrow's walk (before, no item was the tab stop and the arrows moved from the out-of-range index), and an arrow at an end without `loop` no longer activates the end item again.

- 570d4e5: The touch-target and press-feedback helpers are deprecated aliases (the owner's K12-2 decision): `TOUCH_TARGET`, `platformMinTarget`, `minTargetSlop`, `useMinTargetSlop`, `TouchTargetSkin`, `MinTargetOptions`, `pressDim`, `controlRipple`, `surfaceRipple`, `rippleClip` and `platformDisabledDim`. Each keeps working until a major. Nothing replaces them: every kit control shows the platform's press feedback and meets the 44 pt and 48 dp minimum touch target itself (Button, an icon Button included, Chip, and Avatar, Card and the list rows given `onPress`), and a control the kit lacks belongs in the kit, never in a hand-built Pressable. DESIGN.md and Button's Touch area section no longer present them for building your own control.

## 3.6.4

### Patch Changes

- 0fe5f82: The component audit records the web after capture of the FloatingLabel foundation's turn in `audit/turns/floating-label.md`, across the 9 components in its Capture through list, taken while the devices were busy, with the device runs to follow: a variant run (1476 of 1476 cells) and an interaction-state run (480 of 480 cells, the same 6 press states the before capture left not reached, Autocomplete's press on its chevron) after the turn's first fixes, and the same pair again after its required-name fix. Every DOM probe of the second pair is identical to the first's, every component picture matches it to within one level of 255, and the state pictures that differ more show the page prose the six field pages gained above the example. Repository tooling and docs only; nothing in the package changes.
- d98567b: The component audit records the web before capture of the FloatingLabel foundation's turn in `audit/turns/floating-label.md`: the variant run (1476 of 1476 cells) and the interaction-state run (480 of 480 cells, 6 press states not reached, Autocomplete's press on its chevron, the same 6 the Entrance turn's before capture left not reached) across the 9 components in its Capture through list, taken while the devices were busy, with the device runs to follow. Repository tooling and docs only; nothing in the package changes.
- 0fe5f82: The component audit records the device before capture of the FloatingLabel foundation's turn in `audit/turns/floating-label.md`: one run on the iOS simulator and one on the Android emulator, 492 of 492 cells each, across the 9 components in its Capture through list. Repository tooling and docs only; nothing in the package changes.
- e37a60a: VoiceOver and TalkBack no longer read a field's visible label twice or speak its required star, and they now hear a required field as required. On iOS and Android the label of Input, Textarea, Select, Autocomplete, PhoneInput and Stepper (above the field, or floating in it on Android) is hidden beside the field it names, so the screen reader stops once, on the field. React Native has no native required state (its `aria-required` reaches the web alone), so a required field's own name says it there: "Full name, required" for Input, Textarea, Autocomplete, PhoneInput's number field and Stepper's group and field, as Select's trigger already said on every platform; a name the caller gives through `accessibilityLabel` takes the word too. Before, the only native cue was the star in the label's own stop, beside the field. The label Field draws over a control that names itself (a Switch) stays a stop of its own, named without its star and, when `required`, as "Notifications, required". Stepper's native group no longer carries an `aria-required` that React Native dropped. The web is unchanged: the label stays in the page as the field's `aria-labelledby` target, the star stays `aria-hidden`, and the field's `aria-required` announces it.
- 84d0123: The Android floating label does no animated work while a user types or as a form mounts. It built three new animated nodes on every render, and a field re-renders on every keystroke, so React Native attached a new animated graph per keystroke; it now keeps its nodes while their ends hold. And a field that mounts at rest or filled no longer starts two empty 150 ms timings.
- e3a5e0b: FloatingLabel's JSDoc (a deprecated export) states its contract: what it renders and never renders, its defaults, both timings, Reduce Motion, the floated first frame, the right-to-left scale origin, its geometry and what assistive technology gets. It also says truly why its colour tint runs on the JS driver: React Native 0.86's native driver, with its default feature flags, writes a frame onto the native view's props, and Android's text view takes its colour only through the paragraph's attributed text, so the native driver would not repaint the label (read from React Native's source, not yet confirmed on a device). The motion module's native-driver gate now names the tint as its one exception. Comments only; nothing behaves differently.
- d7b35ad: `FloatingLabel`, `FloatingLabelStyles`, `HIDDEN_FROM_A11Y` and `LabelContent` are deprecated aliases (the owner's K12-2 decision): all four keep working until a major, and their `@deprecated` text names what to use instead. Input, Textarea, Select and Autocomplete float their label for you, and every field family (those four with Field, Stepper and PhoneInput) renders its label and required marker from its own `label` and `required` props.
- 200f619: In a right-to-left layout the Android floating label of Input, Textarea, Select and Autocomplete now floats with its leading edge pinned. It scaled around the physical left while it sits at the logical start, the right, so its leading edge drifted inward by a quarter of its width.
- dd73489: The Android floating label of Input, Textarea, Select and Autocomplete follows the user's text size. It measures its rendered line instead of trusting the skin's numbers, raises the value's top padding when a larger text size makes the floated label taller, and lands the float on it; Select's and Autocomplete's fields now grow from a minimum height and Input's grows by what the text adds, so at large Android text sizes the floated label no longer overlaps the value and the value is no longer clipped. At the default text size nothing changes.
- dc17665: The Android floating label of Input, Textarea, Select and Autocomplete paints in the theme's registered face (`<ThemeProvider fonts>`, Manrope for Dark Factory) like the rest of the field. It rendered through React Native's own animated text, which skipped the theme's face, so it painted in the platform's default face beside a Manrope value and required star.
- 2b4a3f1: A long Android floating label stays inside its field. On Input, Textarea, Select and Autocomplete it is now bounded by the field's end padding, or by Select's chevron and Autocomplete's toggle, and truncates on one line with an ellipsis; it ran out of the field before, across Autocomplete's toggle and cut at Select's edge. The field's accessible name stays the whole label.
- d94605d: PhoneInput's label on Android takes the foreground ink at the Android field label's type, so it reads in the dark scheme. It had no colour of its own and painted the platform's default black, about 1.45:1 on the dark page. Input's and Textarea's Android labels above the field (a grouped Input, a flush Textarea) render as before.
- c43020d: Stepper's number paints in the theme's registered face (`<ThemeProvider fonts>`, Manrope for Dark Factory) on every platform. Its value field rendered React Native's own TextInput, which skips the theme's face, so the number painted in the platform's default face beside a Manrope label.

## 3.6.3

### Patch Changes

- 2e0bee2: The component audit records the web after capture of the Entrance foundation's turn in `audit/turns/entrance.md`: the variant run (1764 of 1764 cells) and the interaction-state run (846 of 846 cells, the same 27 press states the before capture left not reached) across the 13 components left in its Capture through list once Dialog and AlertDialog stopped rendering Entrance, taken while the devices were busy, with the device runs to follow. Every picture and every DOM probe is identical to the before capture's. Repository tooling and docs only; nothing in the package changes.
- 276563a: The component audit records the web before capture of the Entrance foundation's turn in `audit/turns/entrance.md`: the variant run (2016 of 2016 cells) and the interaction-state run (1008 of 1008 cells, 27 press states not reached, the same press states the AnchoredOverlay turn's capture left not reached) across the 15 components in its Capture through list, taken while the devices were busy, with the device runs to follow. Repository tooling and docs only; nothing in the package changes.
- a5cd71a: The component audit records the device before capture of the Entrance foundation's turn in `audit/turns/entrance.md`: one run on the iOS simulator and one on the Android emulator, 672 of 672 cells each, across the 15 components then in its Capture through list. Repository tooling and docs only; nothing in the package changes.
- 18da97a: Dialog and AlertDialog size their card with a plain view instead of `Entrance`. They never held the card (Entrance stayed ready there, a leftover of the removed spring), so nothing changes on screen, for touch or for assistive technology: the rendered tree is the same apart from the wrapper's redundant `pointer-events: auto` class, and the card's readiness for nested overlays passes through unchanged.
- 80ec047: `Entrance` and `EntranceProps` carry JSDoc that states the readiness hold's contract exactly: while held a card is invisible, takes no touch or click and is hidden from VoiceOver, TalkBack and the web's accessibility tree, its controls stay mounted (and, on the web, in the Tab order, so a hold lasts only the frame or two a placement takes), focus enters only at release, `ready` defaults to true and an enclosing hold holds it too, and `style` is the wrapper's layout and position only, never a restyle. The hold has no motion, so Reduce Motion has nothing to remove. Documentation only; nothing changes at runtime.
- 16f2775: `Entrance` and `EntranceProps` are deprecated aliases (the owner's K12-2 decision): both keep working until a major, and their `@deprecated` text names what to use instead. Entrance is the readiness hold every anchored card (Popover, Dropdown, Select, Autocomplete and the rest) applies to its own card until its placement is measured, so use those components rather than the hold itself.
- 8eb4a79: An anchored card opened from inside another anchored card that is still waiting for its placement (both open at mount) now waits with it. The inner card renders in the overlay outlet, outside the outer card's view, so for the outer card's frame or two of measuring it showed and reached screen readers while the trigger it belongs to was still invisible and hidden. The readiness hold now holds a card while any logical ancestor holds, and the card takes its modal hold, its keyboard bridge and its focus entry only once it is shown. Nothing changes for a card whose ancestors are shown, which is every card opened by a user.
- a64a8ba: On iOS, an anchored card covered by a modal card opened from it (a Popover under the Select list it opened) is hidden from VoiceOver as intended. The readiness hold passed `aria-hidden={false}` whenever the card was shown, and React Native lets a defined `aria-hidden` overwrite `accessibilityElementsHidden`, so the covered card stayed exposed under the top card. `aria-hidden` is now set only while the card is held; the web output is unchanged, and Android kept the card hidden already.
- e6a822d: An anchored card's readiness hold (Popover, Dropdown, Select, Autocomplete and the rest) stops taking touches through `style.pointerEvents` instead of the `pointerEvents` prop, so react-native-web no longer prints its "props.pointerEvents is deprecated" warning in development when a card opens. A held card still takes no touch or click, and a shown card takes them as before: React Native reads the style natively, and on the web the overlay outlet passes pointer events to the cards in it.
- 7d53b5e: Reveal's `style` documentation no longer says it is "documented exactly as Entrance documents its own": Entrance is the anchored cards' readiness hold, its `style` is not the LayoutStyle Reveal takes, and the comparison dated from the overlay spring the owner removed. The /components/reveal props table follows. Documentation only.

## 3.6.2

### Patch Changes

- 4fb1a09: The component audit records the web after capture of the BreakpointOverride foundation's turn in `audit/turns/breakpoint-override.md`: the variant run (3618 of 3618 cells) across the 29 components in its Capture through list, the interaction-state run (1260 of 1260 cells, the 45 press states the before capture also left not reached) across the 24 of them with state recipes, and the page run (18 of 18 cells) of `pattern-responsive`, taken while the devices were busy, with the device runs to follow. Every variant and state picture is byte-identical to the before capture's; on the page the three sections whose text the turn changed differ, as does each cell's viewport shot, which shows the changed Breakpoints text, while four untouched sections differ in solid mode at the anti-aliasing level only, invisible to the eye. Repository tooling and docs only; nothing in the package changes.
- 7d623ae: The component audit records the web before capture of the BreakpointOverride foundation's turn in `audit/turns/breakpoint-override.md`: the variant run (3618 of 3618 cells) across the 29 components in its Capture through list, the interaction-state run (1260 of 1260 cells, 45 press states not reached) across the 24 of them with state recipes, and the page run (18 of 18 cells) of `pattern-responsive`, taken while the devices were busy, with the device runs to follow. Repository tooling and docs only; nothing in the package changes.
- d8f3adb: A container measurement's pre-measurement seed now follows a `BreakpointOverride`. `useContainerBreakpoint` with `seedViewport` resolves its map from the active viewport bucket, and `useContainerWidth` falls back to the window width clamped into that bucket, so Row `stacks` and spans, Grid, Form, DataTable, DescriptionList, GridList, Tabs, Steps and Navbar paint their first frame in the simulated tier under an override (a docs preview's phone, a test pinning a tier) instead of the real window's. Without an override nothing changes: the window already sits in its own bucket. A window that reports no width (0, as a hidden iframe does) still seeds 0 under any bucket, so nothing is sized against a guess. The seed is still withheld until hydration, and the measured width still wins once it lands.

  The viewport tier's contract is stated in its JSDoc: the form factor is the window's width alone on every platform (a narrow desktop window is the phone tier, a landscape phone usually the tablet tier, and there is no separate desktop-OS tier), and `BreakpointOverride` pins the tier, not the geometry, so a drawer or a phone sheet under it lays out against the real window.

- 3277b66: `SsrBreakpointContext` and the `useWindowDimensions` pass-through are deprecated aliases (the owner's K12-2 decision and its OD5): each keeps working until a major, and its `@deprecated` text names what to use instead. For `SsrBreakpointContext`, pass ThemeProvider's `ssrBreakpoint` prop. For `useWindowDimensions`, use `useBreakpoint` or `useFormFactor` for window-level chrome, `useContainerWidth` or `useMeasuredWidth` inside a component, or React Native's own `useWindowDimensions` for raw pixels.
- 158f64e: ThemeProvider no longer remounts the app when `ssrBreakpoint` is set or cleared. It wrapped its tree in the server-breakpoint context only while the prop was set, so an app that dropped the prop after hydration changed the element type above it and lost all of its state. The context is now always provided, carrying the prop or, when it is omitted, the nearest parent provider's assumption, so a nested ThemeProvider still inherits it.
- d76c411: Under a `BreakpointOverride`, `useBreakpoint`, `useResponsive` and `useFormFactor` no longer subscribe to the window. The override is their answer, yet each held the window subscription and re-rendered on every real bucket crossing, for example the docs' simulated phone while the browser window was resized. Outside an override nothing changes.
- 23bbd7d: The viewport tier is documented on the Layout & flexbox page (`/tokens/layout`): the buckets and the phone, tablet and desktop form factors (the window's width alone, on every platform), `responsive`, `Responsive`, `formFactor` and `FormFactor`, and `BreakpointOverride` with what it pins (the tier for every viewport reading under it, the portaled overlays it opens included), what it does not (container-measured components follow their real width; a drawer or phone sheet lays out against the real window) and its accessibility notes. The theming page's server-rendering guidance names ThemeProvider's `ssrBreakpoint`, and the Responsive pattern names Popover's phone sheet among the window-level readers.

  DESIGN.md's responsive passage is restated from the design law: fill and hug inside the bounds the parent provides (no fixed width), the three container hooks and their bucket seed, the window-level readers (the drawer modes, an overlay's phone sheet, app shells), the thresholds, `base` for an unknown width, `ssrBreakpoint` and `BreakpointOverride`.

## 3.6.1

### Patch Changes

- d16973d: The Android native smoke builds its release before the emulator boots, so the build no longer starves the idling emulator, and a failed instrumentation build now leaves evidence: Gradle runs it with `--stacktrace`, and the passive host diagnostics record free disk space and inodes on the filesystems the build writes to.
- ca20083: The native avatar smoke scrolls to the pressable identity discs, which sit below a phone's fold, before tapping them.
- aa09d0d: The native materials fixture loads the optional Android capture module under its published name, `@nannier/canvas-blur`, so the smoke exercises native capture again.
- f054bbc: The native Command smoke bounds Maestro's settle wait on its two scrolls to the last result (1000 ms, authorized by the owner on 2026-10-11): the focused search field's caret never lets the screen settle on CI's slow screen checks. Both steps also take a 40 s limit (the owner's decision of the same day), since the iOS driver's fixed 3 s still-screen wait before each comparison leaves about 6.5 s per swipe on CI.
- ec4d2c8: The native materials smoke reads the focused editor above the software keyboard: with the keyboard up, the material switch's records and the editor do not fit on one phone screen, and Android's visible hierarchy leaves out what the keyboard covers, so the flow scrolls between them instead of expecting both at once.
- e846a25: The native Command smoke settles the results list at its end before selecting the last row: Maestro's "above" compares top edges and its visibility ignores the results pane's clipping, so it could stop with the last row mostly under the footer, where the selecting tap landed on the footer.
- bcdf6bd: The native materials smoke checks Android capture where it runs: in-page glass has no safe capture target and keeps its solid skin (the owner's P3a decision), so the flow expects idle capture on the page and reads the counters from an open menu, whose frost captures the page, in a glass parent and in a glass descendant of a solid parent.
- 2ee7fcb: The native candidate smoke reads only what an open anchored card leaves in the iOS accessibility tree: an open menu or palette is modal to VoiceOver, so iOS hides the page under it. The flow reaches the Drawer menu's trigger by its measured place (its tap there failed with "Element not found: Open menu"), and checks the palette's typed query in its own search field rather than in the page's record. No kit change.
- 4f050bb: The standalone starter moves to Expo 57.0.27 (React Native 0.86.3) and adopts the UIScene lifecycle through `expo-build-properties`, so a build linked against the iOS 27 SDK no longer traps at launch. Its Expo Modules JSI compiler patch is carried onto 57.1.1.

## 3.6.0

### Minor Changes

- 2bad617: Minor, for three user-visible capabilities (the owner's P3e and AO-6 decisions):

  - AnchoredOverlay gains a placement preference: `above` places the card above its trigger, and `besideStart` / `besideEnd` place it beside the trigger at its leading or trailing side, each flipping to the other side and then to above or below when the preferred side lacks room. `preferSide` keeps working as the old name of `besideEnd`, which now means the trailing side first in the layout direction. `useOverlaySide` reports the side a card took, `start` and `end` included, so a decoration aims the right way.
  - Popover's `top` now anchors the card above its trigger (it used to anchor below and only move the iOS beak). Either placement flips to the other side when its own lacks room, and the iOS beak follows the side the card took.
  - At the phone form factor a Popover that can close is presented as a sheet on the bottom edge of the window, over the app's tab bar and a dimming scrim, across the window's width (up to 640) with its content kept clear of the home indicator: Dark Factory's sheet on the web, the iOS sheet on iPhone and the Material 3 bottom sheet on Android. Under glass the sheet takes the dense layer, so its text keeps 4.5:1 over the dimmed page the frost samples and the covered tab bar does not read through it. AnchoredOverlay offers it through its new `sheetStyle`. Menus, Select and Autocomplete stay anchored; Command and PhoneInput's country list adopt the sheet in their own audit turns.

### Patch Changes

- 85298b7: The API changes of the AnchoredOverlay audit turn, all optional and backward compatible, on exports the public API manifest classifies as deprecated aliases (`AnchoredOverlay`, `AnchoredOverlayProps`, `placeOverlay`) or as internal by accident (`Entrance`, `EntranceProps`, `OverlayHost`). The placement preference and `sheetStyle` are the minor's (see the placement option changeset). Changes:

  - `AnchoredOverlayProps` gains `dismissOnTab` (the card is a menu or an option list: a Tab out of it closes it, and its rows are its one tab stop, so its scrollport takes none), `combobox` (the card is a combobox's popup: it is not modal to VoiceOver and TalkBack, and focus is never handed back to its field) and `returnFocusRef` (the control that opened the card, where `triggerRef` is a wrapper around it: the card's controls follow it in the tab order, and focus returns to it).
  - `onCardMount` no longer fires for a card that cannot close (`dismissable` false, a card pinned open): nothing moves focus into a card nobody opened.
  - `placeOverlay` takes `besideStart`, `besideEnd`, `visibleLeft` and `visibleRight`, and its result names the side a beside card took (`side`, `start` or `end`).
  - `Entrance` forwards a ref to its wrapper and takes `accessibilityViewIsModal` and `obscured`, the native screen-reader modality of an anchored card (both dropped on the web).
  - `OverlayHost` gains `holdModal`, `subscribeModal` and `topModal` (the modal cards open in a window layer) and `measureWindowBounds` (the window a sheet stands in: the layer's box cut at the keyboard's top, and its safe area). A window layer's `measureVisibleBounds` band now also keeps clear of the window's safe-area insets, taken from the layer's own box as it measures it.
  - On Android a window layer whose page has no capture host now wraps its page in a View of its own, laid out as the capture path's content split, which is the node TalkBack is kept out of while a modal card is open: the rendered tree gains that one View.

- 2bad617: `AnchoredOverlay`, `AnchoredOverlayProps`, `placeOverlay`, `useOverlayAnchor` and `useOverlaySide` are deprecated aliases (the owner's K12-2 decision): kit internals published by accident, each keeps working until a major, and its `@deprecated` text names what to use instead (`Popover` for a floating card, `Dropdown`, `Select` or `Autocomplete` for a menu or an option list). AnchoredOverlay's contract is stated in its JSDoc (what it renders, what it never renders, its defaults); its reference page comes with the generated `/foundation` page.
- 2bad617: AnchoredOverlay's placement follows the layout direction, keeps to the window's safe area and stops reading layout every frame (the AnchoredOverlay audit turn, AO-4, AO-5, AO-7, AO-10 and AO-11). Behaviour changes:

  - When a consumer does not say, a card follows the app's layout direction: in a right-to-left locale its leading edge is the physical right, on every consumer (only Dropdown passed it before). Output changes only in RTL locales: the patch's stated assumption. A fixed-width card aligned by its trailing edge in RTL is now clamped inside the window instead of extending past its left edge, and its physical insets go out under the style keys React Native resolves when it swaps left and right.
  - A window layer bounds its cards by the safe area on all four edges, its own box as it measures it at each placement less the insets `react-native-safe-area-context` reports when a SafeAreaProvider is mounted, so no card sits under the status bar, the Dynamic Island, the home indicator or a landscape sensor housing, and a card opened after the window is resized is placed and flipped against the window's new size.
  - Every anchored edge keeps 8 px inside its band, and a card with no fixed width is capped to the band less both insets, its trigger-derived minimum width included, so it never reaches the far edge.
  - An open card follows its trigger on events: the layout events of its frame and window, the keyboard, and the scrolls the kit hears (every scroll on the web, the kit's ScrollView and overlay scrollport natively). It reads layout every frame only while its trigger sits in a React Native ScrollView or list the kit cannot hear. A closed overlay holds no state and re-renders on no resize.
  - The kit's ScrollView publishes its scrolls to every child it holds, sticky headers and pages included, and still hands React Native its children one for one: a `pagingEnabled` scroller on the web keeps one snap cell per child, sticky headers keep their indices, and an empty slot adds no cell.

- 2bad617: Anchored cards keep their place in the keyboard order and are modal to screen readers (the AnchoredOverlay audit turn, AO-1 and AO-2, as the owner decided them). Behaviour changes:

  - On the web an open card's controls sit in the tab order right after the control that opened it (its trigger; PhoneInput's country segment, not the number field beside it). Tab from the card's last control goes on to the control after the trigger instead of the end of the document, Shift+Tab from its first returns to the trigger, and Tab from the trigger enters the card. A card opened from inside another (a Select in a Popover) sits in its parent's run, right after its own trigger there. Focus is never left on the page body with a card open.
  - Inside a modal (a Dialog, or any `aria-modal` container) that order is the modal's: Tab past its last control wraps to its first, as the modal's own focus trap does, so leaving a card whose trigger is the modal's last control never reaches the page behind it.
  - A focused node that is no tab stop itself moves by where it stands: in a card (the card's panel, a row the roving stop is not on) to the card's own stops on either side of it, and in the page to the stops around its place, an open card's controls standing at their trigger's, as the browser would in flow.
  - Menus and option lists (Dropdown, RowMenu, ButtonGroup's split menu, Select, the PhoneInput country list) close on a Tab out of them, and focus goes on in the page's order. Their rows are their one tab stop (the WAI-ARIA menu and listbox patterns): an overflowing list's scrollport is no longer a stop of its own, since a scroller is one only while it holds no focusable child. Popover and Command stay open while Tab moves through and past them.
  - Popover's panel is a dialog named by its title (by its trigger's label when it has none) and described by its supporting line.
  - A card presented as a phone sheet is modal on the web as well, as it dims and covers the page: keyboard focus stays in it (Tab and Shift+Tab wrap at its ends, a list opened inside it sits in its run, a Tab from the page behind it enters it, and a Dialog opened from it keeps its own order), and Popover's dialog says so with `aria-modal`. The anchored card stays non-modal on the web.
  - RowMenu, ButtonGroup's split menu, Select and the PhoneInput country list now move focus in when they open, as Dropdown already did: the first enabled row of a menu, the selected option of a list. The arrow keys, Home and End move among the rows, and Enter and Space activate one.
  - When a card closes, focus it held goes back to what opened it (the trigger, or the control that opened it programmatically). A combobox's field (Autocomplete) keeps focus and is never refocused, so its list does not reopen.
  - A card pinned open (a controlled `open` with no change handler, such as a docs example) is content on the page that nobody opened, so it takes no focus as it appears: Dropdown and Popover pinned open no longer move focus into themselves when the page loads, and neither do the other menus and lists.
  - On iOS and Android every anchored card that can close (menus, option lists, Popover, Command, the Calendar day peek) is modal to VoiceOver and TalkBack while open: the page behind it is hidden from them, a card opened from it covers it, their focus moves into it (the first row, the selected option, Popover's first line, the peek's heading) and returns to the trigger control when it closes. Autocomplete's suggestion list stays non-modal, so its field remains reachable while typing. A card pinned open is not modal.

- 92c73a2: VoiceOver and TalkBack now land where an open anchored card says they do (the AnchoredOverlay audit turn's device check of AO-2). Behaviour changes:

  - On Android, TalkBack moves into a menu, an option list, a popover, the command palette or the Calendar day peek when it opens, and back to the control that opened it when it closes, by Back, a selection, an outside tap or the escape gesture. The kit sent Android's `focus` accessibility event, which TalkBack takes as a report of input focus and ignores from a view that holds none, so nothing moved it: it seated its own focus on the first node of the window as a card opened (a section label above a menu's first item, a whole option list) and on the screen's header as it closed. It now sends `viewHoverEnter`, the event TalkBack's explore by touch moves on. VoiceOver keeps `focus`.
  - An option list opens scrolled to the row focus enters at, on every platform: Select's and the PhoneInput country list's selected option far down the list was focused out of view (on the web its scrollport stayed at the top), and TalkBack, which cannot move to a node off the screen, fell back to the whole list.
  - Command's search field takes the input focus when the palette opens on iOS and Android, a frame after it is revealed, with the screen reader moved to it. React Native runs a focus command ahead of the mount of the batch it arrives in, so the field was focused before it was in the window and kept no focus on Android.
  - A text field inside a card gives the input focus up natively as the card closes. Android handed the focus of a field removed while it held it to the first view of the window that takes focus by touch (a selectable text, another field), and TalkBack followed it there.
  - When a close hands the input focus to a field (PhoneInput's pick gives it to the number), VoiceOver and TalkBack follow it there rather than going back to the trigger, as the web's focus does.

- 9dc29a5: The component audit records the web after captures of the AnchoredOverlay foundation's turn refixes in `audit/turns/anchored-overlay.md`: for each, the variant run (2358 of 2358 cells) and the interaction-state run (1014 of 1014 cells, the 27 press states the before capture also left not reached) across the 18 components in its Capture through list, taken while the devices were busy, with the device runs to follow. Repository tooling and docs only; nothing in the package changes.
- 85fce80: The component audit records the web before capture of the AnchoredOverlay foundation's turn in `audit/turns/anchored-overlay.md`: the variant run (1746 of 1746 cells) and the interaction-state run (846 of 846 cells, 27 press states not reached) across the 13 components in its Capture through list, taken while the devices were busy, with the device runs to follow. Repository tooling and docs only; nothing in the package changes.
- 867c819: Record the owner's decisions for the AnchoredOverlay audit turn (Tab in menus, screen-reader modality, the phone sheet presentation, the RTL default and event-driven tracking) in `audit/DECISIONS.md`. Repository tooling and docs only; nothing in the package changes.
- a4ec33a: The component audit gains a `deferred` finding status, for a finding whose fix the approved plan schedules in a named later turn: it is not counted as open. Repository tooling and docs only; nothing in the package changes.

## 3.5.2

### Patch Changes

- a2d9022: Join the Foundations tier's checklists and turn capture with the F0 work: the checklist and status readers take the sign-off table from its one reader, the regenerated checklists carry both sides' facts, and LoopView's checklist records an open question for its turn (whether the Spinner, the indeterminate Progress and the InputOTP caret run on the loop primitive, as the instructions say, when only Skeleton renders through `LoopView`). Repository tooling and docs only; nothing in the package changes.
- e79409d: The component audit gains its Foundations tier and a one-command turn capture. Each of the 16 foundations (the style-layer renderables with no component page, AnchoredOverlay through ThemeProvider; the design tokens the `tokens/*` pages document, with the names those pages show and every other public name of their modules; and FoundationReference, the kit internals the generated `/foundation` page documents) has a checklist under `audit/foundation/`, generated and checked by `audit:checklists` and counted by `audit:status`: its source files (its implementation included: the private declarations its exports read), its seams (the private declarations it alone reads), its public exports with their API manifest kind and K12-2 status, the docs routes that document it or are planned to, the tests of it, and its consumers, every kit component that renders through it, read from the source by a new reader (`tools/audit/kit-graph.ts`) directly, through shared modules or through other kit components, with the Capture through list its turn re-captures. Every deprecation the owner decided is on one of those checklists, and `audit:checklists:check` fails on one that is not. `bun run audit:turn -- --slug=<slug> --phase=before|after` takes a component's, page's or foundation's capture in one command: the web export reused when it is this checkout's and built when it is not, served on the turn's own port, the variants, states and pages on the web, the installed Canvas Audit build on iOS and Android reused when it is this checkout's Release build and rebuilt incrementally when it is not, the native capture, then the analysis, sheets and index scoped to the slugs, with every run id recorded under its phase in `audit/turns/<id>.md`, which every checklist now links; `--dry-run`, `--web-only`, `--native-only` and `--only-first=<n>` for a large foundation. Every audit command reads a name the same way, a component's slug naming the component alone (`calendar`, whose template page is `template-calendar`); a web run that cannot tell whose source it captures is refused with exit 2, and the turn stops on a refused run whatever its exit status; and a run is recorded dirty only when the source it captured differs from its commit. Repository tooling and docs only; nothing in the package changes.
- 5c54622: Record a GridList finding for its audit turn: a pressable people tile nests its own action buttons inside the tile's button on the web. Repository tooling and docs only; nothing in the package changes.
- 10caa37: Join the Phase 3 test coverage, shared-defect and shape-table work with the audit tooling: TextInput's Disabled example gets a disabled state capture, the state tests follow GeoMap's moved focus stop and the primitive's new disabled signal, the regenerated docs and checklists match the merged sources, and the checklist findings the merge would have dropped (Badge, Card, MediaObject, Navbar) are restored, citing main's commits and the owner's recorded decisions. Repository tooling and docs only; nothing in the package changes.
- 484c6e4: Record the owner's decisions for the component audit in `audit/DECISIONS.md`: the public exports to deprecate, axis precedence normalization, stacked Row alignment, busy controls, Tooltip semantics, ChartFrame naming and the hand-off's scope. Repository tooling and docs only; nothing in the package changes.
- b5deb5a: The component audit's state coverage recognises a control whose only feedback for a state on a row is one the web runner cannot see: react-native-web drops `android_ripple`, so a control the source gives the ripple and nothing else that repaints a held press there (no style function reading `pressed` or `focused` that the build's skin reaches, no `onPressIn`, `onLongPress` or raw responder) shows the runner nothing on the Android row. The reader names that feedback on the control per build, evaluating the ripple's value and each style function's reads with every entry's skin; the coverage records such a press as judged on devices, never as a recipe's, and refuses a recipe applied on such a row on such a control. Dialog's and AlertDialog's press recipes are applied on the iOS row alone (their iOS capsules dim to the skin's pressed opacity), so the 12 Android cells that could only be not reached are no longer planned. The checklists' "Interaction states" row now says what the runner sets up (`recipes:`), since the facts are generated from the source with no capture, and lists under `not reachable on the web:` each state the source says the web can never show, with why: one no row of the page renders, one whose only feedback on a row is `android_ripple`, and a recipe whose field is disabled only through a TextInput's `editable`, which react-native-web renders read-only and never announces disabled (Input's and Textarea's Disabled; the recipe stays, since its cell records that finding). Repository tooling and docs only; nothing in the package changes.
- 48262be: The component audit's signal reader follows a disabled value through the helper whose return brings it: a control given `...textEntryState({ disabled, readOnly })`, or `{...accessibility.field}` of `stepperAccessibility({ ..., disabled: !!disabled })`, reads the helper's parameters (object-destructured ones included, in the same module or imported) as the arguments the call gives them, so its `disabledBy` names the component's `disabled` prop exactly as the same value written on the tag does. Input's, Textarea's, Autocomplete's, InputOTP's and PhoneInput's fields and Stepper's helper-given field and group were read as disabled by nothing in particular, which the state coverage took as a control no example asks for; their Disabled examples now ask for them on every channel (`editable`, `aria-disabled`, `accessibilityState`), Autocomplete's explicit `{ ...entry.accessibilityState, expanded }` and PhoneInput's country segment (`!editable` of the helper's return) included. A parameter the reader cannot bind (a render helper read where it is written, a call that spreads its arguments) is marked unread instead, and the coverage fails on it rather than read the control as one no example asks for. Repository tooling and docs only; nothing in the package changes.
- 0289e91: The component audit captures interaction states and pages, and analyzes, sheets and indexes every capture. `bun run audit:web -- --states` hovers, focuses (with the Tab key), presses, opens, and finds invalid and disabled each component the interaction registry lists, as far as it has those states, through one recipe table that names each state's example (a state a component shows in more than one place has a recipe for each, its cells named for their examples: Dropdown's disabled trigger, and the disabled item inside its menu), verifies the state from the page before photographing it (the hover's lift or wash, `:focus-visible` and the ring in the pixels on every side, the pressed style against the hovered one, a chart's value flag or readout under a press, the opened panel and where it painted, `aria-invalid`, `aria-disabled` and whether the disabled control is still a tab stop, on the component's own surface or inside the overlay its example opens: ActionSheet's disabled action, RowMenu's and Dropdown's disabled item and AlertDialog's confirm, disabled until its token is typed, each photographed in the viewport with the overlay probed beside the row, as is the overlay a hover, a focus or a press inside one is in), records a state it cannot confirm as not reached instead of photographing it, and measures each release (a press that should have been cancelled, an inspection or an overlay that does not clear become flags, named with what they mean in typed tables; a press is measured on the control pinned when it went down, so one that takes its control out of the page, as a dialog's Cancel does, is read rather than waited for, and a click that reaches the control as the button comes up after the pointer moved off counts as the press firing, since react-native-web runs `onPress` from that click, even when its handler changes nothing to see); overlays open from the web row and from the iOS and Android rows whose builds the docs inject, at every width (a drawer a component becomes only at and below a breakpoint, at those widths). The table is held to each component's own source, read statically: every press, scrub, resting-pointer handler, tab stop the web makes (every Pressable, a control role, `focusable`) and overlay it renders or opens, whether written on the element or spread onto it from a value the reader can follow, needs a recipe or an exemption whose claim is checked against that source and the page; a spread it cannot follow gives nothing. Every control the source disables (`disabled`, `aria-disabled`, an `accessibilityState`'s `disabled`, or `disabled` handed to another kit component) is read with what disables it (the props its value is true with, through local constants, and the keys of the item data it reads) and the overlay it renders inside (through a constant used there and a local component that portals its children); one an overlay is kept closed by whenever it is disabled (a Select's option rows) gives nothing. Where a rail example asks for a disabled control, by passing what disables it, the place it is in (the component's own surface, or an overlay it opens) needs a disabled recipe there, a recipe inside an overlay opening it first; a place no example asks for, and a control the component disables by itself, need nothing. Each overlay a component opens is answered one by one: each one its source renders, or hands its open state to (AvatarMenu's Dropdown, a part its platform entries inject, included), needs a recipe, and a component whose source renders more than one (the Calendar's hover card and day peek) has each recipe that opens one name it, so one overlay's recipe cannot stand in for another's. A hover, a focus and a press are answered place by place too, on the component's own surface and inside each overlay its source renders them in (Dropdown's, Select's, PhoneInput's and RowMenu's rows, ButtonGroup's split menu, Command's palette, the Calendar's day peek, the provider's toast, FilterPanel's rows inside the drawer it becomes at a phone's width), and only through a control of the component's own: every recipe names its control (the ARIA role its target is found by, and the function that renders it where the role does not single it out), which must be one the source renders there in the recipe's example, with that role, whose function takes the state, and the capture holds the element the state lands on to that role. A recipe on a control another kit component renders for the component answers nothing (FilterPanel's focus was its header's Clear and now is its option rows; Dropdown's focus and press were its default trigger, a kit Button, and now are its Custom trigger example's button and its menu rows; DescriptionList's were its Update link and now its focus is the inline-edit field the link swaps in, its source giving it no press; AvatarMenu's hover is now its pill; GeoMap's focus is now the zoomable map and its press a press-to-inspect on a bubble; ActionPanel, Field, Form's focus, GridList and DashboardGrid have no such control of their own and say why). A place is also answered row by row on the docs' three-up: each signal says which platform builds render it, read by evaluating the skin conditions around it with each entry's skin (Dialog's iOS capsules, its Android text buttons, the web build's kit Buttons), each row shows one build (the one the docs registry injects, or the web build), and a recipe answers only on the rows it is applied on, through a control of the component's own on that row, so Dialog's and AlertDialog's focus and press on the web row's Cancel, a kit Button, answer nothing and are captured on the iOS and Android rows instead; a control no row of the page renders is recorded as judged on devices, never as captured. A component the component's own factory builds and renders as a tag is read where it is used, so Sidebar's drill-down rows are the Drawer's, with recipes of their own inside it at a phone's and a tablet's width. Each signal on an element names it (its tag, the function it is rendered in, the frame it sits in, and the roles the page gives it as react-native-web maps them) and is placed where it renders, a control on the surface and in a drawer in both; a kit component's content is inside its overlay only when its source renders `children` there (a Drawer's, not a Dropdown's); the reader also follows a local constant's value, `children` given between tags, the props a look's input is read under, a render helper's literal argument at each call, a prop given on one side of a condition, a web `Platform.select` tab index and a TextInput's `editable`. A state a component shows on its surface and inside the overlay the same example opens is named `<state>-<variant>-inside` for the second. A raw primitive's rail examples are read the same way, spreads included. Each checklist's facts gain an "Interaction states" row, which says when a capture is applied inside the overlay it opens and where the source disables controls no example asks for. `bun run audit:web -- --pages` photographs every pattern and template page's first screen and each of its sections, and probes them; a section, like a state's row, is photographed with the margin its own paint needs (how far past its box the furthest shadow, outline or transform-moved box of it or anything in it reaches, cut to the clips inside it), so a card's drop shadow is not cropped, and the margin, what set each side and any side the page itself cuts are in the probe; audit pages hold infinite loops still. Every probed region records where its boxes sit in the viewport and every text the opacity that dims its ink apart from the groups it shares with its backdrop, so the analysis can find each text in its photograph. `bun run audit:analyze` reads variant cards, state shots and page sections the same way: contrast from the DOM where it resolved, and otherwise the text's painted ink over the background read from the photograph (with the colour behind a dimmed group read around it), fail-likely under 4.0 of 4.5 and review up to 4.5; type floors, small targets per region, the tree held identical across looks and surfaces, a state's own and release flags, and a state not reached with its reason. `bun run audit:calibrate` measures the photograph readings against the DOM over every family. `bun run audit:sheets` lays out per variant the card, row, device and comparison sheets, per component its states (desktop first, each tile its photograph with its flags) and per page its first screen across widths and looks, every sheet within 1600 px and cut into numbered sheets where a grid would be unreadable. `bun run audit:index` builds `.audit/current/` from the newest capture of each cell across every run, with an index per component (its cells, its states table) and per page (its cells, its sections linked one by one), and a summary ranked by flags with every state not reached and every release flag (a state cell of a recipe the table no longer has is left out with a warning, and out of the sheets); `bun run audit:prune` deletes old runs while keeping the newest two captures of every cell, then rebuilds the index so none of its links points at a deleted run. Nothing in the package changes. In the docs, the pattern and template pages wrap each section in one more View, which carries its `data-mockup-section` key on the web for the page capture; the pages look as they did.
- b4e4c48: Record what every axis resolver in the kit returns, for no member, each member alone and each pair (`test/fixtures/axes.json`: 118 resolvers across 75 component pages, the measure axis (which also reaches Field and Form) and ThemeProvider), as the baseline each component's move to axis tables is proven against; `bun run axes:record` rewrites it for a change meant to move a precedence. To reach the resolvers, the component shells export them to the test, and the axes that were resolved inline move into named functions with the same logic: AvatarMenu's menu edge, Skeleton's line length, Swatch's stretch, Tooltip's trigger, DescriptionList's value form, StackedList's row badge, Stats' delta, DataTable's column alignment, Sidebar's drawer edge, Tabs' wrap and ThemeProvider's scheme and surface. The package's exports and every component's output are unchanged.
- 9dd237e: Add the kit's internal axis primitive (`src/style/axis.ts`): a style axis is declared as data, an ordered list of boolean members with the value each resolves to and a fallback, and `pick` resolves the highest-precedence member a component's props set, the way the measure axis already reads the width scale's declaration order. The primitive loads nothing but the tokens, so the docs generator can read axis tables without React Native. The package does not export it, and no prop or behaviour changes; the first resolver to go through it is the measure axis, in this same release.
- b5b483f: A Carousel with a `defaultIndex` (or a controlled `index`) past the first slide now shows that slide on Android when it first measures, instead of the first slide under a page indicator that names the right one. Android's scroll view capped the opening scroll at the content's unmeasured width, and the Carousel counted it as done; it now issues the scroll again once the content size reports every slide's width.
- 3028e1b: The catalogue-menu check clicks the page beside the Playground stage, in the gutter level with the trigger, rather than the page's heading, which a 390 px window scrolls out of view once a component page carries its overview. Repository tooling only; nothing in the package changes.
- 278270a: Every corner in the kit is now a role of the row of the platform its skin draws, for a role its component plays, the pill (9999), square, or concentric with its container. Corners that sat off their platform's row, or on a role the element does not play, move to the role it plays. Rendered changes:

  - Web: the Autocomplete, Select and PhoneInput option rows 10 to 8 (a menu row, as Dropdown's; PhoneInput's country list takes Select's rows); the StackedList row menu button 10 to 8; the Stepper group 6 to 10 (a field box); the DataTable inline edit field 8 to 10; the Command search trigger 6 to 10 on every platform (a field); the Tooltip icon trigger 6 to 8 and the text trigger's press layer 4 to 6 on every platform; the CodeBlock inline code box 4 to 6 on every platform (the inline code corner Typography's `code` already draws); Emblem and Swatch at the tile corner, 12, at every size on every platform (small was 10, large 14); the SplitButton menu's pressed row 2 to 8 (and square on iOS and Android, as their menus' rows); the DragDrop drop ring 10 to 14 on every platform; the Dialog and AlertDialog overlay scrim square on every platform (it was 18 on the web, 8 on iOS and Android).
  - iOS: the ButtonGroup spaced peers and the stepper kind's outer ends 8 to a capsule, as the iOS Button; the SplitButton menu 12 to 26, the iOS menu corner; the Calendar peek card 20 to 26; the Command palette 16 to 12, the web's corner (iOS has no command palette); the contained Dialog and AlertDialog backdrop 8 to 28, so the dialog inside it is no rounder than its container; the Dialog's capsule buttons 22 to a full capsule; the DataTable outline, MediaObject surface, Navbar bordered and floating surface and the bordered Sidebar column 10 to 12, the iOS card corner; the DataTable inline edit field 6 to 8; the GridList thumbnail 10 to 12; the MediaObject compact icon box 7 to 6, the web's; the StackedList row menu button 6 to a circle; the RowMenu trigger 8 to a circle (it paints no fill).
  - Android: the Command palette 8 to 12, the web's corner; the contained Dialog and AlertDialog backdrop 8 to 28; the DataTable outline 8 to 12; the snackbar (Toast) action button 10 to the Material 3 full round.

  Corners that only change their spelling render as before: capsules and circles written as half a size (the iOS Slider, Switch thumb, Stepper and Progress, the Drawer and ActionSheet handles, the Sidebar active dot, the Android TabBar indicator, the InputOTP caret, the snackbar's dismiss button and the Android Dialog, DataTable, DragDrop and RowMenu buttons) are now 9999, and 999 is 9999. The CandlestickChart body, the DataTable header band and the DragDrop ghost keep their corners as roles of the web's own row.

- 55646b8: The component page gate learns the Accessibility section (rule S10): once a component's audit checklist (`audit/components/<slug>.md`) signs a platform off, with a run id in its sign-off table, `docs:gen` refuses that component's page unless it carries a `## Accessibility` section after Do & Don't, which renders as one of its guidance sections. No component is signed off yet, so no page needs one today; the rule is proven on a copy of a real checklist with a sign-off typed in. The sign-off table now has one reader (`tools/audit/sign-off.ts`) that audit:status, the checklist gate and the docs gate share. Repository tooling and docs only; nothing in the package changes.
- fc947c7: A component's description is now written in one place: the first paragraph of its `.md` intro. `docs:gen` generates every description into `docs/src/core/descriptions.ts`, the component page's lead and the search index read it there (the lead with its code spans in the mono face, a search result as plain text), and the docs registry entries carry no description of their own. The two texts matched on only 24 of the 104 pages; an editorial pass over the other 80 kept the truer wording, rewriting 53 intros so the first paragraph is a short summary and the detail moves into the overview, with no line of an intro lost: Select is no longer described as a native select (it draws its own trigger and panel), Card names the stat, section and generic uses rather than `StatCard` and `SectionCard` exports that do not exist, BulletChart drops the claim that every row shares one scale, Icon drops "inherits currentColor", Drawer's intro opens with its description, and the Field description loses its em-dash. Repository tooling and docs only; nothing in the package changes.
- 48fde0f: Component pages now show every line of their `.md`. The docs generator reads each page into one document model (`parseDoc`), and the page renders all of it: the intro after its first paragraph as an overview under the lead, the prose written beside an example as its note under the Playground rail while it is selected (and the prose before the first variant as a note on every example), Do and Don't captions whole with their code spans in the mono face and their strong runs at semibold, and the guidance sections after Do & Don't with their fences as live examples (Button's "Real links (href)" and "Touch area", Chip's "Touch area", Typography's "Inline links (href)"). A bullet list in that prose is a real list, `ul` of `li` on the web, as the theming page's bullets now are too. The page gate gains two rules: S7 (a section of the page's own goes after Do & Don't, holds something, holds no Do/Don't pair, and takes a name of its own) and S8 (every non-blank line reaches the page as written, so a line the model drops, or a link, emphasis, ordered list, table or deep heading the page cannot render, fails `docs:gen`), and S2 now requires the intro to open with a paragraph. The e2e routes and the audit inventory read the same examples as before, through `splitDoc`, now a projection of the model. Repository tooling and docs only; nothing in the package changes.
- bd1fadf: The docs generator and the component page gate now read a page through one model, so they can no longer disagree: before, a heading spelled `##  Variants` or `##\tDo & Don't` passed the gate while the generator dropped the whole section. The gate also rejects such loose heading spellings (an extra space, a tab, an indent, a closing `#` run) under the rule that owns the heading, rejects prose in Do & Don't that the page never shows, and locates a registered component with no page at its entry's line in the docs registry. A caption that wraps onto the lines below its marker is now read whole, so the Grid, Field and Carousel Do & Don't captions that were cut off mid-sentence show in full, and a caption that opens on a quote or a parenthesis keeps it. Code spans in Do & Don't captions and titles render in the mono face instead of showing their backticks, and DepthChart's one-sided Don't caption describes what the chart draws (the bids stretch across the price axis and the Asks legend entry labels nothing). Repository tooling and docs only; nothing in the package changes.
- cd674f6: The component page gate now fails every `##` that would silently end Do & Don't: a `##` with no name, and a section of the page's own that holds Do/Don't markers (a pair's `###` title typed as `##`). Both passed before while the generator dropped that pair and every pair after it. A Do/Don't marker in Usage or Variants fails too, since only Do & Don't renders pairs. DepthChart's one-sided Don't caption now says the price axis covers only the bids, which is what the chart draws. Repository tooling and docs only; nothing in the package changes.
- 071399b: The docs generator now refuses a component page that is not shaped the way it reads one: `docs:gen` and `docs:gen:check` check every `src/*/*/<dir>.md` against located, numbered rules (the registry name as its title, a prose intro, `## Usage`, `## Variants` and `## Do & Don't` each once and in that order, one fence per example, every Do & Don't pair under a `###` title with a caption and its own fence, and a Don't that differs from its Do), and `bun run test` holds the pages and the docs registry to the same rules. Do & Don't now ends at the next `##`, so a section of a page's own after it is never read into the last pair. Every page passes: Image, Pressable, ScrollView, TextInput, Video, FilterPanel, AreaChart, CandlestickChart, DepthChart and ScatterPlot gain Do & Don't lessons of their own, Popover gains Inline, Description only and Controlled variants, Sparkline's two Variants sections are one, Container's "Naming a measure" Don't shows the hand-capped View it warns against instead of repeating its Do, and the untitled pairs (and Switch's second pair under "On") have titles. Repository tooling and docs only; nothing in the package changes.
- f3bbe14: On Android, an open Drawer (and so the Sidebar's drawer and the FilterPanel's drawer) no longer names its panel and its whole window after the text inside it. The Drawer wrapped its panel in two press areas, a scrim that closed it and a no-op that swallowed stray taps, and React Native's Pressable always hands its view an accessibility state, which Android's accessibility delegate answers by building a content description from the view's non-focusable text: a footer with a "Palette" label over a segmented group named "Palette" made the drawer and the window announce "Palette, Palette". The tap-to-close target is now an empty full-bleed Pressable beneath the panel, a sibling and never its wrapper (as ActionSheet's is), and the panel sits in plain box-none layers, so a tap off the panel, beside a side drawer or a capped sheet, still closes it and a tap on the panel stays in it. A model of the Android naming rule pins every drawer, sheet and dialog in the kit: none names a view that cannot take focus after a footer's text.
- f78d3ec: Make the F0 records exact: the axis changesets count the measure axis's pages and agree with each other, the platform catalog changeset names the two shell comments it changes in the package, the repo instructions say what an axis left out of a Preview link's look query does on a cold launch and on a warm one, and `audit/DECISIONS.md` records the palette control's outcome (native rows added, the desktop Topbar left link-only for want of 8 px). Repository tooling and docs only; nothing in the package changes.
- 1f217fb: A GlassPane now renders nothing wherever its material resolves solid (an Android surface in the page, which has no capture plane it may sample; a missing optional peer; a browser without a backdrop filter), reading the same resolution as the component it sits in, so Android glass Chips, Badges and Kbds lose the black 1 dp ring and no pane draws a second hairline inside its host; the Video control bar keeps its fill there. A glass surface keeps a border that shows a state (the Slider's press halo) over its material. Disabled Input, Textarea, PhoneInput, Autocomplete and InputOTP fields now reach the browser as disabled (the disabled attribute and aria-disabled, out of the tab order) and report disabled to VoiceOver and TalkBack, while read-only fields stay focusable on the web. The TextInput primitive's keyboard focus ring takes the theme's ring colour, like every kit Pressable, and Slider and GeoMap show the kit's 2 px ring on keyboard focus in the ring colour, visible under glass.
- 8ef53a8: The docs app's iOS header menu now check-marks the palette in force beside the current page. expo-router hands every menu to iOS as single-selection unless told otherwise, and UIKit applies a single-selection menu's rule to its whole tree, inline sections included, so the menu's root made the current page and the palette one group and only the page's check survived. The root is now multiselectable, which leaves each section (a component category, the Palette section) a single-selection group of its own. The menu is built in a module of its own, and a unit test follows it through expo-router's header config and react-native-screens' bar button config and holds it to UIKit's single-selection rule. Repository tooling and docs only; nothing in the package changes.
- 9fcc06b: The keyboard ring sweep (every keyboard stop on every component page shows the kit's ring) runs each look as a test of its own and reads each stop's cue in one round trip from the page, so the Calendar page, whose every day is a keyboard stop, no longer runs past the test's minute on the CI runner. The assertions are unchanged. Repository tooling only; nothing in the package changes.
- 9be1e83: A link Text now carries the kit's focus ring, like Pressable, TextInput and ScrollView: the Text primitive gives a Text with an `href` (which react-native-web renders as an `<a>`) or the `link` role the palette's `ring` colour 2 px off the text, so a Typography `href` link shows the kit's ring on keyboard focus instead of the browser's own colour. Plain text is untouched.
- 0810edb: A Pressable with the `link` role presses on Enter on the web, so Breadcrumb's crumbs and home link and Navbar's links work from the keyboard: react-native-web leaves a link's Enter to the browser, which activates only a link with an `href`, so these links ignored the key. Space still does nothing on a link, a held Enter presses once, and a link given an `href` keeps the browser's own activation. The docs prop tables read a `//` comment that wraps over several lines whole, where they kept only its last line (ActionPanel, Button, Card, Chip, Dialog, Tabs and eight more had rows that began mid-sentence), and the audit's platform-entry facts name what an entry passes on unbuilt (BadgeGroup, GridItem, the Card parts).
- 842da8d: Resolve the measure axis (`stepOf`, which every measure step on Autocomplete, Button, ButtonGroup, Container, Field, Form, Input, Listbox, PhoneInput, Progress, Select, Slider and Textarea goes through) with `pick` over its declared table, the first resolver on an axis table. The recorded characterization is unchanged: the narrowest step still wins. No prop or behaviour changes.
- 7625dfa: Image and CardMedia announce their `alt` text on the web: react-native-web dropped it, leaving the picture unnamed; an unnamed Image stays decorative. A labelled BadgeGroup is a named group, a vertical Divider says it is vertical, and a labelled Divider is named by its label. Breadcrumb's `maxItems` JSDoc now says it keeps the last `maxItems - 1` crumbs, as it always has. The test suite also mounts every platform-entry export in the skins smoke test, tests MediaObject and ActionPanel on every platform entry, owns QRCode, DepthChart and five charts' dev warnings, and the audit facts credit RevealGroup.
- 2648b56: The docs app on iOS and Android gains the Blush/Mint palette choice, in the light scheme only: a Palette section of check-marked rows in the iOS header menu, and a Palette row of the kit's segmented ButtonGroup in the Android menu drawer's footer. The desktop web Topbar stays as it is, because at the desktop cut the longest page title leaves 116 px and the control needs 124. The docs Sidebar adapter now takes its drawer footer from the shell that opens it instead of deciding by platform, and unit tests pin the control's choices, its absence in the dark scheme and the iOS section's check marks. Repository tooling and docs only; nothing in the package changes.
- ef133d0: The ScrollView primitive now takes the theme's ring colour for the keyboard focus ring the browser draws on a scroller with nothing focusable inside it, like the kit's Pressable and TextInput. A zoomable GeoMap draws its ring around the whole chart on keyboard focus, so it no longer runs under the zoom buttons. An errored Input, Textarea or PhoneInput, whose red edge shows the error rather than focus, now shows the kit's 2 px ring around that edge on keyboard focus instead of no focus cue at all. The Android Textarea's focused indicator and the Android InputOTP's active cell use the `ring` colour, like every other field. Under glass, a field's focus or error border, an open trigger's border and an active cell's border stay over the material, so the Android fields' focused indicator no longer disappears there. A Video with the platform's own controls gives its surface the theme's ring on the web.
- 4861c55: A named Image (an Avatar photo, a CardMedia cover, a MediaObject photo) is a single image to assistive tech on the web, named on react-native-web's own hidden `<img>`, and on iOS and Android it takes the image role, so VoiceOver and TalkBack announce it as an image. CardMedia names its cover in Image's order: when both `alt` and `accessibilityLabel` are set, `accessibilityLabel` wins, as it does on Image and in React Native.
- 7df7acf: The open-overlay screenshots open every overlay with the Playground stage at the top of the page's scrollport, so the prose a component page carries above its stage no longer decides which side of its field a Select or Autocomplete list opens on in the picture. Repository tooling only; nothing in the package changes.
- 0b5f9a1: `PLATFORM-REFERENCES.md` now has a row for every docs component, the primitives and layout components included (View, Text, TextInput, ScrollView, Pressable, Row and Column, Container, Grid, Reveal, Image, Video, QRCode, Swatch, Field, PhoneInput, Board, Drag and drop, DashboardGrid), and `test/platform-references.test.ts` holds the table to the code. The Treatment legend is redefined: Shared means the component's own skin is the web skin on every platform, while an injected platform part may still differ. Badge's row no longer cites the iOS and Material 3 badges (count dots on icons, a different job), and the Listbox and Skeleton Android cells say Material 3 ships no such component; Badge, Pagination, Alert, Emblem and Avatar are retagged Shared and Listbox Light. The guard fails a native skin of a component's own under a none note unless it is a recorded one-job substitute or names the audit turn that aliases it, a cited control where the component draws the web skin unless the reason no native shape is owed is recorded, a Treatment the code contradicts, and a list entry the code has moved past. The shape gate's shared-part declarations for Board and Drag and drop, which stood in for their missing rows, are gone: the rows' none notes cover them. The Emblem and Swatch shell comments, which still called them Light, now say Shared. No skin changes, and the audit checklists name each component's native shape to match. In the package itself only the Emblem and Swatch shell comments change; no code, type or behaviour does.
- 75836a7: One reader for the platform reference catalog: `tools/skins/references.ts` reads `PLATFORM-REFERENCES.md` and maps a docs component to its row (the aliased Stepper, Emblem and Drawer rows and the one charts row) for the audit facts, the shape gate and the docs coverage report alike. The coverage report used to strip names with a parser of its own, with no aliases and no charts row, so it listed 48 components without a row where 18 had none; it now reports the 18, and `--strict` fails on a component without a row. A test fails any module that reads the catalog without going through the reader. Repository tooling and docs only; nothing in the package changes.
- d41a812: Preview links may carry an optional look query (`scheme`, `surface`, `palette`), documented in the repo instructions beside the route-only default. The docs app's launch decisions move into pure helpers in `docs/src/theme/theme-links.ts` (`launchRequest`, `firstLook`, `withOverrides`, `EXPORTED_LOOK`), and unit tests now pin what a deep link paints: a cold iOS or Android launch starts in the link's look on its first render, the web hydrates in the exported look and lands in the link's look after, the launch URL wins an axis the router params also name, and a warm link moves a running app axis by axis while ordinary navigation never does. Repository tooling and docs only; nothing in the package changes.
- 2a519a8: The style foundation is now published through an explicit list, `src/style/public.ts`, instead of the kit's internal style hub, so a helper the kit adds for its own skins no longer becomes public by accident. The package exports the same 532 names with the same value and type bindings on the web, iOS and Android (a committed snapshot, `test/fixtures/public-api.json`, holds them). Every public component, part, hook, token and utility now carries a JSDoc summary in the published types; the web entries of Breadcrumb, the drag-and-drop parts and the Toast system export each member on its own so their summaries reach the declarations (the same objects as before). `bun run check:api` now also fails on a public value without a summary, stages every export with no docs page in `PENDING_DOCS` with the page planned for it (and fails once that page names it, or once the foundation reference exists while names are still staged), holds the materials manifest to the same docs routes (`check:materials` fails on a public foundation renderable with no route), counts a name that is an ordinary word only where a page shows it as code, compares each export's facts (value, component, callable, deprecated) across the iOS and Android resolutions, and ignores files npm never packs. No export is added, removed, renamed or deprecated.
- 0db7b8d: The kit's public surface is now an explicit, checked list: `tools/api/manifest.ts` classifies every export of `@nannier/canvas` (component, part, hook, token, utility, type, deprecated alias, or internal by accident) and every file under `@nannier/canvas/styles/*`, each with the docs page that names it or the page planned for it. `bun run check:api`, run by the pre-push hook and CI, fails when an export or a file is added or removed without an entry, when the iOS or Android resolution of the native entry exports a different set from the web build, when a kind disagrees with the source, or when a named docs route does not exist or never names the export. No export is added, removed, renamed or deprecated. Repository tooling and docs only; nothing in the package changes.
- 7cce601: The shape table is the one source for a corner that plays a role. `shape.ios.checkbox` changes from 5 to 9999: the iOS checkbox has always drawn the edit-mode selection circle, and no surface ever drew 5; the iOS checkbox renders the same circle as before. DESIGN.md's Shapes section now lists every role per platform, the roles only one platform draws, and the components that draw each role.

  The CSS hand-off (`styles/tokens/platforms.css`) follows the skins:

  - iOS `--p-check-radius` 11px to 9999px (the same circle on the 22px box).
  - `--p-overlay-radius` and `--p-sheet-radius` from a stale 12px to the dialog and sheet corners: 28px and 38px on iOS, 28px and 28px on Android.
  - The rendered changes listed in the corner changeset: `--p-select-row-radius` and `--p-ac-row-radius` 10px to 8px, `--p-stepper-group-radius` 6px to 10px, `--p-emblem-radius-small` and `--p-swatch-radius-small` 10px to 12px, `--p-emblem-radius-large` and `--p-swatch-radius-large` 14px to 12px; on iOS `--p-table-radius`, `--p-media-radius` and `--p-grid-gallery-radius` 10px to 12px, `--p-cmd-radius` 16px to 12px, `--p-alert-btn-radius` 22px to 9999px; on Android `--p-cmd-radius` 8px to 12px and `--p-table-radius` 8px to 12px.
  - Corner tokens no check read before, now held to the skins: the web `--p-alert-btn-radius` (Dialog) and `--p-ad-btn-radius` (AlertDialog) 12px to 9999px, the pill of the kit Buttons the web footers render (a web surface painting its dialog buttons from the hand-off now draws pills, as the kit does); Android `--p-sheet-cancel-radius` now declared 0px (it inherited the web's 18px for a cancel card Android does not draw: its Cancel is the sheet's last row); the web `--p-radio-section-radius` now declared 0px (only iOS draws a grouped section around its choices); Android `--p-slider-thumb-radius` 2px to 9999px, the same capsule on the 4px handle bar. DESIGN.md's component recipes list these tokens and `--p-toast-radius-multiline`, the slider track and inner corners and `--p-stepper-group-radius`, whose values were already the skins'.
  - Equivalent spellings of the same capsule: the web `--p-slider-track-radius`, `--p-slider-inner-radius` and `--p-slider-thumb-radius` 999px to 9999px; iOS `--p-ad-btn-radius` 999px to 9999px; Android `--p-alert-btn-radius` and `--p-ad-btn-radius` 20px to 9999px and `--p-drag-handle-radius` 20px to 9999px (each on a box twice that tall, so the same capsule or circle).

- 278270a: Comments in the skins and the CSS hand-off no longer name the brand Canvas replaced, call the violet `primary` indigo, or call a skin "the established Canvas look", and every corner a comment states is one the code it sits on draws: a comment over an element states that element's corner, and a module's header comment names roles (the field corner, the iOS menu corner, a capsule) rather than numbers. The hand-off's MediaObject, GridList and Autocomplete notes said 10pt where the iOS skins draw 12 and 8. Source comments that stated a corner their element does not draw now state the one it does: the web Autocomplete and Select option rows (said 10, draw the control corner, 8), the iOS and Android DataTable outlines (said 10 and 8, draw the card corner, 12), the iOS ButtonGroup stepper (said rounded 8, draws capsule ends), the iOS RowMenu card (said about 28pt, draws the iOS menu corner, 26), the iOS StackedList overflow button (said the web's control corner, draws a circle), the iOS Feeds card (said the grouped-list corner, draws the iOS card corner), the iOS Card note (called its 12 corner larger than the web's 14), and the Select shell note (said no option row has a corner, where only the Android rows have none). A comment over several elements states a number only every one of them draws, so the web DataTable skin's "10px-cornered" header note now sits on the header band, and the iOS and Android GridList entry notes name the Card's corner role rather than a number over the whole build. DESIGN.md's Shapes section now says when a native skin may draw a part the web's way: only by sharing the web skin's same part (drawn from the same place), and only where the platform gives that part no shape, because the component's row in PLATFORM-REFERENCES.md says the platform ships no control for the job or the part is declared with the reason; that a constant, helper or component no platform names, in any module, is drawn by every skin that uses it; and that a shell's own code is drawn by every platform only when every platform's entry builds with it, while a helper only some entries build with (one that adjusts the iOS skin before the iOS entry hands it on) is drawn by those entries' platforms. These are comment and documentation changes only; nothing they describe renders differently.
- 47e269f: The skin divergence reader (`tools/skins/divergence.ts`) now says what each reason a platform build differs from the web build is made of: a skin or data of the component's own, another component's platform build injected as a part, another component's skin or data handed to that component's own factory, or a form it cannot read. Data an entry hands another component's factory is that part's look: AvatarMenu's 6 gap on the native Dropdown no longer reads as a look of Avatar's own. The reasons it spells out, and so `check:skins`, the shells gate and the audit facts, are unchanged. Repository tooling and docs only; nothing in the package changes.

## 3.5.1

### Patch Changes

- 7ddc216: The a11y structure check of the Typography page names the heading each outline lookup expects, so a missing "Props" or "Do & Don’t" section h2 says which one is missing instead of two identical "Expected: 2, Received: undefined" failures. Repository tooling only; nothing in the package changes.
- 96173ce: The audit checklists' generated facts now hold for every component and page, or the generator fails with the file and line of the code it cannot read. A static reader (`tools/audit/static-eval.ts`) reads table-driven test imports row by row, so the skins smoke test credits each component of its CASES table; binds a helper's parameters at every call site, so `e2e/behavior/text-entry-clear.e2e.ts` names the routes its `entry(page, recipe)` builds and `test/touch-target-clips.test.tsx` is credited with each component its `entry(path, name)` loads; drops the rows a guard skips; reads a kit namespace through casts and `spyOn`; follows the files a test lists from the checkout (`tools/audit/hosts.ts`); and never reads a `const` the module changes as its literal. Every dynamic import and every navigation must resolve, and every route a spec reaches must be named or swept, so nothing is silently missed. The e2e catalog sweeps (every spec that loops over `componentRoutes()`, `contentRoutes()`, `allRoutes()`, `componentExamples()`, `MATERIAL_ROUTES` or the overlay recipes, in a `for...of`, a `for...in`, an indexed `for` or a callback, and navigates to each row) are credited per route in a row of their own. Tests are the files `bun test` runs, credited with the fixtures they use; the starter's own e2e suite and the audit's runner are not the docs suite. A page's facts name the kit components its own entry uses, and a finding's Cell must be one of its checklist's capture ids or `source`. The touch-target fact names the kit's whole touch-target vocabulary and the coverage test's record of each pressable, the raw primitives' facts read the modules that build them (or say React Native's own component is re-exported), the skin divergence reader counts any platform-entry export form it cannot classify, and any object or literal an entry writes that the web entry does not match, as the platform's own, the shells gate follows barrel re-exports to the module that builds each name, and a `fixed` finding needs its fix commit. Repository tooling and docs only; nothing in the package changes.
- 04eae81: The component audit can photograph the docs on the iOS simulator and the Android emulator: an in-app capture driver built only into a separate "Canvas Audit" app (its own bundle identifier, package and URL scheme, so it installs beside the docs development app the Preview links open), the host that drives it over loopback HTTP and cuts each Playground card from the screen in every look and surface (`bun run audit:native`), the build that makes and installs it (`bun run audit:native:build`), and a test that keeps the driver out of every ordinary bundle. The host checks every segment's resolved look, route and example against the item before photographing it, counts only the driver's time against an item's timeout, turns away another app's hello without ending the run, and abandons a platform whose app stays silent through three relaunches so an unattended sweep finishes. The build parks its native projects outside `docs/` and puts the docs app's own `docs/ios` and `docs/android` back untouched, and stamps a native fingerprint (app config, config plugins, native dependencies, local native modules) that the host checks beside the source fingerprint, so a build whose native project is stale is refused. Repository tooling and docs only; nothing in the package changes.
- aecbc2e: The component audit gains its web capture runner: `bun run audit:web` photographs every example variant of every component page at phone, tablet and desktop width in the blush, mint and dark looks, solid and glass, and probes each cell into `.audit/runs/`: text at the size it paints (a transform-floated label at its scaled size, the computed size kept), its weight and its contrast against the background the DOM resolves, form-field values and placeholders included on the field's own background, clipping, interactive targets against 44 pt, 48 dp and 24 px, overflow, axe on the web row, and the page's console, CSP and request problems. It captures only this checkout's source: a static export must carry this checkout's source fingerprint, and a live dev server (Metro, for the fixer loop) is recorded as one and must serve this checkout's docs app. The manifest's capture settings are read off the audit's Playwright configuration and the suite's fixed clock. Repository tooling and docs only; nothing in the package changes.

## 3.5.0

### Minor Changes

- 0c73b5c: Add Command descriptions, externally filtered results, embedded composition, deferred autofocus, empty-state text and keyboard handlers. Long command lists scroll within the palette. Add opt-in Dialog backdrop dismissal and allow its content to shrink to the available overlay height.

### Patch Changes

- 88ae634: A loading Button no longer exposes an unnamed progress bar: react-native-web draws its spinner as a `progressbar` with no name, which assistive tech met as a second control inside the button, so the spinner is now hidden from assistive tech the way Spinner hides its renderer, and the button itself stays a named, busy button. Form takes an optional `accessibilityLabel` that names its form landmark (ARIA asks every form landmark for a name, and it is how two forms on one page are told apart); without it a Form renders exactly as before. The docs follow: the catalog's category headings are h2 under the page's h1, its two forms are named, and its ScrollView tile scrolls a bar of filter chips a keyboard can reach. The Activity and Team templates keep their search and email fields beside a Select that hugs its value, where a filling Select had squeezed the field to nothing and lay over it. The docs mark the look a page has committed (`data-look` on the root element), and the browser suite waits for it, since a link's scheme and surface now land after hydration and a dark page could be scanned while it still showed the server's glass. The structure check expects the Do & Don't section heading the component pages render.
- fd0ea8c: The component audit gets its checklists: `audit/components/<slug>.md` for each of the 104 component pages and `audit/pages/<kind>-<slug>.md` for the 24 patterns and templates, each with a generated facts block and variants table (one row per example, a tick cell per platform) merged by key so a reviewer's ticks and notes survive regeneration, and a hand-maintained rubric, family and specific checks, findings and sign-off seeded once from the plan. `bun run audit:checklists` writes them, `audit:checklists:check` (in CI and the pre-push hook) fails on a missing, orphaned or stale checklist and on any table row it cannot read, named by line, and `audit:status` counts ticks, open findings by severity and sign-offs, listing every row it could not count under the counts and exiting non-zero when there is one. Every audit table (variants, findings, sign-off, and the `PLATFORM-REFERENCES.md` catalog the facts read) goes through one escape-aware reader, so a `|` typed in a summary or a note stays in that cell and an empty cell typed `| |` no longer drops its row. The facts come from the repo's own records: tests and e2e count for a component when they import it from the kit, or for e2e when they drive the exact route of its docs page or of a hidden `/testing/*` page that renders it (a route in a comment, or `/components/button-group` for Button, no longer counts), and the touch-target and measure facts read identifiers in the component's TypeScript modules, never its markdown. Which builds look different per platform is now read per export with the TypeScript parser and fails safe: a platform part counts only when the export it injects diverges itself, and a namespace import, a helper function or a wrapped spread the reader cannot follow counts as the platform's own skin rather than the web build. The shells gate judges each imported export by that same read, and the docs' platform-skin registry drops the entries it proves are the web build: Avatar and AvatarGroup on both tables, Tabs on iOS, and MetricBreakdown on iOS, whose iOS Chip aliases the web skin (its markup is identical to the web build's in every scheme and surface). Repository tooling and docs only; nothing in the package changes.
- acacf47: The docs app runs on the iOS 27 SDK again: apps linked against it that keep the app-delegate-only lifecycle stop at launch, so the docs move to Expo 57.0.27 with `expo-build-properties`' UIScene support turned on, every Expo SDK package at its matching version, and the five docs patches re-created for the new versions (all still needed, all still in force under `check:patches`). Two end-to-end journeys measure what they mean again: the wide-trigger popover is checked against its overlay outlet, which the page frame now puts inside its gutters, and the search resize journey starts on the desktop shell that has the topbar opener, whose compact form now carries the same "Search components" name. The Linux visual baselines are re-minted for that page frame: every preview card is 16 px wider and its rows hug their content, and no component's own rendering changed. Repository tooling and docs only; nothing in the package changes.
- 07ae54d: The docs' committed-look marker now names the palette as well as the scheme and surface (`data-look="light solid mint"`), and the browser suite waits for all three, so a page opened in a mint link is read only after the mint palette has committed. Repository tooling and docs only; nothing in the package changes.
- 3b3c880: A docs link or deep link can open the mint palette: `?palette=blush|mint` rides beside `?scheme` and `?surface`, the docs hand it to the kit's ThemeProvider as `mint` (dark still wins), and the phone web drawer's appearance footer gains a Blush/Mint control in the light scheme. The preview opener validates each of the three axes on its own, refuses a value outside its set by name, and now quotes the Android remote command for the device's shell: `adb shell` joins its words and the device's sh parses them again, so the `&` between two axes had sent `am start` to the background and the emulator opened every multi-axis link with only its first axis. The page shown when a launch fails prints a hand-run command that pastes into zsh or bash unchanged. The opener's pure half moves to `docs/scripts/preview-links.mjs`, pinned by `tools/docs/preview-server.test.ts`, which splits the commands back through real shells; an end-to-end test clicks the drawer's Palette control and reads the backdrop repaint, and the scheme and palette checks read the page backdrop itself rather than the largest opaque box. Repository tooling and docs only; nothing in the package changes.
- c95717d: The docs preview opener finds `adb` the way Expo CLI finds the Android SDK (`$ANDROID_HOME`, then `$ANDROID_SDK_ROOT`, then the default SDK location, then `PATH`), so an Android Preview link works from a dev server whose `PATH` has no platform-tools, and its failure page reads "Make sure an Android emulator is booted" with the platform's own capitalization. The opener's shell-quoting tests now require zsh rather than skipping without it, and CI installs zsh before the unit tests, so the check that a pasted command survives zsh's `?` glob runs there too. Repository tooling and docs only; nothing in the package changes.
- 0c73b5c: Keep Switch labels and descriptions within their parent bounds on narrow layouts while preserving the track size and whole-row tap target.

## 3.4.4

### Patch Changes

- 98596c4: Retry the npm publish that `patient-scopes-return`'s release attempt started but couldn't finish: that changeset's version bump (3.4.3 / 0.1.1) landed in git and on GitHub, but the registry publish itself failed on an invalid NPM_TOKEN (`ENEEDAUTH`), so neither package actually reached npm and the git tags were never pushed. Per this repo's own recovery path (a partial release gets a new changeset, not a retry of the same version), this one carries the versions forward so the next CI run makes a fresh publish attempt with the corrected token.

## 3.4.3

### Patch Changes

- 47fe3e3: Move the repository and npm scope back to the personal account. GitHub `ionizeio/canvas` transfers to `bnannier/canvas`, and the packages publish as `@nannier/canvas` and `@nannier/canvas-blur`, continuing the version lineage `@ionizeio/canvas` and `@ionizeio/canvas-blur` were on. `@ionizeio/canvas`, `@ionizeio/canvas-blur`, and `@nannier-com/canvas` stay installable but should be deprecated pointing at the new name once this publishes.

## 3.4.2

### Patch Changes

- ba73413: On iOS and Android, a named `StackedList`, `Feed` or `GridList` now reaches the screen reader as a list, and this corrects the Android note in 9117fdc's entry, which said a windowed list and a `GridList`'s root already read as named lists there. They did not: an eager list's container carried only a role and a name, so React Native's renderer removed it or moved its rows out of it and Android showed no list at all, and a windowed list's scroller was a list with no name, since React Native's ScrollView does not pass `aria-label` on to the platform.

  A list with a name (a `StackedList` title that is a string, or `label` on any of the three) now keeps a native container that holds its rows, named in the platform's own label, so TalkBack says "Team members. List" and then "In list Team members" on the first row, as it does for a `Listbox`. On iOS the container holds its rows and VoiceOver reads the rows as before. A list without a name keeps its old native behaviour, each row read on its own: React Native's Android delegate would otherwise read the text of every row as the container's own description before the rows. A `StackedList` title that is not a string names the list on the web by reference, and `label` names it natively. On the web a string title now names its list directly rather than by reference, with the same name, and nothing else in the page changes.

## 3.4.1

### Patch Changes

- 3fa683a: The `E2E soak` workflow's notes record what its crash capture found: the one Firefox parent-process SIGSEGV it caught in the Firefox journeys is a use-after-free in Playwright's own Firefox patch (the headless compositor widget's draw target, swapped and released on the compositor thread when a window resizes while WebRender's render thread reads it without a lock), which stock Firefox does not have and the docs pages do not cause; the two earlier crashes left no core and are only consistent with it. The notes also say a kept core is safe in the public artifacts only because the soak job holds no secrets. Repository tooling only; nothing in the package changes.

## 3.4.0

### Minor Changes

- 9117fdc: `StackedList`, `Feed` and `GridList` are lists to a screen reader, as the Tailwind UI lists they port are, and take a new `label` option that names the list. Minor, for that new option: `label?: string` on all three names the list for assistive technology ("Recent activity, list, 12 items").

  On the web the rows had no list semantics at all: they were plain elements (a clickable row was a button), so a screen reader announced no list, no item count and offered no list navigation. Now the rows sit in a `list` and each row is a `listitem`, which react-native-web renders as a `<ul>` of `<li>` elements. A clickable row, a pressable event and a tappable tile stay buttons, each inside its item. A `StackedList`'s header `title` names its list; `label` names one without a title (the title wins when both are set). In a `reorderable` list each item wraps its draggable row, so the preview a pointer drag lifts carries no list item, and a list with no string title gives its drop zone the `label` for a name.

  A `virtualized` list with a bounded height is itself the list and the keyboard stop, where the stop was an unnamed group. It mounts only the rows near its viewport, so each rendered row carries `aria-setsize` and `aria-posinset` and a screen reader counts the whole list, not the rows it has rendered. Focused, a list with no name is named in Chromium from the text of every row it has rendered, so a windowed list needs a `title` or a `label`, and the kit warns in development when it has neither.

  Natively, React Native's role parser accepts both roles. iOS gives a list and a list item no trait, so VoiceOver reads the rows as before. On Android, TalkBack reads a windowed list's scroller and a `GridList`'s root as a list (`android.widget.AbsListView`), with `label` as its description, where they were plain view groups; a list item maps to no Android role.

### Patch Changes

- 3bc3149: On Android, TalkBack now reads a `Listbox`'s name in every configuration, and a single-select `Listbox` as a list. React Native's Fabric renderer removes a view whose props neither paint nor mark it (a role or an accessibility label does not count), and moves the children out of a view that only paints or carries a `testID`. So an unbordered, enabled `Listbox` had no native container at all, and a bordered one, or one given a `testID`, was an empty node named by its `accessibilityLabel` with its rows beside it; only a disabled list held its rows. The container is now always a native view that holds its rows. A single-select list is an `android.widget.AbsListView` named by its label, so TalkBack reads "Teams. List", then "In list Teams" on the first row and "Out of list Teams" on leaving. A multi-select list is a group, which Android gives no role, so TalkBack reads "Teams" before its checkboxes. Before and after screenshots match: the plain list in glass and the bordered lists in glass and solid on iOS and Android, and bordered multi-select lists on iOS.

  On iOS the container is now a native view carrying the label too, but it is not an accessibility element and declares no accessibility container type (React Native sets none), and the Simulator's accessibility tree still lists only the rows. Whether VoiceOver speaks the list's name was not tested, since VoiceOver does not run in the Simulator. Read from React Native's iOS source, not observed: the new view groups the list's rows for VoiceOver's navigation, and an `accessible` ancestor without a label of its own now takes the list's label instead of joining the rows' labels. The web's DOM is unchanged (`role="listbox"` or `role="group"` with its `aria-label`).

## 3.3.0

### Minor Changes

- b8087c8: `DataTable` takes a new `stacks` option (with `stackBreakpoint`, default `sm`), the minor's user-visible capability: when the table's own container is at or below the breakpoint, each row lays its cells out top to bottom at the full width instead of side by side, so long text reads at the table's width rather than a column's share of it. The first cell leads the row as its title and every later cell sits under its column label, set in a new small muted label type per platform (the kit's caption role on the web and Android, where Material 3 has no data table to take a label from, and Caption 2 on iOS). The header row stays only while it holds a control (a sortable column, or the box that selects every row), as a wrapping band of its labels; the selection box and the row actions keep their places. A stacked table never pans, and on iOS it stacks instead of collapsing to its first column. Like `Row stacks` it is measured against its own container, seeded from the window on the first frame, and crossing the breakpoint changes only the layout: rows, stateful cells and an open editor stay mounted. Without `stacks` nothing changes. The docs' prop tables use it, so on a phone every prop's description now reads under its name and type, on iOS too, where the table used to show only the prop names.

### Patch Changes

- 52d57d5: A web or Android `DataTable` narrower than 640 px no longer grows to fit its longest cell on one line. While it panned, nothing bounded the table's width inside its horizontal scroller, so every cell's text stayed on a single line and the longest cell set the width of the whole table: a 1,507-character prop description made the docs' prop table about 9,500 px wide on a phone, and on Android each row split that width by its own content, so short rows pushed their text thousands of pixels to the right. The table is now the scroller's width, or the columns' minimum widths added up when those are wider: the columns share that width and wrap their text, and the table scrolls sideways only when the minimums do not fit, as the documentation already described. Short cells that used to sit on one line inside a panning table now wrap within their column.
- 65ec0e3: Follow-ups to `DataTable stacks`. A stacked row's selection box and row actions now center on its first line of cell text, as the kit's labelled controls do; on the web they sat about 10 px above it. A stacked cell whose column has no label (a trailing menu column) no longer carries an empty label above its content. The Android skin's stacked label is the web's caption label, since Material 3 has no data table of its own. The DataTable docs now say that the iOS first-column layout, unlike panning and stacking, drops the later columns' cells below its width and mounts them again when the table widens. The docs' phone-width check now also fails a page whose Props table scrolls sideways inside its own frame, which the page-level check could not see.
- e72ed20: A `virtualized` DataTable's body is now a keyboard stop the table manages, on the web, in every browser. Its rows scroll in a list of their own under the fixed header, and the table never said whether that list was a tab stop: Chromium and Firefox made it one when nothing inside it could take focus (a read-only table's rows), with the browser's own ring drawn on the list (blue in Chromium, black in Firefox, and on a phone-width table clipped to its top edge), while WebKit left it out of the tab order, so Safari's keyboard could not reach the rows below the fold. The body is now a tab stop while its rows overflow and none while they fit, after the column scroller when a narrow table pans, and while it has keyboard focus the table's surface draws the theme's focus ring for it, just inside the edge of an `attached` table, as it does for the column scroller. As with the column scroller, the rule holds whatever the rows hold, so a windowed table whose rows have controls (selection boxes, row actions) gains the body as one more stop, just before its first row's controls, and the keyboard can always scroll it. The body is the table's row group, so the rows it renders stay inside the table for a screen reader rather than behind an unnamed focusable node. A click into the rows draws no ring until a key is pressed, and a table that switches to its loading rows while the body has focus drops the ring with it.
- 64ed0d7: The docs site takes expo-font 57.0.4, whose web loader compares a font face's family name without the quotes Firefox's CSSOM keeps. With 57.0.1, Firefox found none of the pre-rendered page's seven faces loaded, so right after hydration expo-font moved their style element seven times and declared every face a second time: each face was fetched again and the page's text vanished, then showed in a fallback face, before its own returned. A new cross-engine journey (`e2e/journeys/fonts.e2e.ts`) holds Chromium, Firefox and WebKit to adopting the document's faces. Docs site and tests only; nothing in the package changes.
- 3e04884: The docs site keeps a photo-bearing example mounted when its preview re-renders. The Playground and the Don't previews rebuild their example scopes on every render, and the wrapper that resolves the sample photo and clip paths was a new component each time, so every Image, Avatar, Video and other photo-bearing example remounted whenever the stage re-rendered, for instance when its measured width changed. Entering full screen resizes the viewport in Firefox, so the Video's full-screen button put the clip in full screen and the remount took the `<video>` out of the document at once, which ends full screen. Each component is now wrapped once, and `test/docs-photos.test.tsx` holds a rebuilt scope to the same component types and a re-rendered example to one mount. Docs site and tests only; nothing in the package changes.
- 8e295c9: The end-to-end suite's `gotoDocs` now says what its paint check saw when it gives up: a page read in the wrong look reports how many readings it took and the last one, and a page whose first evaluation never returned says it was never read, instead of both reporting that the page "never painted". Tests only; nothing in the package changes.
- db2b3fc: The end-to-end suite gains an opt-in record of the protocol events that decide which JavaScript worlds Playwright can reach in a page (execution contexts created, destroyed and cleared, navigations, and every evaluation with whether it was answered), attached to a failed test when `E2E_PROTOCOL_EVENTS=1`; the manual `E2E soak` workflow turns it on with `protocol_events: on`. Repository tooling and tests only; nothing in the package changes.
- 2c30f95: The `E2E soak`'s crash capture reads a core's executable from the auxiliary vector gdb reads (file(1) gives up on a core of more than 2048 program headers, which a long-lived browser can reach), names the trap behind a fault (a page fault's read, write or instruction fetch and its address, which the kernel does not log for Firefox's parent), labels SI_KERNEL as a general protection fault or an undeliverable signal, keeps every core, report and browser build even when one of them fails, and moves that work into `scripts/soak-crash-dumps.mjs keep`. The lane's self-test now crashes the browser process itself even behind WebKit's launcher script, gives up after 30 seconds instead of waiting out the job, and puts its core through the same `keep` path, checking the compressed core and that the build archive holds the executable. Repository tooling and tests only; nothing in the package changes.
- 50768ec: The `E2E soak`'s crash report reads the fault a crashed process's own signal handler caught, from the signal frame on its stack: Firefox's parent process catches SIGSEGV and re-raises it, so the signal its core records is the re-raise and the kernel logs no segfault line for it. The report now gives both the signal the process died of and the one its handler caught first (with the fault address or sender, and the interrupted instruction and registers), and the lane's self-test checks the caught one. Repository tooling and tests only; nothing in the package changes.
- 50ef9e9: The manual `E2E soak` workflow keeps a record of a browser that crashes outright: every lane saves the kernel's line for each process a fault killed (the thread, the address, and the module and offset of the instruction), and `crash_dumps: on` keeps each crashed process's core dump with a gdb report of every thread's stack, the signal and Mozilla's crash reason (`scripts/soak-crash-dumps.mjs`), after first proving the capture on a browser the lane crashes itself. Playwright's Firefox has no crash reporter of its own, so this is the only record of the rare Firefox parent-process SIGSEGV the Firefox journeys meet. Repository tooling and tests only; nothing in the package changes.
- 152d40c: The manual `E2E soak` workflow can replay the cross-engine journey and touch projects (`journeys-chromium`, `journeys-firefox`, `journeys-webkit`, `touch-chromium`, `touch-webkit`), installs only the browser the chosen project runs, and takes the hang probe's delay as an input (`hang_probe_ms`, checked and passed through `env`). The hang probe now reports on Firefox and WebKit too: on Linux what their processes' threads did across a window, whether a new evaluation in the page's own world and one in Playwright's utility world still answer, the document's loading and font state, and whether a screenshot comes back. Repository tooling only; nothing in the package changes.
- 2cc86c1: The manual `E2E soak` workflow gains `browsers: fresh`, which runs each pass as its own Playwright run so every pass meets its first tests in newly launched browsers, keeping only the failing passes' reports; `passes` and `hang_probe_ms` are checked to ranges a loop and a timer can hold. The hang probe also samples WebKit's WPE and MiniBrowser processes and no longer throws when a process exits mid-sample, and `gotoDocs` reports a paint check whose evaluation was still out at the deadline as "stopped answering" even after earlier readings. Repository tooling and tests only; nothing in the package changes.
- 565528c: A lane of the `E2E soak` that keeps a crashed browser's core dump (`crash_dumps: on`) now also keeps the Playwright browser build that wrote it, compressed beside the core: a core reads only against its exact binaries, and Playwright's are stripped, so naming its frames later takes those files and the report's module offsets. Repository tooling only; nothing in the package changes.
- 749f6a3: The manual `E2E soak` workflow's `spec` input also takes a directory under `e2e/` (checked to contain no dots, so never `..`), so a soak can replay a project's whole journey set in the order a shard runs it. Repository tooling only; nothing in the package changes.
- 3564b7b: `FOCUS_RESET`'s notes now say why it names an outline style as well as a zero width: the browser's own focus ring is `auto`, which ignores the width, so `outline-width: 0` alone still draws it in Chromium, Firefox and WebKit wherever the CSS hand-off is not loaded. The repository's focus tests treat an `auto` outline as drawn whatever its width. Comments and tests only; the reset's value does not change.
- f63f103: `FOCUS_RESET` now switches the focus outline off with a zero width and a `solid` style, in place of `outlineStyle: "none"`. React Native parses the outline keys natively on View and TextInput, and its parser accepts only `solid`, `dotted` and `dashed`, so on iOS and Android every node carrying the reset logged "Could not parse OutlineStyle:none" and a `react_native_expect` failure: the Input (every skin), Textarea, Autocomplete, PhoneInput and InputOTP fields, the Slider, Command's search field, the DescriptionList edit field, and the scrollports inside CodeBlock, DataTable, Carousel and the option lists. Nothing changes on screen: a zero-width outline draws nothing natively, and on the web the inline style still beats the browser's own `:focus-visible` ring and the CSS hand-off's layered rule. `FOCUS_RESET` is now typed against React Native's own style types rather than cast, so a style native cannot parse no longer type-checks. A reset node's computed `outline-style` is now `solid` rather than `none`: code that reads it to decide whether a node's ring is off should read a zero `outline-width` instead.
- d91177c: The calendar Heatmap is one keyboard stop on the web instead of one per day. Its day cells asked to stay out of the tab order with `focusable={false}`, which react-native-web's Pressable overrides with a tab index of its own, so crossing a year took 372 presses of Tab through cells with no role or name, and the arrow keys never panned the grid. The cells now also pass `tabIndex={-1}`, and the grid's scroller takes the scrollport keyboard stop DataTable, CodeBlock and Carousel share: only while the grid overflows, Tab lands on the scroller, the arrow keys pan it, and it wears the theme's focus ring. The scroller takes no role or name of its own: Chromium names it from the one image inside, so it announces the grid's summary ("Contribution activity, 371 days, 1317 total"). Pressing or hovering a day still opens its flag, and iOS and Android are unchanged apart from the scroller becoming keyboard focusable on Android while the grid overflows, as DataTable's and CodeBlock's are.
- e012da7: On Android, TalkBack's touch exploration reaches the tabs, cards, rows and cells inside a horizontal scroller whose content fits again. A fitting `DataTable`, `CodeBlock`, `Carousel`, `Tabs` row, `Board` or calendar `Heatmap` disables its scroller on Android so it stops claiming sideways drags, and React Native's disabled horizontal scroller also drops every hover event before its children see one; touch exploration is made of hover events, so an exploring finger could not land on anything inside. The scroller now stays enabled while touch exploration is on (and until Android has reported it), where a finger explores rather than presses, and still stops claiming drags whenever TalkBack is off. The web and iOS are unchanged.
- e0e718f: On iOS and Android, an option list no longer logs a native prop-parser error. The container of a single-select `Listbox`, of the `Select`, `Autocomplete` and `Command` option lists and of the `PhoneInput` country list carried `role="listbox"`, which React Native's native role parser does not accept: every mounted list logged "Unsupported Role value: listbox" and a "react_native_expect failure" to the app's stderr, and then carried no role. Natively the container is now a `list`, which the parser accepts. VoiceOver reads the options exactly as before, since iOS gives a list and a listbox the same empty trait set. On Android, TalkBack now reads the container as a list (`android.widget.AbsListView`) where it read a plain view group, wherever the container is a native view: an open `Select`, `Autocomplete`, `PhoneInput` or `Command` list, and a `Listbox` that is bordered, disabled or given a `testID`. On the web the container is still a `listbox`, in every build the documentation previews.
- 80d6a9e: An option list whose rows overflow its card (Select, Autocomplete, the PhoneInput country list, Command, or any anchored card's content) now shows the theme's focus ring on the web when its scroll area takes keyboard focus, instead of the browser's blue one clipped by the card's corners. The scroll area sits flush inside the card's clip, so the card draws the ring around itself, the same solid 2 px `--ring` at the kit's offset that CodeBlock, DataTable and Carousel cards now draw, and only while a key lands on the list (Shift+Tab from its first row, or a key pressed on it); a click draws none. The bare Command palette's card does the same for its result list. iOS and Android are unchanged.
- 153e322: The end-to-end suite runs on Playwright 1.64.0-alpha-2026-09-25 (Chromium 155, Firefox 156 build 1551, WebKit 26.6), pinned exactly with an override that keeps `@axe-core/playwright`'s `playwright-core` peer on the same build. Firefox build 1551 carries the fix for microsoft/playwright#42731: a navigation to a `Cross-Origin-Opener-Policy: same-origin` page, which every docs page is, could drop the page world's execution context event, so a Firefox test's first `page.evaluate` waited forever (about one Firefox test in 8,000 on the E2E soak; none in 44,800 on this build). The committed Linux screenshot baselines hold on Chromium 155. Tests only; nothing in the package changes.
- 5c06033: The kit's `Pressable` keeps a `focusable={false}` Pressable out of the tab order on the web. react-native-web's Pressable renders a tab index of its own (0 unless disabled), which wins over `focusable`, so a surface that asked to be pointer-only stayed a keyboard stop. The kit's Pressable now spells the request as a tab index of -1 unless the caller sets one; React Native reads that as `focusable={false}` on iOS and Android, so nothing changes natively. In the DataTable, a pressable or selectable row was an unnamed tab stop in front of its activator button or checkbox (two presses of Tab per row, the first landing on the row), and each inline-editable cell of a pressable row was one more; in the Video, the picture under the web control bar was hidden from assistive technology yet still took keyboard focus. Tab now crosses only the named controls. The Pressable page says what `focusable={false}` does.
- 52f5a9d: A keyboard-focused CodeBlock, DataTable or Carousel scroller now shows the theme's focus ring on the web instead of the browser's blue one. Each of these scrollers sits flush inside a clipping card, so it could not wear the ring itself: the card clips a ring drawn outside the scroller, and Chromium paints the scrolled content over a ring drawn inside it (a terminal's dark rows hid even the browser's ring). The card now draws the ring instead: the CodeBlock's card or terminal window, the DataTable's surface, and the Carousel's viewport with the slide's corners. It is the CSS hand-off's solid 2 px `--ring` at the kit's 2 px offset; an `attached` DataTable, which sits flush inside a parent frame that clips, draws it just inside its own edge instead. It shows only while the scroller has keyboard focus: Tab (or Safari's Option+Tab) onto it, or a key pressed on it, and it comes back when focus returns from another window. A click into the code draws no ring, just as `:focus-visible` draws none, and the scroller drops its own ring so the two never stack. iOS and Android are unchanged, since their views take no key events unless an Android app turns on React Native's `enableKeyEvents` flag.
- c299daf: On Android, the horizontal scrollers in `Tabs`, `Board` and the calendar `Heatmap` no longer claim a sideways drag while their content fits. Android's horizontal scroll view takes any drag that moves sideways past touch slop even when it has nothing to scroll, so a tab row, a board or a contribution grid that fit its container dropped a press when the tap drifted sideways (a tab, a card, a day cell), and a swipe that started across it could not scroll the page for that gesture. Each now takes a drag on Android only while its content overflows, the rule `DataTable`, `CodeBlock` and `Carousel` already follow. Selecting a tab still scrolls it into view. The web and iOS are unchanged, and the shared scroll measurement now re-renders a scroller only when its content starts or stops overflowing, not on every resize.
- 2c4cee1: On the web, a Video's picture is no longer a keyboard stop in Firefox. The picture is expo-video's `<video>`, which takes no tab index or ARIA prop, and Firefox makes a `<video>` without the browser's controls a tab stop with no role or name, so Shift+Tab from the control bar's Play button, or from the inline player's picture control, landed on an unnamed element in front of the player's named controls. Wherever the kit draws the controls, the clip surface and its poster now sit in an `inert` layer, which react-native-web renders as the HTML attribute (spelled `true` for React 19 and as an empty string for React 18, the only value each writes): out of the tab order and the accessibility tree and never the target of a press, while full screen still works from inside it. Chromium and WebKit never stopped there. iOS and Android are unchanged: React Native has no `inert` prop and drops it, and with `controls` the platform's own player controls keep their surface.
- c561c84: A `virtualized` StackedList, Feed or GridList is now a keyboard stop the list manages, on the web, in every browser. Each scrolls its rows (or tiles) in a list of its own, and none of them said whether that list was a tab stop. With read-only rows, Chromium and Firefox made it a tab stop of their own, with the browser's ring drawn on that inner list (blue in Chromium, black in Firefox; inside a StackedList's or Feed's card, and cut off at the edges of the avatar Feed's flush card), while WebKit left it out of the tab order, so Safari's keyboard could not reach the rows below the fold. The list is now a tab stop while its rows overflow and none while they fit, and while it has keyboard focus the theme's focus ring is drawn around the StackedList's or Feed's card (or a plain StackedList's frame) and around a GridList, which has no card of its own, as it is for a windowed DataTable's body. The rule holds whatever the rows hold, so a windowed list whose rows are buttons (a `clickable` StackedList, a Feed with `onItemPress`, a GridList with `onPressItem`) gains the list as one more stop, just before its first row, and the keyboard can always scroll it. The stop is an unnamed group: as a plain scroller, Chromium named it with the text of every row it rendered, so a screen reader would have read them all out on focus. A click into the rows draws no ring until a key is pressed. A list that renders eagerly (without `virtualized`, without a bounded height, or a `reorderable` StackedList) is unchanged.
- 6f883aa: The manual `E2E soak` workflow's `playwright` input takes a prerelease too (e.g. `1.64.0-alpha-2026-09-25`), swaps both `@playwright/test` and `playwright` so the CLI that installs and launches the browsers matches the runner, and checks the version it reports. The `Visual baselines` workflow gains the same input, so an upgrade's Linux baselines can be minted before the upgrade lands. Repository tooling only; nothing in the package changes.

## 3.2.4

### Patch Changes

- a299273: The e2e suite's intermittent 60 s timeouts in the glass material captures are fixed at their cause. On the CI runner, where the software compositor takes about a second per frame of the glass pages' backdrop blurs, Chromium's renderer sometimes held its next draw for a display acknowledgement that never came, and the whole frame pipeline went to sleep for good. Every Chromium project now launches with `--disable-frame-rate-limit`, which lifts that wait: the E2E soak measured 8 hangs in 128 passes of the material spec without it and 1 in 320 with it, with the Linux screenshot baselines unchanged. It mitigates the Chromium defect rather than curing it. The four-frame drain added before captures (5da48254) is removed, since the soak showed it did not prevent the stall; the real-frame waits in `settledBox` and the overlay-state resize audit stay, because `page.clock` fakes the page's own `requestAnimationFrame`. Repository tests only; nothing in the package changes.
- b65c8e8: The manual `E2E soak` workflow can drop the suite's own Chromium switches (`pacing: chromium-default`) and soak another `@playwright/test` release installed on the runner only (`playwright`), to tell whether a newer Chromium still needs `--disable-frame-rate-limit`. Repository tooling only; nothing in the package changes.
- 384eb9c: The e2e configuration's note on `--disable-frame-rate-limit` now states what the soak measured on the shipped configuration: 1 hang in 320 passes of the material spec with the switch, against 8 in 128 without it, so the switch mitigates the Chromium stall rather than removing it. Repository tests only; nothing in the package changes.
- 170cc56: On Android, a horizontal scroller whose content fits no longer claims a sideways drag. Android's horizontal scroll view takes any drag that moves sideways past touch slop even when it has nothing to scroll, so a `DataTable` that fit its container (always wrapped in its scroller since its rows stay mounted across the pan width) dropped a row's press when the tap drifted sideways, and a swipe that started across it could not scroll the page for that gesture. The same held for a `CodeBlock` whose lines fit and a `Carousel` with nothing to page. These scrollers now take a drag on Android only while their content overflows. The web is unchanged: a disabled scroller there would set `touch-action: none`, and a finger on it could no longer scroll the page.
- e8cb328: The manual `Visual baselines` workflow no longer interpolates its `project` input into a shell script: the value reaches the script through `env` and must exactly name a project defined in `playwright.config.ts` (read from the config itself), and anything else fails with an error that lists the projects. Repository tooling only; nothing in the package changes.
- a650c4a: The manual `npm deprecate` workflow no longer interpolates its `range` and `message` inputs into shell scripts: both reach the scripts through `env`, and the lines that echo them (and the registry's copy of the message in the run summary) are fenced with `stop-commands`, so a line in a message cannot act as a workflow command. The `E2E soak` workflow's spec and Playwright-version checks become anchored patterns and print a rejected value shell-quoted. Repository tooling only; nothing in the package changes.

## 3.2.3

### Patch Changes

- 64dc6b2: The manual `E2E soak` workflow can now run full Chromium in its new headless mode (`channel`) or add Chromium switches (`chromium_args`) through its own `playwright.soak.config.ts`, which layers them over the suite's configuration for the Chromium projects only, and soaks no longer queue behind each other, so two hypotheses about one commit run side by side. Repository tooling only; nothing in the package changes.
- 6c777c9: The manual `E2E soak` workflow gains a `trace` input (`retain-on-failure`, the suite's own setting, or `off`), so a soak can tell whether the screencast a Playwright trace records takes part in a failure. Repository tooling only; nothing in the package changes.

## 3.2.2

### Patch Changes

- 5da4825: The e2e suite's captures now start only once the page's own frames have gone through Chromium's frame pipeline. `fitElementForScreenshot` resized and scrolled the page and handed it straight to `Page.captureScreenshot`; on the CI runner, where the software compositor takes about a second per frame of the glass pages' backdrop blurs, the capture sometimes began while that frame was in flight and wedged the pipeline, so a material test hung until its timeout (3 of about 26 Deploy runs; about 1 pass in 26 of the material spec in the E2E soak). The new `drainFramePipeline` waits four real animation frames (from an isolated world, because `page.clock` fakes the page's own), runs at the end of every fit and before every page-level material capture, and fails by name when no frames come; `settledBox` and the overlay-state resize audit wait real frames too. Window-layer overlay panels are counted outside the page's scroller, so a Do/Don't card pinned open on the page, which appears once its placement is measured, can no longer change the specs' before and after counts. Repository tests only; nothing in the package changes.

## 3.2.1

### Patch Changes

- bf00dfe: Android's hardware back now closes an open menu, list or popover instead of navigating away underneath it. Every card that floats beside its trigger (the Dropdown and AvatarMenu, RowMenu, Select, Autocomplete, Popover, Command, the PhoneInput country list, the ButtonGroup split menu and the Calendar day peek) subscribes to back while it is open, closes on it and consumes it, so the next back belongs to the page again. A card open inside another one closes first. A card pinned open with no `onOpenChange`, which cannot close, leaves back to the page, and the web never subscribes.
- 69a41c3: A tap anywhere off an open menu, list or popover now closes it, also when the card opens inside a nested `OverlayProvider`. The dismiss layer used to fill only the nearest provider, so on a docs example stage (and in any app that nests a provider around a panel) a tap beside that provider left the card open on iOS, Android and the web. A card that can close now paints, with its dismiss layer, in the outermost provider of its window, at exactly the place its nearest provider gives it, so the tap closes it and goes no further, the way a platform menu behaves; while it is open the page under it takes no touches and does not scroll. An open card also follows its trigger when the content under it moves without a touch (a keyboard or programmatic scroll, a layout shift), where it used to stay behind in an app whose provider sits outside its scrolling content; a card opened from inside another card is placed by the same provider as its parent. A card pinned open with no `onOpenChange` still paints in its nearest provider, and a provider in a separate window (a `Drawer`, or `separateWindow`) remains its own window. Portaled content now resolves the `BreakpointOverride` of the component that opened it, wherever its provider sits.
- f523111: Re-mint the Linux visual baselines for the Carousel with its arrows beside the slides.
- a12770f: The Carousel's prev and next arrows now sit beside the slides instead of over them. They were laid 8px inside the slides, where they covered the start of any slide text (a slide titled "Start" read "art") on the web, and on iOS and Android wherever `showArrows` turned them on. The track is now a row of the prev arrow, the slides and the next arrow inside the carousel's own bounds, the way shadcn's carousel hangs its arrows outside the track, so the slides narrow by the two arrow gutters. On iOS and Android each arrow's touch area stays inside the carousel and off the slides; on the web the arrows sit 4px in from the carousel's edge, so their focus ring stays inside it. The DOM now follows the screen, so Tab and screen readers meet the previous arrow, the slides, the next arrow, then the slide picker (the slides used to come first).

  A slide is the card surface. In solid mode it now carries the card's resting hairline, so it no longer disappears on a surface of its own color (under glass the hairline keeps its 1px width in a transparent color and the material's rim is the edge, as on a Card), and a plain-string slide is inset from its edge (20px on the web, 16 on iOS and Android) instead of sitting flush against it. The docs examples pass strings rather than nesting a Card, which framed every slide twice and, on Android, lost the Card's edge near the Material 3 item's 28dp corners.

- d73bb2f: `Carousel` now keeps every slide mounted from the first frame. Until its viewport had measured, it rendered a stand-in copy of the current slide and then swapped it for the paged list, so the current slide's content mounted twice on every mount (its effects ran twice). A carousel whose viewport was hidden and shown again (inside a collapsed panel, say) also dropped every slide's state, including a field's typed text. The slides now sit in one paged ScrollView for their whole life, and measuring changes only their styles: before a width is known, the current slide fills the viewport and the others wait hidden, which looks the same as before. The list is a ScrollView rather than a FlatList, so a carousel no longer windows its slides; FlatList's default window already mounted up to about 21 slides, so only very long carousels mount more than before. On leaving that unmeasured layout, the list takes a 1px step within the current slide's page before landing on it, so a browser that re-snaps to the previously snapped slide after a layout change (Safari/WebKit does, as the CSS Scroll Snap spec asks) cannot jump to another slide.
- 0d18838: On Android a tappable Chip's touch area is now the 48dp minimum, measured from the chip without it changing size: 7dp above and below the 34dp chip, and nothing sideways once the chip is 48dp wide, so chips side by side in a Row no longer reach into each other. A tappable chip with a remove glyph reaches the same minimum from its label, which covers the whole chip up to the glyph. iOS keeps the 11pt of extra touch area it has always had around its 25pt chip. The chip's docs state both.
- fc9799b: A web or Android `DataTable` now keeps everything inside it mounted when its container crosses the 640px width where the columns start or stop panning. It used to add or remove its horizontal scroller at that width, so React remounted the header, every row, stateful custom cells, the `emptyMessage`, an open inline editor (swapped for a new field) and a virtualized body (its scroll position lost), on every resize across the width and once on every phone mount, whose first frame is unmeasured. The scroller is now always there on those platforms and panning switches only its styles: while the table fits, its content is exactly the table's width, so the layout is unchanged, nothing scrolls and there is no extra tab stop. The table role now sits on the same node at every width, so the pagination footer follows the table instead of sitting inside it at wider widths. iOS, which collapses to its primary column instead of panning, is unchanged.
- c35bea5: Three follow-ups to keeping a web or Android `DataTable`'s scroller in place at every width. A table that fits its container again clips columns that cannot fit (fixed column widths wider than the container), as it did before; it had become sideways-scrollable there with no keyboard stop to reach the rest. A height-bounded table that pans on a phone again keeps its pagination footer in view and scrolls its rows inside the bound, as it did before. And while a soft keyboard is up (an open inline editor), a tap on the table's own controls (Save, Cancel, a row's checkbox) now acts on the first tap instead of only dismissing the keyboard. A table that fit never had that delay, since it had no scroller; a panning one did, and now neither does.
- 55b7d6a: An inline DescriptionList no longer runs past a narrow container. The value side of each row could not shrink, so in a sidebar card about 250px wide a mono id beside its Copy button pushed the button past the card's edge while only the term wrapped. Now a value that does not fit beside its term wraps onto its own line under the term, still at the trailing edge, so every value and every Copy button stays in one column; a value wider than even its own line ellipsizes (a copyable value) or wraps (plain text). Rows that already fit lay out exactly as before, on the web, iOS and Android.
- 0f9eab4: The docs' pre-rendered Carousel "Default index" page (/components/carousel/defaultindex) now hydrates without React error #418. Expo's static export rendered every page at a location it derived from the file name by stripping a trailing `index`, and it stripped those letters from any last segment that merely ends in "index", so that page was rendered for /components/carousel/default and shipped with no examples and a canonical link to a page that does not exist. The docs now patch Expo's CLI to strip only a whole `index` segment, `check:patches` runs the export's path function over such a name, and an e2e behavior test loads the page. Docs build only; nothing in the package changes.
- d380a3d: The docs' `check:patches` script now states that it resolves `@happy-dom/global-registrator` from the workspace root install, which CI runs before the docs install, and why the docs package must not declare it: under the docs' Metro resolution (`disableHierarchicalLookup`, bare imports from docs/node_modules only), a docs dependency's transitive packages can replace what the site's bundles resolve. Docs tooling only; nothing in the package or the site's bundles changes.
- 6fe9054: The repository gains a manual `E2E soak` workflow that replays an end-to-end spec in shard order, several passes per lane on parallel Linux runners, to reproduce a failure CI meets only rarely, and an opt-in hang probe (`E2E_HANG_PROBE_MS`, armed by the soak) that attaches the renderer's state, per-process CPU and a Chromium compositor trace to a test that is still running past the threshold. It exists to find the root cause of the glass material captures that intermittently hang in Chromium's `Page.captureScreenshot` on CI. Repository tooling only; nothing in the package changes.
- f808af7: The manual `E2E soak` workflow can now replay another commit (`ref`) and run 4, 8, 12 or 16 parallel lanes (`lanes`, default 8), and soaks of different commits run side by side. A replayed commit older than the hang probe runs without it. Repository tooling only; nothing in the package changes.
- 6d6f848: The e2e hang probe now finishes against a wedged renderer: every step is bounded and recorded, the passive samples (per-process CPU and, on Linux, each renderer and GPU thread's state, wait channel and CPU ticks) come first, a JavaScript stack is taken through the debugger's pause, and no step waits on the renderer to release a DevTools session. Repository tooling only; nothing in the package changes.
- 7407cfd: The web `link` Button dims to 0.9 under the pointer instead of Dark Factory's 0.85, so its `primary-text` label keeps 4.5:1 on the page while hovered (0.85 left it at 4.33:1 on the blush page and 4.42:1 on mint; 0.9 holds 4.80 and 4.90).
- 92b1872: The docs' third-party notices now name the exact copy of each package the site ships. The notices scan records the directory every shipped package came from, and the generator reads version and licence from there instead of looking the name up, so a dependency installed twice can no longer be reported at a version the site does not ship. The regenerated list adds mdn-data (CC0-1.0), whose CSS data css-tree compiles into the native bundles and which the scan now sees by reading Metro's own source maps. Docs tooling only; nothing in the package changes.
- 70307e2: The repository's pre-push hook now runs `notices:gen:check`, so a package.json or lockfile change that stales the docs' third-party notices is caught before a push rather than only in CI. Repository tooling only; nothing in the package changes.
- 41390cf: On Android, the controls whose ripple is clipped to their rounded shape now reach the touch area they always declared: Button, Pagination's pages, arrows and rows-per-page trigger, the pressable Steps circles, the RowMenu trigger, the CodeBlock copy chip, the Stepper buttons, the Toast action and dismiss, and the StackedList row menu reach the 48dp minimum, and so does the tappable Chip, whose touch area is now measured (see the Chip entry). The view that rounds the ripple (`RippleClip`) clips on Android, and React Native hit-tests a clipping view only inside its own bounds plus its own `hitSlop`, so every touch slop inside it was cut at the control's visible edge. `RippleClip` now takes a `hitSlop` prop, applied to its own view on every platform (inert on iOS, where the wrapper does not clip and is flattened away, and dropped on the web), and each of those controls passes it the slop its pressable carries. The prop is new public API on an existing export and ships as a patch because it completes this bug fix.

  Three more kit views cut a declared slop and no longer do: the Android Chip pill (it clips to round its surface) carries the part of its remove glyph's and a removable chip's body slop that reaches past it, the grouped Input box carries the part of the clear and eye glyphs' slop that overhangs the 36pt iOS small field, and under glass the CodeBlock no longer clips at its rounded edge (nothing it holds paints past it), so its copy chip keeps its whole touch area there too. A textless Radio in an iOS list keeps no slop, since its full-width row already meets the minimum. Nothing changes size or moves on any platform. The Dialog and AlertDialog text buttons on Android are unchanged and still short of the minimum.

- cf5164c: A Row with `span` children no longer remounts a child when a sibling before it appears or disappears, or when an item is inserted ahead of it in a keyed list. The Row keyed each span cell by its position among the rendered children, and that position shifts whenever an earlier child turns to `null` or a new item comes before it, so React remounted every cell after it and a field in one lost its text and focus. Each cell now takes its child's own key: an explicit key where the child has one, and otherwise its slot in the children as written, empty slots included. This applies side by side and, with `stacks`, while stacked.
- 49c01fd: A `Row stacks` whose children carry a `span` now keeps every child mounted when it crosses its breakpoint. It used to drop the span cells and re-key its children as it stacked, so React remounted everything inside: a field lost its text and focus when a window was resized across the breakpoint, and on a phone the whole row remounted right after hydration, when it switched from the server's desktop layout. The stacked Row keeps its cells and drops only their width, which is how Grid already behaves, so the stacked layout looks the same as before.
- d398800: The kit's small controls now carry their touch slop from their first frame on iOS and Android instead of after their first layout: Button, the Pagination cells, the pressable Steps circles, the RowMenu trigger, the CodeBlock copy chip, the Switch and the Android tappable Chip seed it from the least box their skin gives (an icon square's size, or the padding and border around one line of label), and the measurement then refines it; a control whose declared box changes (a Button turning small or gaining an icon) seeds again. A native view that hugs one of them, such as a Tooltip's own root around a Button, a component root carrying a `testID` or the CodeBlock's floating copy wrapper, records the slop at its first layout, so a tap just outside the control reaches it from the start. `useMinTargetSlop` is unchanged apart from keeping its slop when a new layout differs by less than a device pixel. Nothing changes size or moves.
- cf5164c: A `span` on a child of a `Row stacks` no longer logs the development warning that the span does nothing while the Row is stacked. The span is still a Row child's, and it applies again as soon as the Row unstacks. To tell the two cases apart, the layout-axis context a stacked Row publishes (`useLayoutAxis()`) now carries `stacked: true`. The field is optional and absent everywhere else, so existing readers of the context see no change.
- 1a756f7: DESIGN.md (Shapes) and the Button and Chip docs now state where a small control's touch area reaches on iOS and Android, in React Native's own terms: a touch area never reaches past a native ancestor that does not contain it (a container that clips stops it at its edge, and one that paints, carries a `testID` or handles pointer events admits it only as far as its last recorded layout), and where two touch areas overlap the later sibling takes the tap. The kit's own clipping views carry the touch area of the controls inside them and a component splits the gap between two of its own controls; between controls you place it does not, so leave at least twice the extra touch area between small controls in your own layouts, or split the seam in a control you build with `useMinTargetSlop`.
- 1dce4ec: A tap inside one of a component's controls no longer presses the control beside it on iOS and Android. React Native gives a point that two touch areas both cover to the later control, so a touch slop that reached into its neighbor's box took the taps aimed at it: on both platforms a tap on an Input's clear glyph could reveal the password, and on iOS a tap at the end of the value could clear it, a tap on the right edge of a tappable Chip's label removed the chip, a tap on the right edge of a Board card's drag handle opened the card's menu, and a tap just under a vertical Steps circle, over its connector, pressed nothing. Where a component places two of its own controls side by side or stacked, it now splits the gap between their touch areas, from its own padding and sizes, so they meet without overlapping: the Toast's action and dismiss, the Stepper's buttons and its value, a tappable Chip's label and its remove glyph, an Input's clear and eye glyphs and its value, a Board card's drag handle and menu trigger, and the Steps circles down a vertical rail (across a horizontal connector the margins already keep them apart). The Steps connectors and a horizontal step's label take no touches, so a tap in a circle's touch area over either presses the circle. A removable Chip's remove glyph keeps its touch area biased away from the label in a right-to-left layout too. Each touch area keeps the platform minimum on every side that faces no other control; nothing changes size or moves.

## 3.2.0

### Minor Changes

- 69383bd: Add `Video`, an atom that plays a clip with an optional poster. Without `controls` it is an inline player: the picture is one play and pause control, with a play emblem while paused and a spinner while loading. With `controls`, iOS and Android show their own player controls (AVKit and Media3) and the web shows the kit's control bar under the picture: play and pause, elapsed time, a seek slider, the length, mute and full screen. Fit is a boolean axis (`contain`, the default, `cover`, `stretch`), playback takes `autoplay` (skipped when the system asks to reduce motion), `loop` and `muted`, and the frame takes `aspectRatio` and `radius`.

  Playback runs through `expo-video`, a new OPTIONAL peer loaded like `react-native-qrcode-svg`: apps that never render `Video` need nothing new; without the peer, `Video` keeps its size and poster in a labeled frame and warns once in development.

  Minor because it adds public API: the `Video` component and its `VideoProps`, `VideoSource` and `VideoRadius` types.

### Patch Changes

- b3d25c3: Docs site: pre-rendered pages now hydrate with the same `useId` ids the server wrote. Expo's static renderer rendered the app inside the `+html` document, which shifted every id under `#root` away from the ids the client builds, and Expo's development wrapper put a toast beside the app on the development client, which shifted that client's ids away from the server's. React reported the mismatch on 13 of the 18 template pages on the dev server, and on the published site an Autocomplete's `aria-controls` named a listbox id that did not exist. The docs now patch both Expo files, a docs check fails if a later Expo upgrade leaves a patch behind, and an end-to-end test checks the hydrated ids on the export. No change to the published package.

## 3.1.0

### Minor Changes

- 697dc4c: Minor because it adds public API: `ColorTokens` gains the optional `action` and `action-foreground` roles, the call-to-action fill and its ink. `<ThemeProvider tokens={{ action }}>` now recolors the primary Button (its fill, label, loading spinner and glass puck) and the split button's action half and chevron without touching `primary`, which keeps what is selected, checked, current, linked or focused. Omitted, actions paint with `primary` exactly as before, and a `tokens={{ primary }}` rebrand still repaints actions with its primary pair. This is the seam the Dark Factory palette uses to draw actions green and selection violet.
- 0f2505e: Button gains `raised`, a new option: the page's main call to action rests on a soft glow in its own colour (Dark Factory's raised button), on every platform, for a primary button only and never while disabled.

  The web Button takes Dark Factory's look. Every intent is a pill with a 1px border (none on the link): the call to action is the green `action` pill with an 800 label and Dark Factory's tracking, `destructive` its red sibling, `secondary` Dark Factory's violet outline (its label in `primary-text`, which holds 4.5:1 where Dark Factory's violet does not), `outline` its hairline button, `ghost` borderless and `link` bare with no underline. Small and base are Dark Factory's 29 and 36 px, large steps up to 40. Hovering washes the outline looks at once and dims a link; a primary still rises 1 px. A disabled web button shows Dark Factory's disabled look (transparent, a hairline, a muted label) instead of fading, and a loading one keeps its intent's look. Under glass only the filled web buttons are pucks, since the outline looks paint no surface. The iOS and Android buttons keep their platform shapes and colours.

- 5b3b39a: Adds `selection` to Checkbox, and makes a one-setting Checkbox the platform switch on iOS and Android. A Checkbox that turns one thing on or off (no `selection`, no `indeterminate`) now renders the iOS or Material 3 switch on those platforms, with the same props, value and callbacks, since neither platform uses a checkbox for a single setting; it stays a checkbox on the web. A Checkbox that picks items from a list takes the new `selection` prop (a select-all with `indeterminate` implies it) and stays a checkbox everywhere: the edit-mode selection circle on iOS (a 22 pt ring, the brand fill with a white check or dash when selected), the Material 3 box on Android and the box on the web. DataTable's bulk selection passes `selection`. On iOS and Android the accessibility role follows what renders: a one-setting Checkbox reports `switch` there. Minor: new public API (the `selection` prop). Stated assumption: the switch substitution is the owner's idiom decision (a new look and role on native, with no prop or value change), so it ships as a minor rather than a major; a consumer whose native tests query a one-setting Checkbox by the `checkbox` role adds `selection` or queries `switch`.
- c4f3221: Minor because it adds public API: `shadow(level, tokens?)` takes the active tokens and tints the shade by the palette's `shade` role (the call without tokens keeps working and uses the light palette's).

  On the web the ladder is now Dark Factory's: each level is cast straight down with a negative spread, so it pools under a surface's lower edge instead of haloing it (sm is DF's tile, the default its card, md its hovered card, lg its popover, xl its dialog, a black top-layer shade on every palette). On iOS and Android each level keeps its platform geometry, retinted from the ink to the palette's shade. Every component now passes its tokens, so shadows follow the palette and the scheme, and the `--shadow-*` properties spell the ladder over `var(--shade)`.

- 44d5a8d: Minor because it adds public API: `ColorTokens` gains optional translucent roles, with matching `--*` custom properties in `styles/canvas.css`: the soft washes `primary-soft`, `success-soft`, `warning-soft` and `destructive-soft`, the text-field fill `field-fill`, the modal backdrop `scrim`, the shadow tint `shade`, and the inverse surface pair `inverse` and `inverse-foreground`. Every modal backdrop (Dialog, AlertDialog, ActionSheet, Drawer) now dims the page with `scrim` when the theme carries it, and keeps its former black dim when a custom token map omits it.

  The default colors are now Dark Factory's, in light and dark, on every platform: a lavender-tinted page with white cards and indigo-gray ink in light, deep indigo surfaces in dark, `primary` in Dark Factory's violet (selection, links, focus) and `action` in its green (the primary Button and the split button's action). Each role is a Dark Factory value or a recorded rule over one (`tools/darkfactory/derive-tokens.ts`), solved to the kit's contrast floors where the raw value falls short; `bun run check-df` replaces the Riskora Figma parity check. The eight chart series are new, anchored on Dark Factory's hues and ordered so every neighbouring pair stays distinct for colorblind readers. Avatar initials take the better of white and near-black on their identity fill, so they keep 4.5:1 on the new series.

  `brandColors` and the `--orb-*` custom properties are deprecated: nothing in the kit reads them, and they keep their former values until a major removes them.

- 14d3d69: Minor because it adds public API: `shape` gains a `tile` corner role (12 on every platform, `--radius-tile` in the hand-off) for KPI cards, stat tiles and icon tiles, tighter than a content card.

  The web corners are now Dark Factory's: 8 on rectangular controls (icon buttons, menu rows, nav highlights, pagination tiles), 10 on fields, 12 on menus and popovers, 14 on cards, 18 on dialogs and toasts, 22 on sheets and drawers. iOS and Android keep their platform corners. `--radius-control-ios` now names the pill it always was in the skins (it read 10px), and the Skeleton card placeholder follows the card corner.

- 2cd6f93: Adds `hover`, Dark Factory's translucent hover wash, as a color role: the brand violet at 8% (10% in dark) that a hovered row or quiet control washes with, in every palette (`rgba(123, 108, 240, 0.08)` blush, `rgba(63, 127, 224, 0.08)` mint, `rgba(164, 150, 255, 0.1)` dark), as `ColorTokens.hover` and `--hover` in the CSS hand-off. It is translucent, so it reads on any surface and over glass; `accent` stays the same wash composited on `card`, and a token map without the role washes with `accent`. Minor: new public API (a color role and its custom property).
- b1cde5e: Adds Dark Factory's mint palette as a theming option. `<ThemeProvider mint>` paints the light scheme in mint (blush stays the default, and `dark` still wins, since Dark Factory has one dark palette); `ssrPalette` holds a server-rendered palette through hydration the way `ssrScheme` holds the scheme; `useTheme()` reports `palette`; `mintColors` and `colorsFor(palette, scheme)` expose the token sets; and on the web `setPalette("mint")` / `getPalette()` switch `data-palette="mint"`, whose block in the CSS hand-off (`styles/tokens/colors.css`, included by `styles/canvas.css`) carries the mint tokens and mint's frost tints; the block never matches in a dark context, on the root or on any wrapper, and the accessibility fallbacks now also reach palette and scheme wrappers inside a glass root. Minor: new public API (a ThemeProvider option, a server-rendering prop, two web helpers, a token set and its resolver).

  A new optional color role, `inverse-primary`, is the brand on the inverse surface. The Android snackbar now paints Dark Factory's toast pill (`inverse`, with `inverse-foreground` text) in both schemes, and its action takes `inverse-primary`, the active palette's own brand hue lightened to 4.5:1 on the pill, solid and under glass, instead of the other scheme's brand text. An app that recoloured the snackbar through `foreground` and `background` overrides now sets the three inverse roles instead; token maps without them keep the old inverted bar.

- c2667b6: Adds `statusColors(tokens, tone)`, the status colors every toned surface reads from the theme's roles: for `success`, `warning`, `error`, `info` and `neutral` it returns the `ink` that names the tone in text (4.5:1 or better over its wash and on a plain surface, in every palette), the translucent `wash` behind a toned pill or panel, and the solid `dot`. `info` rides the primary family, as Dark Factory's info is its violet accent, and a token map without the soft roles gets Dark Factory's wash alphas. Badge and Alert now read it and take Dark Factory's look on every platform: Badge's tones are Dark Factory's pills (the quiet surface pill by default, the solid call-to-action pill for `default`, a hairline pill for `outline`, the soft red pill for `destructive`, and for `status` the live-state pill whose leading dot carries the tone beside a foreground label), and a toned Alert is the soft panel (the tone's wash, its ink on the title, the foreground on the body, its solid color on the icon), with Dark Factory's 12px corner and dense type; the Icon's `success` reads the success role. Minor: new public API (`statusColors`, `StatusColors`, `StatusColorTone`).

### Patch Changes

- 6937b1f: Avatar's initials fallback is Dark Factory's identity disc on every platform: the name resolves to one of Dark Factory's ten stage hues through a stable hash, and the disc is that hue's 135 degree blend from a pale tint to a deeper, warmer neighbour (the same colours Dark Factory computes, bit for bit), drawn with react-native-svg so iOS, Android and the web paint one gradient. The initials are bold (800) at about a third of the disc (10px on `tiny`, 11px on `small`) in near-black, which holds 4.5:1 on both ends of every blend where Dark Factory's white initials measure about 1.8 to 2:1. Like a photo, the disc is identity content: it keeps its colours under glass, and only the neutral tile of an avatar with no name or initials takes the control material. The iOS and Android skins now alias the web one (no platform ships an avatar control), so the rounded square takes the 8px control corner everywhere, while the press feedback stays each platform's own (the Android ripple, the iOS and web dim). The docs gain an Identity colours example, and the stack examples pass full names so their discs differ.
- 4a5fb19: The Avatar documentation no longer says the initials are white: they take whichever of white or near-black reads better on the identity color, which is what the component draws since the Dark Factory palette landed.
- eecb765: AvatarMenu's trigger is Dark Factory's identity pill on every platform: bare at rest (no pane under glass), the `accent` fill under the pointer and while its menu is open, a 28 px disc in the viewer's violet glow ring, the name at 12 px bold over the email in Dark Factory's muted micro type, and a chevron that no longer turns. The menu it opens stays each platform's own Dropdown; on the web it stands off by Dark Factory's 8 px. Under glass a disabled pill's ink dims by each platform's own disabled dim.
- ef84b7f: Re-mints the Linux visual baselines for the identity-disc Avatar (and the pages that show it), the charts' status colours, and the FILL RadioGroup. No change to the package's behaviour.
- 380daf5: Re-mint the Linux visual baselines for the Dark Factory buttons, segmented control and pager.
- dc3fd21: Re-mints the Linux visual baselines for the Checkbox page, whose iOS and Android previews now draw the platform switch for a one-setting Checkbox, and for the FilterPanel page, whose iOS option rows now draw the edit-mode selection circle. No change to the package's behaviour.
- 4cd7d2d: Re-mints the Linux visual baselines for the Dark Factory Chip, Progress and Swatch: the Progress page's slim green meters, the Swatch page's tile corners, and the dark Chip page. No change to the package's behaviour.
- fa80d1c: The visual regression baselines now show the Dark Factory palette in Manrope. Test fixtures only; the package itself does not change.
- 05eae4d: Re-mint the Linux visual baselines for the Dark Factory menus: the web Dropdown, RowMenu and Listbox rows, the Popover panel and the Tooltip bubble, in light and dark. Test-only; nothing in the package changes.
- 19c6904: The visual regression baselines now show Dark Factory's palette-tinted elevation. Test fixtures only; the package itself does not change.
- d099410: Re-mints the Linux visual baselines for the open Select and Autocomplete's focus border and for the platform parts the docs' iOS and Android previews now draw (Dialog, AlertDialog, ActionSheet, Dropdown, Popover, Tooltip, Navbars, DragDrop). No change to the package's behaviour.
- 48f92a3: Re-mints the Linux visual baselines for the Dark Factory quiet atoms and status colors: the Badge and Alert pages, Command's key caps, and the Board, FilterPanel and GridList pages, whose badges are now Dark Factory's pills. No change to the package's behaviour.
- 4155f84: Re-mints the Linux visual baselines for the Radio page, whose iOS preview now draws the checkmark list, and for the Listbox and FilterPanel pages, whose iOS previews now mark a choice with a trailing check. No change to the package's behaviour.
- 7382563: The visual regression baselines now show Dark Factory's type scale and web corners. Test fixtures only; the package itself does not change.
- 4d3dce7: Brand-tinted glass pucks now solve their tint for the ink the component actually paints on them (the call-to-action, primary, destructive, success or warning pair), not the stronger of black and white. The two agree on today's palette, so nothing renders differently; they diverge on a brand color where the skin's ink is not the stronger one, which the Dark Factory violet is.
- 2a8d653: The web ButtonGroup is Dark Factory's segmented control: the options sit in a pill track (the `secondary` surface with a hairline, a 3px inset and a 2px gap) and the selected one rides a white thumb on Dark Factory's segment shadow, with bold labels in the foreground or the muted ink. The split button is the web Button's green call-to-action pill with its chevron half, and the stepper and the spaced peers are hairline pills the height of a Button beside them. Under glass the split is now its call to action's brand-tinted glass on every platform, as a primary Button is, instead of violet text on plain glass. The iOS and Android groups keep their platform shapes.
- 98e2ab8: The Dark Factory vendor script now defaults to the repository's new location, `../Argus` (it moved from `../Dark Factory`), and the repo instructions name that path.
- 2a984bd: The repo instructions record the Dark Factory design direction (tokens, web look, where native shapes stay, type, one job with a different control per platform, web frost, docs page look, examples, hover lifts), and AGENTS.md now carries the same text as CLAUDE.md, kept equal by a test. Documentation only; no component changes.
- 2301c7f: Records the Dark Factory reference cards (page look, frost material, hover lifts, avatar) that the coming Dark Factory restyle is tuned against. Tooling and evidence only; no component changes.
- c378f0a: Vendors Dark Factory's theme source (palettes, type scale, radii, shadows, materials, hue ramp) into `tools/darkfactory/theme.json` with `bun run df:vendor`, and pins the palette facts the coming token mapping relies on. Tooling only; no component changes.
- 28b5418: Typography's roles take Dark Factory's dense type scale: bold titles stepping from the display 24 down to the heading 14 (`h1` 20, `h2` 17, `h3` 16, `h4` 15, `h5` 14), body copy at 12.5 medium, `lead` at 14 medium, `small` and `muted` at 11.5 semibold, `tiny` at 11 semibold, and `caption` as Dark Factory's uppercase eyebrow at 10 bold. `code` and `mono` sit at 11.5 and 12 in Geist Mono. Every line height is a whole pixel, so text lays out the same on Android, iOS and the web. The `--role-*` custom properties carry the same values. The props are unchanged; only the sizes and weights they resolve to differ, so screens that use Typography render smaller and bolder titles.
- b181855: Web glass is now Dark Factory's plain frost on every browser: each surface paints its layer's tint over a 24px backdrop blur with no saturation shift, edged by a 1px inset hairline, with no refraction and no specular highlight. The Chromium lens (the SVG displacement filter) is removed, so glass renders the same in Chrome, Safari and Firefox. Text-entry fields and other clear surfaces draw an unblurred translucent fill, as Dark Factory's inputs do. The web tints are Dark Factory's (a white 0.52 shell in light, a near-clear shell in dark, denser content and menu panes); iOS Liquid Glass and the Android blur keep their material and tints, and `glassByScheme` keeps its values as the native set. In the CSS hand-off `--glass-lens` and `--glass-illumination` become deprecated aliases and `--surface-backdrop` is `none`.
- 1f37f10: Chip takes Dark Factory's look on the web and iOS (iOS ships no chip control, so its skin is the web skin): the neutral chip is Dark Factory's quiet borderless pill with an 11px bold label, a coloured chip is its soft pill (the status tones from `statusColors`, so a success chip matches a success Badge and Alert; a free palette hue at Dark Factory's soft alpha under its deep label, in solid and glass alike), `primary` is the primary color's soft pill instead of a fixed indigo, and a selected filter chip is the solid primary. Android keeps its Material 3 chip, recolored: the outlined idle chip and the tonal selected filter chip with its checkmark. `HUE_WASH` is deprecated: the kit no longer paints with it. No prop changes.
- 209a9ca: Progress, Emblem and Swatch take Dark Factory's look. A progress bar is a meter, so it fills with the call-to-action `action` color on every platform (Dark Factory fills its meters green), its `warning` and `danger` tones read their colors from `statusColors`, and the web bar is Dark Factory's slim meter (4, 6 or 8px, on its line color, in its dense label type); iOS and Android keep their bars, recolored. Emblem washes each tone with its soft role from `statusColors` (its monogram in the tone's text-grade ink) and takes Dark Factory's 10/12/14px tile corners, and Swatch the same corners; both render the web look on iOS and Android, which ship no such control. A `tokens` override that repaints `primary`, `success`, `warning` or `destructive` now repaints that tone's soft wash too, at Dark Factory's alpha for the scheme, unless the override sets the soft role itself. No prop changes.
- 2f1768c: The quiet atoms take Dark Factory's look: a labeled Divider sets its label in Dark Factory's eyebrow (10 px, bold, uppercase, wide tracking, muted), Kbd becomes Dark Factory's compact key cap (20 px, its 6 px key corner, a 10.5 px bold label), and the Skeleton's button placeholder is a pill like Dark Factory's buttons. The same look renders on iOS and Android, which have no control of their own for these. No prop changes.
- 9268cec: The docs dev server no longer runs out of memory after 50 to 100 pages. Every document it renders re-evaluates the web server bundle, and expo-updates, imported by the hidden diagnostics route, subscribed at evaluation time to a module object Expo keeps process-wide, so each render left a listener that pinned the whole previous bundle (about 20 MB). The diagnostics route now reads the update identity through a native-only module, so expo-updates stays out of the web bundles, and a docs test keeps it there. No change to the package's behaviour.
- bf239b1: Keep two end-to-end checks true after the Dark Factory buttons: the Navbar docs page now fits at 768, so its known-overflow record is gone, and the scrolled-overlay check scrolls the harness page itself instead of relying on it being taller than the viewport above its trigger.
- f70755d: Every field shows a keyboard focus state. The web Autocomplete field turns its border `ring` while focused as well as while its list is open (and stays so after Escape closes the list over a still-focused field); the web Stepper box turns its border `ring` while its value field holds focus, and a bare iOS or Android Stepper field keeps the themed focus ring; the Command search row's rule turns `ring` and thickens while the search field holds focus; the web Select trigger turns its border `ring` while its list is open, as Dark Factory's select does; a flush Textarea keeps the themed ring inside itself. Under Increase Contrast a border that shows a state (a focused or errored field, an open trigger, a focused slider knob) now keeps its colour instead of being overwritten by the contrasting hairline.
- f64b08b: The material end-to-end checks now recognize a text field's clear glass well (Dark Factory's unblurred tint and hairline) as the field's own material. Test fixtures only; the package itself does not change.
- ff8f019: Brings Dark Factory's hover feedback to the web. A pressable Card (one with `onPress`) rises 2 px over 180 ms while its resting shade deepens to the raised one in step, and settles back when the pointer leaves (a `raised` or `flat` card rises with its shade unchanged); a primary Button rises 1 px over 150 ms, never while disabled or loading; a Sidebar row's `hover` wash fades in over 150 ms in the rail, the collapsed rail and the drill-down, beneath the row, so its pressed and active fills still switch at once. While it lifts and settles, a card stacks above its neighbours (and lifts its Grid or Row span cell above theirs), so its deeper shade falls over the card below instead of under it, as Dark Factory's does. The browser runs each transition, so nothing re-renders per frame; hover is read on the control's wrapper, which never moves, so a lifted surface cannot slide out from under a resting pointer; a touch never hovers; Reduce Motion makes every change instant. iOS and Android keep their platform feedback (React Native delivers no pointer hover there by default). `RippleClip` forwards `onPointerEnter` and `onPointerLeave`, and `useReducedMotion` now shares one subscription among all its callers, which also stops react-native-web's reduced-motion listeners from leaking.
- e4d8ce2: The interaction evidence inventory no longer lists the removed Backdrop organism, so the CI registry check passes again and releases resume. No component changes.
- 753d247: On iOS, Listbox and FilterPanel mark a choice the way an iOS list does: every chosen Listbox row carries a trailing check in the brand color, in single and multi select alike, and is no longer filled for being chosen; a FilterPanel option row puts its label first, its count next and a trailing check on a chosen filter. The web and Android keep their leading marks (a checkmark and a filled row, or the platform's selection checkbox). Select, PhoneInput and the other pop-up menus keep the leading check, which is where an iOS menu puts it. No prop or value changes.
- a3465da: Listbox rows take Dark Factory's menu row on every platform: bold 12.5 px labels at an 8 px corner, 2 px apart, a muted micro second line, the bordered list in Dark Factory's panel corner, and the hover wash on the web. The chosen single-select row now reads in the selection violet (its label in `primary-text`, the checkmark in `primary`) with no fill, where it used to be filled; iOS keeps its trailing check with a plain label, and multi-select keeps each platform's selection checkbox.
- 0eeb352: The brand sans face is now Dark Factory's Manrope: `typeface.sans` names it and the web hand-off's `--font-sans` stack leads with it. Geist Mono stays the code face. An app registers Manrope 400 to 800 as static faces (`@expo-google-fonts/manrope`) and hands them to `<ThemeProvider fonts>`; the kit still ships no font files and still renders in the system face when no fonts are passed. The type scale itself is unchanged in this release.
- 9c2b27c: The scrolled-overlay e2e check scrolls the materials harness page by setting its scroller's `scrollTop`. It called the scroller's `scrollTo({ top })`, but react-native-web replaces that method with its ScrollView's `scrollTo({ x, y, animated })`, so the call animated the page back to the top and the check's precondition failed in CI. Test-only; nothing in the package changes.
- a138915: The glass material host and its runtime are split per platform: Android gets its own files (the canvas-blur capture and expo-blur frost), the web keeps the CSS frost and the Chromium lens, and each platform resolves its own glass tints. Nothing renders differently; an Android app no longer bundles the web lens (the Android Button fixture drops from 8.7 KB to 7.1 KB gzip).
- 9a25237: The web Dropdown and RowMenu take Dark Factory's menu: a card of bold 12.5 px rows 2 px apart at an 8 px corner that take the hover wash at once, an uppercase eyebrow over a section, the shortcut as a muted caption, 14 px icons, an 8 px standoff from the trigger, and a disabled row in the muted ink instead of a dim. The RowMenu trigger on the web is Dark Factory's plain icon button (a 28 px square with a muted glyph and the hover wash). iOS and Android keep their platform menus; a disabled RowMenu row now dims to each platform's own value (0.4 on iOS, 0.38 on Android) and the iOS RowMenu takes the kit's iOS menu corner. RowMenu's root now hugs its trigger through the kit's sizing instead of a fixed `alignSelf`. Anchored overlays with no fixed card width (menus, option lists) now stay inside their outlet: a card that would cross an edge shifts back by the width it rendered at, and one that fits stays where it was anchored.
- c95a293: Follow-ups to the Dark Factory menus. An anchored card too wide for the room left by its trigger is pinned by its far edge in one layout, instead of stepping sideways a frame at a time on the web. The RowMenu trigger keeps its own size, hover area and menu width inside parents that are not kit layout containers, such as a table cell. Every hover wash falls back to `accent` for a theme that omits the optional `hover` role. The AvatarMenu pill's hover is the web's only, as native pointer hover waits on the owner, and under glass its glow ring draws no opaque gap.
- 14fcc24: The responsive e2e suite's record of the Navbar page's known overflow at tablet width is re-measured for the Manrope face (124px, up from 106px). The defect itself, a topbar link row that does not collapse, is unchanged and still recorded.
- d643bc0: Document how typefaces pass through nested providers. The Theming page gains a "Typefaces and nested providers" section: register the faces once on the root provider's `fonts`, a nested `ThemeProvider` keeps them, and its scheme, palette, surface and tokens stay its own (pass the same `tokens` constant to carry a rebrand in, and both `dark={dark}` and `light={!dark}`, with `dark` from `useTheme()`, to follow the parent's scheme). The README's Theming list documents `fonts` and says a nested provider resolves only its own `tokens`, the Typography page says the faces hold under nested providers, and a docs search for fonts, typefaces or nested providers now reaches the Theming page. The Theming page's palette previews drop the `fonts` prop they repeated, since they now inherit the docs' faces.
- 8f0649e: A nested `ThemeProvider` that does not pass `fonts` now keeps the nearest parent provider's registered typefaces, instead of resetting every kit label under it, and every overlay it opens, to the platform's system face. The faces are an app-level registration (what the app loaded), so a provider nested inside the root one, or inside portaled content, paints in them too. An explicit `fonts` still wins, and it replaces the parent's map whole (the roles are not merged). `tokens`, the scheme, the palette and the surface stay per provider, as before: a nested provider is how a subtree shows its own palette or brand, so pass the same `tokens` constant again to carry a rebrand into one.

  This changes documented nested-provider behavior: the `fonts` prop said that omitting it renders the kit in the system face, and a nested provider that omitted it did exactly that. It ships as a patch on the stated assumption that no consumer relies on that reset (no known consumer passes `fonts` today). To keep a subtree in the system face on purpose, pass it an empty map, `fonts={{}}`, held in a module constant so the theme value stays stable.

- 5fa80c8: The docs' open source licences page treats `@ionizeio/canvas-blur` as first-party, like the kit itself: it is published from this repository, so it is named in the Canvas paragraph instead of the third-party list.
- 78ef4e9: Pagination takes Dark Factory's pager on every platform. The page numbers are bare pills at the Button's heights, the current page is the violet pill, and Previous and Next are hairline circles around chevron icons (the text glyphs are gone, and the arrows follow the reading direction). The rows-per-page trigger is a hairline pill with a chevron, the truncation gap an ellipsis icon, a resting cell takes the instant hover wash on the web, and a disabled pager shows Dark Factory's muted look instead of fading. Neither iOS nor Android ships a pagination control, so the native skins are the web skin, with each platform's own press feedback. Under glass only the current page paints a surface (a brand puck); the other pages and the hairline arrows stay bare. The with-size variant now reads the "Showing X-Y of N" range when `itemCount` is set, as its prop documents. The ButtonGroup split button's Don't example is redrawn in the green call-to-action look.
- e411eec: Every shell now takes the components it draws with that look different per platform as parts from its platform entry files, instead of importing their web builds: Dialog (Button, Input), AlertDialog (Button, Input, which Android now passes too), ActionSheet, Dropdown, Popover, Tooltip and GeoMap (Button), Navbar (Button and Avatar beside its Dropdown), Calendar (ButtonGroup), FilterPanel (its checkbox, Badge, Button and the narrow window's Drawer), Sidebar (Drawer and the row Badge) and MetricBreakdown (Chip). A device already resolved them by platform, so nothing changes there; the docs' iOS and Android previews now show those pieces in their own look. DescriptionList narrows its two-column term label when the list itself is phone-narrow, measured, instead of when the window is, so it narrows in a narrow panel on a desktop and not in a wide container on a phone. A new test holds the seams, and the docs' platform registry check shares its definition of a component that looks different per platform.
- 7f8f2e2: Popover and Tooltip take Dark Factory's type. The Popover heading and description are Dark Factory's heading over its body in the muted ink on every platform; on Android, which has no popover, the panel is now the web's Dark Factory card with its hairline and popover shadow, and iOS keeps the iPad popover with its beak. The Tooltip keeps its inverse bubble (dark in the light scheme, light in the dark one, since Dark Factory has no tooltip and its one-colour toast pill would all but vanish on the dark page) with its label in Dark Factory's 12 px label weight; iOS, which has no tooltip, shows the web bubble, and Android keeps the Material 3 plain tooltip.
- 8316a0d: RadioGroup takes the FILL sizing nature on every platform, as its iOS list section already did: it fills its parent's width, so a `row` group wraps its options inside it instead of growing to their combined width and running past its container (the Radio page's card example scrolled a phone-width page sideways). A two-column DescriptionList's value now shrinks and wraps beside its Update link, so a long id or email no longer runs past a narrow card on the web or natively. The docs' Do and Don't frames lay each example out in a definite-width column, the Playground's layout, so paragraphs and wrapping rows fit a phone; a Don't that demonstrates overflow is clipped at its frame, and a new phone-width pass keeps every docs page from scrolling sideways at 390px.
- e07d5ee: Radio on iOS is now a checkmark list, since iOS has no radio button: each option's label leads and the chosen option carries a trailing check in the brand color, and a RadioGroup's plain options form one inset-grouped section (44 pt rows, 16 pt insets, an inset hairline between two rows, the 26 pt continuous corner of the kit's iOS lists) that fills its parent and is a content-layer pane under glass. `card` options stay tiles and mark the choice with the same trailing check, and on iOS the list stays vertical when the group asks for a `row`. The web and Android keep the ring and dot, and the role is `radio` everywhere. RadioGroup is now built per platform from the Radio skin (it is exported from the same entry as Radio, with no change to its props), and the docs three-up shows its iOS and Android builds.
- 05cc5f0: The CSS hand-off's corner radii now match the components on every platform. Several `--p-*` radius properties had drifted from the skins they transcribe: on iOS the select, autocomplete and one-time-code fields now read 8 (the iOS field radius), the badge, tooltip, action-sheet card top and sidebar toggle declare their own iOS corners, and the swatch no longer overrides the web corners; on the web the segmented control, accordion card, drag handle, Tabs and TabBar capsules take the values the components draw; on Android the accordion card, command palette, data table, Tabs and TabBar declare the corners they previously inherited from the web.
- a2f2057: The liquid glass motion is gone. Every selection control (Tabs, TabBar, Sidebar, Navbar, Pagination, Calendar, Carousel, Switch, Slider and the ButtonGroup segment) paints its selected state in place: a static control-layer pane under glass (the brand puck for the Pagination page and the Calendar day, the skin's tint elsewhere) and the skin's own fill in solid mode, with no travelling surface. Every anchored popup (Dropdown and through it AvatarMenu and the collapsed Navbar menu, Select, Autocomplete, PhoneInput, Popover, RowMenu, Command, the split ButtonGroup, the Calendar peek and hover card) opens as a dense or functional glass card beside a trigger that stays painted; there is no droplet, no hand-off, no cover and no spring-back, and the Autocomplete's query echo is gone with them. Dialog, AlertDialog and the anchored cards appear in place: `Entrance` is a readiness hold with no spring and no longer takes `anchor` or `anchorBottom`; `AnchoredOverlay` no longer takes the internal `handoff` prop. The Backdrop organism (`Backdrop`, `Backdrop.Particles`, `Backdrop.Gradient`, `Backdrop.Shader`, `Backdrop.Custom`, `BackdropHost`) is removed together with the `@shopify/react-native-skia` optional peer it probed for; the owner ruled this non-breaking, as the kit has no consumers of it, and it ships under this patch. `GlassSurface` no longer takes a `hostRef`. Functional motion stays: the Spinner, the Skeleton shimmer, the indeterminate Progress sweep and the InputOTP caret on the loop primitive, the floating labels, the Accordion, Collapsible and Reveal transitions, the Drawer and ActionSheet slides and the Sidebar drill-down. Why: react-native-web's Animated JS driver commits through React every frame and React never expires a Suspense retry, so the sidebar pill's spring held every component page's body back 1.3 s per click; the animations were judged not worth that and removed rather than re-engineered. The whole-kit bundle shrinks from about 212 KB to 204 KB gzip, a Button consumer from 10.3 KB to 8.7 KB.
- ba644ea: Remove a stray copy of an old platform CSS hand-off (`ebp`) from the repository root. It was never part of the published package.
- 148b27d: The last status colors move onto the theme's roles through `statusColors`: every menu (the web Dropdown and every RowMenu, which painted a fixed palette red) draws a destructive row in the `destructive-text` role like the native Dropdowns already did, an ActionPanel's danger title takes the same error ink, a Stats or breakdown delta reads in the success color or the error ink, and the charts' status marks (the success and destructive bars and series, the waterfall's rises and falls, the status strip, the Gauge, ProgressRing and Sparkline tones) take the solid status colors, with MetricBreakdown's rate readout in the tone's ink. The CSS hand-off's `--p-menu-destructive`, `--panel-danger`, `--delta-up` and `--delta-down` now reference those roles instead of holding fixed values, so they follow the palette and any override. No prop changes.
- 9ac77b3: The text-entry end-to-end checks hold every web field under glass to Dark Factory's clear well (its own tint and hairline, no blur) instead of the removed lens filter. Test fixtures only; the package itself does not change.
- 19dbdca: Keyboard focus rings take the palette's `ring` colour and sit 2px off the control: the kit's `Pressable` hands the colour and offset to the browser's own ring (Chromium paints it; Firefox and Safari keep their own ring colour unless the page loads `styles/canvas.css`, whose new `:focus-visible` rule draws a solid 2px `--ring` ring everywhere). Controls that hid the ring without painting a focus state of their own now show it: the Accordion and Collapsible headers (drawn inside the header when the group sits in a card or the iOS inset group), the Calendar's day cells, chevrons and event blocks, the iOS-skin Pagination, Tabs, Navbar and Sidebar rows, and the drag handle.
- 94ba6d8: The web and iOS Toast take Dark Factory's toast pill: the deep `inverse` fill in every palette and scheme, a pill with no description (a long message wraps inside the capsule) and the sheet corner once a description sits under the message, a bold 12.5 px message in the pill's ink, and Dark Factory's toast shadow. The description, action and dismiss take the inks the Android snackbar uses on the same fill, and the intent glyphs the dark scheme's status inks, so every part reads on the pill; iOS keeps its 44 pt touch targets. Android keeps the Material 3 snackbar. `toast({ destructive: true })` from `useToast()` now shows the destructive intent; the provider used to drop it.

## 3.0.2

### Patch Changes

- 42ed633: Under the glass hand-off every anchored popup now rests OVER its trigger the way the iOS 26 menu rests over its button: the trigger vanishes whole into a compact droplet hanging under its box, the pane blooms up out of it over the spot the trigger held and settles there, its near edge on the trigger's near edge, and the trigger stays hidden until the close hands back. The droplet's seed is now the hanging drop exactly (the pane travels by its centre through it), on every owner: Dropdown and through it AvatarMenu and the collapsed Navbar menu, RowMenu, the triggered Popover, the split ButtonGroup, Command in trigger mode, and the field popups Autocomplete, Select and PhoneInput. The Autocomplete echoes the field's line at the top of its list (the query, the value or the placeholder, and the chevron) so what you type stays in view while the list covers the field; the editor keeps focus underneath, a press on the echo refocuses it and its chevron closes the list. A field's close keeps its deflation onto the box (its near edge held, the pane pinching to the droplet and re-widening into the box) and the field returns whole at the snap; a pill's close is unchanged. The open-state border of a covered field is not painted while the list rests over it. Solid mode, Reduce Motion and the inline fallback keep the gap under the trigger, where the trigger stays visible beside its popup.
- 06e5761: The liquid popup's dismiss now springs back up into its button the way the iOS 26 menu's does, checked on the real Dropdown docs page against the recorded reference: the pane's body flies back to the pill by its centre on a soft spring at the reference's pace (a quarter of the way at +99, half at +132, home by +300) instead of collapsing under the button and hopping onto it, narrowing to a drop that sinks into the pill's underside and vanishes there; the pill re-forms under the drop as a short fat oval on the travel itself (its step and its scale flip on the same native frame, so iOS no longer shows the pill whole for a frame), from a mark above the seed that only a close reads (a `closing` flag on the hand-off channel); its label fades back at full size over the pill while the last of the drop is still settling; and the dismissed rows retire as legible blurred ghosts through the pane's contraction. The other menu triggers (AvatarMenu, the Navbar hamburger, RowMenu, Popover, the split ButtonGroup, Command) take the same close; the field popups keep their absorb onto the box, now on the same pace.

## 3.0.1

### Patch Changes

- fabbaef: Bring the liquid popup's opening and dismiss onto the iOS 26 menu's frames, checked on the real docs pages against the recorded reference: the droplet hangs from the trigger's far edge with the pill's tone and a whole rim, its rows blurred and sharpening on their own clock (the blur is now a CSS filter string, which react-native-web applies; the object list was dropped on the web), the rows scale outside the scrollport so the first row is no longer clipped at birth, the pane grows as a capsule and relaxes to its corner only once it is full, and a dismiss ghosts the rows on its first frame, contracts the pane about its centre, goes sheer, narrows to a drop hanging under the trigger, re-forms the trigger's material as a small growing body above it and slides the drop onto the box before the label fades back over the merged glass. A Button, an Avatar or an AvatarMenu capsule that is the pill now fades its own foreground, so the owner's fader never sits over its material.

## 3.0.0

### Major Changes

- 7454886: A component never dictates its own width: the input-like controls are FILL and the
  parent layout container provides the bounds.

  Breaking. Input, Textarea, Select, Autocomplete, Listbox, Slider, and Progress no longer
  render at a fixed 320px (240 `narrow`, 480 `wide`); they fill the parent they are given
  (`width: 100%` plus the row-sharing pair, `src/style/sizing.ts`). The `block`, `narrow`,
  `wide`, and `fit` props are removed from those seven components, `useFieldWidth`,
  `FieldWidthProps`, and the `fieldWidths` tokens (`--field-*` in the CSS hand-off) are
  gone, and their `style` prop is now `LayoutStyle`: `ViewStyle` without `width`,
  `minWidth`, `maxWidth`, `flex`, `flexBasis`, `flexGrow`, `flexShrink`, and `alignSelf`,
  so a width shim at the call site is a type error. Form's two-column threshold moves from
  the 480px field width to the `lg` step of the width scale (512).

  Migration:

  - A bare field that used to be 320 wide now fills its column. Where the old measure was
    the point, wrap the field in a Container step: `<Container xs>` is the old default
    (320), `<Container lg>` the old `wide` (512 for 480), and `<Container sm>` (384) or a
    Row `span={n}` cover the rest. `narrow` (240) has no step: give the field a Row span.
  - Drop `block`: filling the container is now the only behaviour.
  - `fit` (a Select hugging its value) is a bare `<Column>` inside a `<Row>` (Bootstrap
    `.col-auto`): `<Row><Column><Select … /></Column></Row>`.
  - `style={{ maxWidth: … }}` / `style={{ width: … }}` on a field: move the bound to the
    parent (`Container`, `Column span`) and delete the style.
  - A field inside a bare Column inside a Row collapses to its content (it always did; the
    fixed width hid it). Give that Column `span={n}` or `fill`; `useFillStyle` warns in
    development when it sees the case.

  Also in this release: every non-layout component's `style` prop is `LayoutStyle` (the
  sizing keys are a type error; Row, Column, Grid, Container, the shells, and the floating
  overlays keep `StyleProp<ViewStyle>` because they are the bounds providers); Card, Feed,
  StackedList, ActionPanel, Skeleton, Sparkline, every SVG chart root, and the Calendar
  containers are FILL with no cap of their own (Alert loses `narrow`/`wide`/`block`,
  Sparkline its intrinsic 120px, the Calendar timelines their desktop widths); Skeleton
  text lines take `long` / `short` instead of a width; the width scale gains the `xxxs`
  (192) and `xxs` (256) tile steps; and the docs generator rejects `width` / `maxWidth` /
  `minWidth` in a `style` on any non-layout tag, so the showcase composes bounds with
  `Container`, Row `span`, and `Grid`.

  `Container` conforms to its parent by default: no cap and full width unless a step is
  named (`page` is now an explicit step, not the default). MediaObject, DescriptionList,
  EmptyState, Collapsible, Accordion, Form, Field, Stats, and DataTable are FILL too, and
  the docs examples render every component at the width its parent gives it; a Container
  step appears in a fence only where the measure is the lesson.

### Minor Changes

- 8998a7e: Add the clear material option to GlassSurface and GlassPane, preserving edge refraction with a lighter tint and minimal blur. This new reusable material capability justifies a minor release.

  Use the clear grade for web Input, Textarea, Autocomplete, InputOTP, Stepper, PhoneInput, and DataTable editors in glass mode. Preserve native field materials, solid and accessibility fallbacks, and normal focus, selection, and text editing. Fields have no click ripple or typing animation.

  Paint grouped focus and error borders above the refractive material so they stay crisp, and align flush Textarea material corners with its editor.

- 86416cb: `DataTable` takes `attached`: the header band squares its corners to a frame the parent draws.

  Minor because it adds a public option. On the web skin the header is Riskora's soft
  10px-cornered band, which is the look of a table standing on its own (the design floats
  it inside a padded card). A table flush inside a frame, a `flush` Card or a bordered panel
  that clips to its corners, now passes `attached`, and the band drops its own corners so
  the frame's clipped corners are the only rounded ones; before, the frame's fill peeked
  out under the band's bottom corners. `bordered` squares the band the same way for the
  table's own outline. iOS and Android bands were square already, so nothing changes there.

  ```tsx
  <Card flat flush style={{ overflow: "hidden" }}>
    <DataTable attached columns={columns} rows={rows} />
  </Card>
  ```

  The docs use it on every prop table, and the docs playground is now a fully rounded
  card sitting a gap above its source block in every example and at every simulated tier,
  instead of squaring its bottom edge into the code block.

- 71216c7: Draw the iOS field family to the iOS input-field reference, and give Input the
  password toggle and the clear button.

  Minor because it adds public capability: `Input` gains `passwordToggle` (a trailing
  eye that reveals and re-masks a `secureTextEntry` value, announced as "Show
  password" / "Hide password"), `clearable` (a trailing circled-x that empties the
  field while it holds text, in both the controlled and uncontrolled mode), and its
  `icon` now accepts ANY Canvas glyph name (`IconName`: phone, calendar, creditCard,
  mapPin, ... instead of six hard-coded names); `SelectOption` gains `leading`, a short
  glyph (a flag emoji, a currency sign) shown before the label in the trigger and in
  the option rows; and the token set gains `field-border`.

  The iOS skins of Input, Textarea, Select, Autocomplete, InputOTP and Field now follow
  the "iOS Mobile Input Fields" design (light and dark): a white `card` box, an 8pt
  corner (`shape.ios.field` is now 8, was 10), a 16pt value, a 14pt regular
  `muted-foreground` title 8 above the box, a 20px leading glyph, a boxed muted prefix
  or suffix addon with a divider (was inline affix text), a gray ▾ select caret (was the
  brand ⇅), and three states: focus tints the border, the glyph and the caret to the
  brand `ring`/`primary`; error tints the border and the glyph `destructive` and washes
  the box with a red-50 fill (`fieldErrorFill`, derived from `card` + `destructive`);
  rest paints the new `field-border` hairline.

  DISCLOSED ACCESSIBILITY TRADE-OFF. `field-border` (gray-300 `#d1d5db` light, systemGray4
  `#3a3a3c` dark) is about 1.5:1 against the field's own fill, BELOW the 3:1 WCAG 1.4.11
  boundary that the `input` token is held to. It is read only for the RESTING state of
  the iOS field skins (through `fieldBorder` in `src/style/field-colors.ts`, which falls
  back to `input` for a token map that omits it); the web and Android skins keep `input`,
  and the focus and error borders keep their full-strength tokens on every platform. The
  choice was made deliberately on 2026-09-16 so the iOS fields read as the iOS reference.

- 97b7c18: Layer the glass model: under glass EVERY surface renders through the material, each
  on the layer it belongs to, and the option lists, alert dialogs, toasts and tooltips
  take the densest tint instead of opting out.

  Minor because it adds public capability: `GlassSurface` gains `layer` ("functional"
  | "content" | "control" | "dense", the under-fill it paints beneath the material) and
  `brand` (a brand-tinted puck: the colour as the under-fill, solved by `brandTint` to
  stay as sheer as its ink's WCAG 4.5:1 allows, or the GlassView's own `tintColor` on
  iOS 26); the kit exports `GlassPane` (the material as a sibling BEHIND a node that
  owns its own interaction or semantics, with `paneStyle` and `PANE_SIBLING_INPUT`),
  `innerFill` / `withInnerFill` / `isGlass` / `inverseDenseTint` (`src/style/glass-fill`),
  the WCAG helpers `channelsOf`, `composite`, `relativeLuminance`, `contrastRatio` and
  `inkOn` (`src/style/color`), `HUE_WASH`, and three glass tokens beside `glass-tint`:
  `glass-tint-content`, `glass-tint-control` and `glass-tint-dense` (`--glass-tint-*` in
  the CSS hand-off). `AnchoredOverlay` gains `dense`; `opaque` remains for a consumer
  that wants the plain box and wins when both are passed.

  What a glass app looks like now, by layer:

  - FUNCTIONAL (sheer): Navbar, TabBar, Sidebar, Dialog, ActionSheet, Popover,
    Command, the calendar peek, a Tabs track, and the Drawer panel, which was the one
    overlay that painted an opaque card before.
  - CONTENT (denser, legible first): Card, DataTable, the lists, feeds, stats,
    description lists, grid-list tiles, board columns, calendars, code blocks,
    carousels, alerts, empty states, every chart, the bordered FilterPanel, the Skeleton
    card. A selected Card and a toned Alert pass their tint.
  - CONTROL (the bright puck): the field boxes (Input, Textarea, Select, Autocomplete,
    PhoneInput, Stepper, InputOTP), Button, Tabs pills, Pagination cells, Chip, Badge,
    Kbd, Switch tracks, Checkbox boxes, Radio rings and cards, Progress rails, Steps
    circles. A brand fill (a primary or destructive Button, a checked Switch or
    Checkbox, a selected tab, page, day or step) is brand-tinted glass with the intent's
    foreground on top; a hue wash (a status Badge, a coloured Chip) is the hue's mid
    step with the label one step deeper (800 in light, 300 in dark) so every palette
    hue holds 4.5:1 over the page, a content pane and a control puck; a control with no
    surface of its own (a ghost or link Button) stays bare.
  - DENSE (the densest tint): Dropdown, Select, Autocomplete, RowMenu, the SplitButton
    overflow, the PhoneInput country list, AvatarMenu, AlertDialog, Toast, Tooltip and
    the chart value flag. The inverse ones (the Tooltip bubble, the M3 snackbar) tint
    with the ink so their inverse text keeps its contrast.

  Fills inside a surface (a hovered or selected row, a header band, a stripe, a code
  pill, a Skeleton placeholder) become ink tints under glass; a state border (a focus
  ring, an error edge, an open trigger) stays over the pane while the resting hairline
  drops and the material's rim is the edge. Solid mode is untouched: every one of these
  renders the same tree it did before, and under Reduce Transparency or Increase
  Contrast every layer degrades to its opaque token.

  The named-import size budgets move (Button 8,704B, Input 39,936B, DataTable 46,080B
  gzip): a control is a glass surface now, so importing one carries the material stack
  a consumer used to pay for only with an overlay or a bar.

- 8f85af0: Add the `static` material option to shared GlassSurface and GlassPane primitives, separating a surface's material role from its readability density. Static surfaces use frost and functional surfaces use the available native liquid material or browser lens.

  Preserve live child state, focus, caret and layout when switching glass and solid. Resolve unsupported native targets and accessibility preferences to the complete solid skin, coordinate foreground and state fills with actual material capability, and release browser lens definitions when their last owner stops using glass.

  The minor release adds the public static material capability without changing component APIs or platform defaults.

- 7bfa999: The measure axis on components: the Container steps, on the field or button itself.

  Minor because it adds public API. `MeasureProps` (`src/style/sizing.ts`) is the boolean
  step axis `Container` already reads (`xxxs` 192, `xxs` 256, `xs` 320, `sm` 384, `md` 448,
  `lg` 512, `xl` 576, `xxl` 672, `xxxl` 768, `wide` 896, `wider` 1024, `widest` 1152, `page`
  1280, plus `start`), and eleven components now carry it: Input, Textarea, Select,
  Autocomplete, Listbox, Slider, Progress, Field, Form, Button, and ButtonGroup. So a short
  field or a call-to-action names its own measure without a wrapper:

  ```tsx
  <Autocomplete xs start options={people} />
  <Button md>Continue</Button>
  ```

  A step is not a width of the component's own. It is the FILL nature capped at that step
  of the shared width scale (`maxWidth`), so below the step the component still fills the
  parent it is given, exactly as `<Container xs>` around it would; without a step nothing
  changes (a field fills its parent, a button hugs its label). The grammar and the
  precedence are Container's: narrowest step wins when several are passed, a step centers
  the box in its column, `start` pins it to the leading edge. Two rules the component
  adds because it is not a layout container: inside a Row only the cap applies (there
  `alignSelf` is the cross axis and would pin a field to the top of the row, so the Row's
  own alignment places the box), and on Button and ButtonGroup a step wins over `block`
  (the segmented and spaced kinds flex their segments to equal shares under a step as they
  do under `block`; split and stepper ignore both with the same dev-only warning).
  `ContainerProps` extends `MeasureProps`, `stepOf` is the shared precedence, and
  `useFillStyle` / `useSizing` take the component's props so a new adopter is one line.

  Also: the design hand-off parity records for the field widths the layout tier removed
  (`narrow`, `wide`, `block` on Input, Select, Textarea, Autocomplete, Slider, and Alert),
  which the check had been failing on.

- 8a0f3c2: Add explicit `OverlayProvider viewport`, `viewportInsets`, and `separateWindow` options for bounded nested panels, measured header/footer occlusions, and custom native Modal windows. This minor adds public hosting capabilities: content-sized hosts inherit measured window bounds, while native-window hosts start a separate coordinate boundary.

  Fit anchored cards above or below their triggers and scroll long content inside the available height. Track resized Android root hosts and iOS keyboard frames, preserve trigger hierarchy while hosted menus open, and exclude touch-dismiss backdrops from keyboard and screen-reader focus. Keep option-list refs and keyboard navigation intact. Preserve Command's search and footer while scrolling results, fit its width to narrow hosts, and focus cards after placement is committed.

  Keep pinned-open cards attached to offscreen triggers. Aim the solid iOS Popover pointer using the rendered card width and trigger center, reserve its protrusion, and clamp wide trigger-matched cards to their host.

- e8e30f1: Add an optional accessibilityLabel to Select and Command so applications can name their purpose independently of visible labels or search prompts. Associate each control with its named result list. Select announces required fields without applying unsupported required metadata to a button. Command preserves the unhighlighted state and never points assistive technology at a nonexistent active option.

  This minor release adds the public accessibilityLabel option to both controls.

- 8f85af0: Add the optional Android capture renderer @ionizeio/canvas-blur and the Canvas integration for Android 12 or newer. This minor release adds a native material capability without requiring the new package for existing Canvas consumers. The module starts at 0.1.0 and uses Expo SDK 57 native APIs.
- c78c2b7: Run every looping animation on the native driver on iOS and Android, and add the cycle shapers that make that possible.

  New public API: `thereAndBack(easing)`, `holdThen(hold, easing)` and `keyframes(points)` in the motion helpers turn one `Animated.timing` into an out-and-back pulse, a hold followed by a sweep, or a keyframed schedule, so a loop can be a single native timing (React Native refuses an `Animated.sequence` inside a native loop and `Animated.delay` hardcodes the JS driver).

  The Backdrop clock, Spinner, the indeterminate Progress sweep, the Skeleton shimmer and the InputOTP caret now pass `useNativeDriver: supportsNativeDriver` (native off-thread, the JS driver on web). Under the New Architecture a JS-driven frame is a Fabric shadow-tree commit per animated view whose cost scales with the whole tree, so the JS-driven Backdrop alone saturated the JS thread of an idle screen (150% CPU and rAF near 3 frames per second on the iPhone 17 Pro simulator); natively driven it idles at a few percent with rAF at 60 frames per second. The Backdrop clock resumes a stopped flight from its wall-clock phase, since a natively driven value cannot report its position to JS. The channel shapes (a linear flight, an out-and-back twinkle and breath, a parked-then-sweep event) are unchanged.

- e629aaa: Add `PhoneInput`, a phone number field with a country segment.

  Minor because it ships a new component. `PhoneInput` is the Input's grouped box with a
  country segment at its start (the chosen country's flag and a caret that open a list of
  countries showing their flag, name and dial code) and that country's dial code inline
  before the number, which is typed on the phone keypad. The number (`value` /
  `defaultValue` / `onChangeText`) and the country (`country` / `defaultCountry` /
  `onCountryChange`, ISO alpha-2 codes) are each controlled or self-managed; the segment
  picks from the kit's curated, alphabetical `PHONE_COUNTRIES` list (66 countries with their
  ITU dial codes, exported with `flagOf` for the emoji flag of a code) or from a `countries`
  list of your own; `label`, `required`, `error`, `disabled`, `readOnly`, `small`/`large`
  and the measure axis work as on Input, and `Field` delegates its label, required mark and
  error into it. Each platform draws it with its own Input and Select skins (the box and
  the list), so on iOS it is the iOS input-field reference's phone field: the white box, a
  flag-and-caret segment whose divider takes the field's state colour, the dial code in
  the placeholder gray.

- 18ec75f: Add the optional `primary-text` color token so consumers can customize brand text separately from primary fills and their foreground labels. This new theme customization capability justifies the minor release. Built-in primary text now stays readable on the kit's neutral and layered tonal surfaces in both schemes, including Android selected tab pills and today's date within a Web Calendar range. Primary fills, standalone icon accents and filled-control foregrounds retain their colors.

  Existing complete `ColorTokens` literals remain valid. `ThemeProvider` preserves primary-only rebrands by using that override for text unless `primary-text` is also supplied. Custom brand contrast remains the consumer's responsibility. Raw CSS handoff rebrands must also set `--primary-text`; `--primary-text: var(--primary)` restores their previous text-color behavior. Nested providers retain their existing independent token and scheme resolution.

  Correct the Web ActionSheet CSS action and Cancel label aliases to match the existing neutral foreground used by its RN skin.

  The default text contrast checks cover solid and tonal surfaces. Glass materials depend on the content behind them and need verification in the rendered app.

- 1fc75ba: Rebrand the kit's foundation to the Riskora Dashboard UI Kit (Figma file
  `YLbmaRirWTzivAzXirDTmX`): the colour tokens, the type scale, the elevation ladder, and a
  new shape token set. This is the first of the phases that restyle the web look to that kit;
  iOS and Android keep their HIG / Material 3 shapes and take only the brand.

  Minor because it adds public API:

  - `ThemeProvider` gains a `fonts` prop (`ThemeFonts`: a `sans` and a `mono` entry, each one
    family name or a map from weight to the face registered for it), and the kit's `Text` and
    `TextInput` primitives now apply the registered face to every kit label. Omit it and the kit
    renders in the system face as before. `typeface` names the brand faces (Urbanist, Geist Mono);
    `resolveFontFace` / `fontStyle` are exported for custom text nodes.
  - `shape` (`ShapeTokens` per `PlatformKey`): the corner radii a platform's skins share
    (`control`, `field`, `card`, `dialog`, `menu`, `sheet`, `checkbox`, `pill`), mirrored as
    `--radius-field`, `--radius-dialog`, `--radius-menu`, `--radius-sheet`, `--radius-checkbox`
    in `styles/tokens/radius.css`. The web column is the Riskora shape (12 / 12 / 20 / 16 / 16 /
    30 / 6 / pill).

  Token changes (both schemes, CSS and JS): a sky/400 `primary` (`#3da3f5` light, `#68cdff`
  dark) whose label is the dark ink in both schemes (white-on-sky is 2.7:1), a charcoal-and-white
  neutral family on one hue (page `#f8fafe` / `#111213`, card `#ffffff` / `#18191c`, ink
  `#0d121b` / `#ffffff`, panel `#f6f7f8` / `#212327`), red/700 / green/800 / orange/800 status
  fills that carry white text, a re-seeded chart series (sky first, the bar-highlight orange
  second), and the sky family on the brand orbs (the `orb-*` keys are unchanged). Every pair is
  solved to the kit's contrast floors where the source falls short; `tools/figma/riskora-variables.json`
  vendors the source variables and the new `bun run check-figma` gate fails on drift or on a
  token without provenance. The Claude-Design render-parity leg (`check-render`,
  `tools/render-parity/`) is retired in its favour.

  Type: the Typography roles are the Riskora ladder in Urbanist, titles at the regular weight
  (display 64/70, h1 55/64, h2 40/48, h3 36/44, h4 28/36, h5 20/30, lead 20/30, body 16/24,
  small 14/20, tiny 12/16, caption 12/16 medium uppercase), and the `fontSize` scale gains a
  `7xl` step. Elevation: an ambient ladder in the ink with no offset (`0 0 20px` at 6% for the
  standard shade, `0 1px 2px` at 4% for `sm`), mirrored in `styles/tokens/shadows.css` and the
  `--p-*` transcriptions.

- 5a23745: Add a runnable independent Expo starter for web, iOS, and Android with published-package installation, workspace editing, controlled input selection, session preferences, and native overlay composition. This minor adds the user-visible starter capability and its setup guide without changing existing component APIs.
- af0419f: `Tabs` takes `wrap`: a row longer than its container lays out on further lines inside
  one track instead of panning in the overflow scroller, so every tab is on screen at once.

  Minor because it adds a public option. The capsule track (iOS and web) squares its
  corners off to the radius concentric with its pills when it wraps, since a capsule's
  9999 on a track taller than one pill would round its ends into semicircles across the
  corner pills; the Material 3 pills track does the same, and its underline tabs stack
  their lines on the one divider. `block` never overflows and wins over `wrap`; a
  `responsive` vertical rail honors `wrap` once it flattens. The docs example rail wraps
  at phone and tablet widths now, where the scroller used to show only the first few
  example labels with no scrollbar to say the rest were there.

- a8376cb: Add typed public React refs to Button, Select, Checkbox, Switch, Radio, and Slider. This minor release adds a public capability: consumers can access each control's interactive React Native host for focus, blur, and measurement, using object or callback refs. Select retains overlay measurement and Radio retains group focus navigation. Native focus delegates to the host and remains separate from accessibility focus. Document the actual inferred ref types in component API tables.

  Fix Space activation for Checkbox, Switch, and Radio on web, including key release, disabled and composition guards, and cancellation when focus leaves the control. Preserve Enter and pointer activation without duplicate callback ownership.

- 411410a: Add the layout tier that gives every component its width from its parent: a shared
  width scale, the sizing natures, and (in the same release) the `Container` atom and
  Row/Column twelfth spans.

  Minor because it adds public API: the `widths` token scale (`xs` 320 .. `page` 1280,
  Tailwind's `max-w` values copied by hand, no dependency) mirrored as `--width-*` in the
  CSS hand-off, and `src/style/sizing.ts` with the two sizing natures every component
  root now declares: `FILL` (`width: 100%` plus `flexShrink: 1` and `minWidth: 0`, so a
  field fills a Column, shares a Row with a hugging button, and splits a Row equally with
  another fill sibling) and HUG (`useHugStyle`, resolved against the nearest kit layout
  container because Yoga ignores `fit-content` against a stretching parent and a bare
  `alignSelf: flex-start` breaks cross-axis centering in Rows). `LayoutAxisProvider` /
  `useLayoutAxis` publish a container's axis, whether it stretches, and whether it is a
  content-sized cell; `useFillStyle` warns in development when a fill component sits in a
  bare Column inside a Row (the one layout that still collapses `width: 100%`). The
  `LayoutStyle` type (ViewStyle without the sizing keys) is the `style` a non-layout
  component accepts.

  The `Container` atom is the bounds provider (Bootstrap `.container` / `.container-fluid`):
  it spans its parent, caps at one step of the width scale (`xs` .. `page`, default `page`,
  `fluid` for no cap), centers itself (`start` pins it to the leading edge), and takes
  horizontal gutters from Row and Column's pad scale. Row children take `span={1..12}`
  (Bootstrap `.col-n`): the Row measures its own width and hands each spanning child a px
  cell with the gaps in the arithmetic (the DashboardGrid twelfths math, now shared as
  `spanWidth`), span rows wrap past twelve, and `stacks` ignores spans once stacked. Row,
  Column, Container, Grid cells, and DashboardGrid cells publish the layout-axis context.

### Patch Changes

- 5d74464: Retain full axe rule, node and check details in accessibility test attachments so failures show invalid attributes and measured contrast alongside concise summaries.
- fb0fbfd: Correct native smoke appearance validation to preserve Android's automatic mode
  and both writable custom subtypes. A shared parser rejects ambiguous or malformed
  output before device mutation, preventing an attempt with an unrestorable value.
  This is a tooling correction; product theme behavior is unchanged.
- 7b17d7a: Fit hosted overlays above the Android keyboard in full-screen edge-to-edge windows, where adjustResize leaves the root layout unchanged. Intersect native keyboard coordinates with measured bounds so legacy resized windows retain their existing geometry without subtracting the keyboard twice.
- 2b834a0: Preserve bounded passive Android CI host evidence around native smoke failures,
  including ADB executable identities, actual listener/process identities, resource
  samples and existing daemon-log tails. Capture failure state before appearance
  restoration while retaining the original test error, exit code or signal.

  This is a tooling observability correction. It does not change ADB lifecycle,
  trace settings, tool versions, build order or native test commands, and it does
  not retry failures or claim that the underlying transport disconnect is fixed.

- 005c8b8: Request native accessibility focus on the surviving Autocomplete input when an accessibility activation immediately closes its suggestions. Preserve text editing and keyboard state, and cancel stale requests after controlled-open refusal, disabling, detachment, reopening, or navigation. Add focused lifecycle and single-activation tests plus native acceptance scenarios.
- a22ca22: `Backdrop.Custom` layers can read the surface box they are laid out against:
  `useBackdropBox()` returns the surface's measured width and height and the scene's
  focus point (null outside a surface). The engine's own particle and gradient layers
  were already laid out against that box; a custom layer could only read the window,
  which put a scene's bespoke art off the visible area whenever the surface was smaller
  than the screen (a documentation stage, a card, a harness column). No change for scenes
  that keep reading the window.
- b25de07: Run the Backdrop and the Skeleton shimmer as compositor CSS animations on the web.
  react-native-web has no native animated module, so a looping `Animated.View` there
  re-renders through React on every animation frame: the docs sky cost one React commit
  and about forty inline style writes per frame at idle, enough to saturate the main
  thread in an unthrottled browser and starve a Suspense retry. A new loop primitive
  (`createLoopChannel`, `LoopView`) binds opacity and transform to a shared periodic
  channel through `inputRange` / `outputRange` tracks with a phase offset, rendering the
  natively driven interpolation graph on iOS and Android and a react-native-web
  `animationKeyframes` animation on the web, with the phase carried in the animation
  delay so a remounted surface continues mid-flight. The Backdrop clock's channels are
  now `LoopChannel`s (bind bespoke `Backdrop.Custom` art through `LoopView` rather than
  through `Animated.interpolate`), the SVG renderer nests twinkle buckets inside their
  layer, and the Skeleton shares one shimmer channel across every placeholder on screen.
- 98043d0: Validate focused keyboard journeys in Chromium, Firefox, and WebKit, plus mobile touch selection and nested Drawer menus in Chromium and WebKit, against the same docs artifact used by release validation.
- 4c97f02: Give the glass ButtonGroup selection a droplet stretch, squash, and settling wobble when switching segments. Keep native materials intact with React Native layout animation and snap to the selected segment under Reduce Motion.
- ef537ee: Make all ButtonGroup variants follow the Liquid Glass theme using Canvas's shared
  platform material. Segmented selection moves with a measured React Native spring,
  while split, stepper and spaced groups gain glass surfaces. Preserve native iOS
  and Android rendering, solid skins, press feedback and accessibility fallbacks.
- 62b844b: Expose selected Calendar day buttons as pressed on web while retaining the native selected trait, including date-range endpoints.
- 0773edd: Wait for the measured Carousel scrollport before checking real keyboard Tab entry in the browser regression test. Preserve all focus, paging, geometry and accessibility assertions. This corrects the test's initialization precondition without changing component behavior.
- 8f85af0: Resolve chart frames as stable glass with complete opaque capability and
  accessibility fallbacks, including the shared LineChart, AreaChart and
  ComposedChart frame. Preserve plain and intrinsic frameless charts, share dense
  inspection material with Heatmap, and keep MetricBreakdown's latest-value
  caption outside its plot so it needs no opaque patch over the data.
- f672ff9: Remove the repaired component accessibility exceptions. Every component page now uses the same serious/critical violation gate, with raw diagnostic evidence retained for failures.
- 72d6d66: Align field material regression coverage with the clear tint and complete synthetic pointer event lifecycles so text selection and press feedback remain isolated across the test suite.
- 9c78e8b: Form the hand-off droplet as a compact drop centred on the trigger, never wider than about half the card: a field that fills its column now vanishes into a drop the way the iOS 26 menu's button does, instead of a bar the field's full width, and a close re-widens the drop into the trigger's box before handing back.
- dd37f0b: Stabilize compiler verification by separating cold TypeScript setup from prop assertions and checking native watch rebuilds in the same process boundary used by development.
- 3a1561f: Correct the color stylesheet's provenance comment to reference the maintained native tokens and contrast contract.
- 8f85af0: Dismiss overlay Dialog and AlertDialog presentations with Android Back through their cancellation policy, preserving nested overlay ordering and contained catalogue panels. Give the shared material lifecycle fixture a viewport overlay host so scrolled dialogs stay visible and same-window glass has a safe capture target.
- 1c23758: Generalize internal liquid selection motion to both axes and add an explicitly activated popup material lifecycle. Preserve foreground hosts, focus readiness, nested dismissal ownership and reopening order. Fix the existing ButtonGroup solid-to-glass measurement gap. Public popup components retain their existing activation policy in this foundation release.
- 4188cf9: Make native docs preview links apply their explicit light/dark and solid/glass
  choices when the app is already running. Seed native launch appearance from the
  actual incoming URL, while preserving manual theme choices during ordinary
  in-app navigation and when an external link omits an appearance axis.
  Keep native status-bar icons legible through Expo's app-wide status-bar API,
  matching the docs' existing native configuration on both iOS and Android.
- 64dc87e: Restore accessible heading levels throughout the shared documentation layouts, including component references, guides, patterns, templates, and the home page. Give the component catalog its own visible page heading while preserving the existing heading typography.
- 4188cf9: Keep native documentation header titles readable when the preview appearance differs from the device appearance.
- 3397d37: Give the docs' animated background broader color bands through the content area
  and richer colors so glass surfaces have visible detail to blur and refract.
  Retain the same three animation layers, timing, hero and accessibility fallbacks.
- 64dc87e: Focus the documentation search input when its native Modal is shown, preserving the opener as the return target when search closes. Expose the desktop search trigger as a button without changing its appearance.
- 64dc87e: Expose the mobile documentation header and primary tabs as banner and navigation landmarks. Enforce document heading, landmark, and scroll-region accessibility separately from component WCAG checks, and remove the obsolete global scroll-focus exception.
- 0c998d1: Replace the docs' site-wide lattice background with quiet spectral currents while
  preserving the existing homepage hero. Shared SVG artwork follows the Backdrop
  clock through LoopView on web, iOS and Android, with independent ribbon motion,
  light and dark palettes, and accessibility fallbacks. Add a tuning fixture for
  the new scene and retain the lattice as a separate assembly demonstration.
- 9f9d68a: Split the docs web export: one chunk per route and one per component's examples and
  prop tables, so a component page ships its own docs instead of every component's. A
  hosted Backdrop ships only its floor in server markup; the star field lands after
  hydration instead of being drawn at zero size into every pre-rendered page.
- 01f23a4: Pre-render every docs page (the web export is static) so a page paints its content
  before its bundle runs, with the bundle fetched early and executed after the first
  contentful paint. Each page carries its own title and canonical link. Two kit changes
  make server rendering faithful: the glass material resolves to frost for a server
  render and the hydration render (the Chromium lens lands in the commit after), and a
  hosted Backdrop paints its surface inline in server markup until the host takes the
  claim after hydration, so a pre-rendered page ships its floor.
- a4ad0ec: The docs ship their seven Urbanist and Geist Mono faces cut down to the glyphs the
  docs can show, about 140 KB over the wire for all seven against 245 KB, since every
  one of them is preloaded ahead of the first paint.
- aa39bbd: Dropdown-class triggers hand off to their menu under glass: the outline button, the AvatarMenu capsule and the collapsed Navbar hamburger give their glass pill to the menu's material as it blooms on the pill's frame, and the menu shrinks back to re-form the pill before the label fades in on close, the way the iOS 26 menu behind a toolbar button opens and dismisses. One mechanism on the native Liquid Glass, the web lens and the frost; the field popups (Select, Autocomplete) keep their field visible and never hand off.
- 4256da8: Hand the Autocomplete, Select and PhoneInput fields' glass to their lists the way the Dropdown hands its pill to its menu: under glass the list pours out of the field's edge in the field's width, corner and tone, and on close absorbs back into the field's box, the field's text yielding only while the pane covers it, so a field that is typed into never vanishes. Solid mode and Reduce Motion keep the plain tree.
- 97965af: Correct focused and error floating-label contrast across the Material filled field family. Focused labels use the existing primary-text role; error labels and destructive action text use a separate destructive-text role while fills, indicators, icons and destructive-foreground remain unchanged. Android Textarea now uses the same opaque muted field surface as Input, Select and Autocomplete.

  The authored error-text colors protect the actual enabled capsule and pressed surfaces, including iOS ActionSheet's translucent neutral row and group opacity, beyond the four ordinary neutral surfaces. Fixed-red menus retain their independence from semantic theme overrides, with a darker existing palette step for light text.

  The optional token preserves old complete ColorTokens literals. A React Native destructive-only theme override keeps its existing text color; an explicit destructive-text overrides text independently. Raw CSS rebrands must also set --destructive-text; setting --destructive-text: var(--destructive) restores their legacy text rendering. This is an accessibility correction to existing components and states, released as a patch.

- 3bcc3ec: Make overflowing CodeBlock and DataTable content reachable by keyboard using the native ScrollView focus props. Add a tab stop only while content overflows, preserving ordinary arrow-key scrolling, table semantics, native touch scrolling, and code wrapping. Restore native and web heading semantics to token-reference page titles and sections.
- 8f85af0: Apply press and disabled feedback to control labels and icons while keeping their material stable. Preserve platform ripple, solid-mode feedback, semantic hosts and focused foreground state. Carry exact nested theme and accessibility settings through overlay portals without changing their safe native capture targets.
- de19a8a: Make overflowing Carousel viewports keyboard focusable and support Arrow, Home, and End navigation without taking keys from slide inputs. Give picker buttons valid current-slide semantics, visible focus and real platform-sized targets. Avoid duplicate requests for the current slide and silently clamp selection when items are removed. Preserve legacy custom-skin typing.
- 96f846d: Improve light glass message contrast using the existing popover foreground color for Dialog's built-in description and currency prefix, the web ActionSheet title and message, and the Android ActionSheet message. Preserve solid and dark styling, stronger title colors, the iOS ActionSheet's translucent foreground message, and each skin's typography, layout, actions and material.

  Use the default body text role in the custom Dialog examples and allow the three-action example to wrap on narrow screens.

- cc178b5: Grid tiles are equal-height and the grid spans its parent.

  The Grid root now carries the FILL nature (`width:"100%"`, sharing a Row with
  hugging siblings), so it reaches its parent's edges instead of measuring
  whatever width its own cells produced from the pre-measurement guess in a
  centering parent. Its cells stretch to the height of the row they wrapped
  onto, and a cell publishes the new `bounded` layout-axis fact
  (`GRID_CELL_AXIS` in `src/style/sizing.ts`), which a Card reads to grow to the
  row's height without `grow`: a row of cards shares a flush bottom edge however
  unevenly their content runs, CSS Grid's default `align-items: stretch`. A
  `GridItem` fills its cell the same way, so a wide hero card matches its
  neighbours. DashboardGrid cells publish the same fact, so a Card widget fills
  its tile without being asked; a field, a chart, or a hug component keeps its
  own height in either grid. Card's explicit `grow` is unchanged elsewhere.

- 98043d0: Track component interaction evidence by test declaration and input method, detect stale or missing registry entries, and distinguish browser checks from unrecorded native and screen reader verification.
- b43114e: Published under the `@ionizeio` scope. The GitHub org was renamed from
  `nannier-com` to `ionizeio` on 2026-09-15; the npm scope follows the same
  rename. `@nannier-com/canvas` is being deprecated in favor of this package.
- 34e2557: Preserve passive iOS native smoke build evidence that binds the embedded Hermes
  bundle to its pre-Hermes source and records the recognized compiled testing-route
  shape. Retain exact input/configuration hashes and unavailable observations without
  changing native build or navigation behavior. This is a tooling correction.
- 0edec27: Move the Calendar's selected day as one measured liquid surface under glass: it travels between the days of one month through the grid and along the week strip (a diagonal move stretches along both axes), while the numbers, event dots, today's tint and the pressed state stay fixed; a month, view, density or cell-size change re-measures and resets it in place instead of travelling between months. In `range` mode the start and end are independent surfaces (the start travels on a restart, the end appears in place on completion and withdraws on a restart, a one-day range keeps both on one cell). The day peek and the hover card open and close on the liquid popup material and stay mounted through their exits. Reduce Motion selects the final bounds at once; solid mode keeps the skin's own filled day.
- 18a79f1: Move the Carousel's active dot mark as one measured liquid marker under glass: it travels between the dots with stretch, recoil and settle, following the committed slide from a dot press, the arrows, the keyboard, a controlled `index` or a finished swipe, while the dots, their press targets and the slides stay still. The marker is ink like the dots and keeps the skin's own active dot size, a loop back to the first slide travels along the strip, a slide-set change resets it in place, and it is absent wherever the dot strip is. Reduce Motion selects the final bounds at once; solid mode keeps the static dots.
- 26481dc: Document the Switch's `toggle` and the Slider's `drag` liquid profiles in their entries (what moves, what stays fixed, Reduce Motion, disabled and solid behavior), and correct the Drawer, Toast and Listbox skin comments that still described their surfaces as never taking the glass material now that the layered model renders them through the functional, dense and content layers.
- 558af6e: Add real-frame motion recipes for existing glass controls, separate from reduced-motion visual baselines, with resource-lifecycle observations and native evidence guidance.
- d0faebf: Move the Navbar's active link fill as one measured liquid surface under glass: it travels between links with restrained stretch, recoil and settle while the labels, `aria-current`, focus and hit targets stay fixed; iOS carries its brand capsule as glass and keeps its inactive capsules, web and Android carry their tinted tile, a collapse retires the surface until the row measures again, Reduce Motion selects the final bounds at once and solid mode keeps the skin's own active tile. The material inventory names the delivered liquid profiles (moving-selection, liquid-popup) and requires their motion evidence.
- 6d5fc0e: Move the numbered Pagination's selected page puck as one measured liquid surface under glass: it travels between page cells with horizontal stretch, recoil and settle while the numbers, chevrons, `aria-current`, focus and hit targets stay fixed. Cells are tracked by page number, and a window shift re-measures them before the puck moves: it travels when the page it sits on kept its frame and resets in place when that page moved or left the window, so a shift never invents travel from a stale slot. The compact and with-size variants keep their anatomy, Reduce Motion selects the final bounds at once and solid mode keeps the skin's own selected cell.
- 865be6f: Open and close Dropdown and Select menus with the liquid popup material under glass: the menu grows out of the trigger's edge with a bounded contour overshoot, recoils and settles to the fitted shape, and stays visible briefly on close while its rows are already inert and out of the accessibility tree; selection and open state commit immediately, reopening during the exit continues from the retained material, Reduce Motion settles at once and solid mode keeps the ordinary entrance. AvatarMenu and the collapsed Navbar menu inherit the behavior from Dropdown.
- 977f38a: Open and close the remaining popups with the liquid popup material under glass: the triggered Popover, RowMenu (and Board's card menus), the split ButtonGroup menu, Autocomplete's suggestion list, PhoneInput's country list and the triggered Command palette grow out of their anchor edge, recoil and settle, and stay visible briefly on close while their content is already inert. Inline Popover cards and the bare Command palette stay static. A split ButtonGroup disabled while its menu is open now closes it, and a PhoneInput that becomes disabled or read-only while its country list is open closes the list.
- ca1c1bd: The moving glass selections whose surface is a brand puck (numbered Pagination,
  Calendar days, the Navbar's iOS brand skin) now keep their label readable through
  the flight: the label ink follows the travelling surface instead of switching to
  `primary-foreground` at the press, so a newly selected number stays in its resting
  ink until the puck is under it and the label the puck leaves takes its resting ink
  back as the puck departs. A target's own resting tile (the web page cell's border,
  the iOS neutral link capsule) dissolves under the arriving surface and re-forms
  behind it rather than popping. This is a shared measured-selection contract
  (`ink` / `uncovered` / `SelectionText`), so any future moving brand-puck selection
  inherits it.

  The brand fill of a glass puck now paints over the frost and lens material rather
  than beneath it, so the colour the contrast solver chose is the colour that
  renders: a settled `primary-foreground` label on a brand puck holds >= 4.5:1 on
  both schemes, fixing the too-dark settled puck the frost produced on the dark
  scheme (Calendar days and Pagination pages). Layer tints and explicit tints are
  unchanged, and iOS 26's native Liquid Glass path (the brand as its GlassView
  tintColor) is unchanged.

- 9da8d06: Move the glass selection of Tabs and TabBar as one measured surface with directional stretch, recoil and settle: filled Tabs selections (pills, the iOS segmented track, surfaced vertical rails) travel between triggers while labels, badges, focus and tap targets stay fixed, and the web and Android TabBar indicator travels between icon positions with restrained deformation; iOS keeps its tint-only TabBar selection. Reduce Motion selects the final bounds immediately and solid mode keeps the platform treatment. Adds the isolated liquid geometry probe to the materials fixture and the native motion evidence log.
- 3fe773a: Move the Sidebar's active row fill as one measured liquid surface under glass: it travels between rows, across sections and through the scroll body with vertical stretch, recoil and settle while icons, labels, badges, `aria-current`, focus and hit targets stay fixed; a row hidden in a closed section withdraws the surface, a collapse or density change resets it in place, Reduce Motion selects the final bounds at once and solid mode keeps the skin's own row fill. GlassSurface gains an internal host ref so a surface can serve as a measurement space.
- b4dfc61: Route lookout's docs sweep through each sidebar group's own path base. The Templates
  and Patterns groups live at `/templates/<slug>` and `/patterns/<slug>`, but
  `lookout.config.ts` filed every sidebar entry under `/components/`, so those 24 pages were
  never captured and the `calendar` template was folded into the Calendar component's
  route. Template and pattern pages render `MockupDocPage`, which has no preview card, so
  they are shot full-page; the component reference keeps its element shot and overlay
  states. No kit code changes.
- a22ca22: `LoopView` on the web now lands on the right phase when its channel is played again
  while already playing (`channel.play(phase)` on a running channel). A running CSS
  animation keeps the start time it began with, so handing it a new negative delay
  re-phased it relative to that start rather than to now: every view landed late by the
  animation's age (the Lattice harness's "jump to moment" driver arrived 1.6 s into a
  moment that should have started a second later). The view now remounts its node on
  such a re-phase so the new animation starts now at the delay computed for now. Park
  and resume are unchanged (a parked view keeps its node), and native was already right
  (the native loop restarts from the phase through its head timing).
- 4188cf9: Complete the CSS glass handoff's opaque print and accessibility fallbacks, including every material tint and decorative highlight. Reconcile material policy and theming documentation with role-based glass, solid fallback, and the existing surface and density CSS attributes.
- 2f2c030: Reconcile the solid, static glass and Liquid Glass material policy and maintain
  an exhaustive public-component inventory checked against exports and docs routes.
  The inventory records required verification rather than claiming native rendering
  or interaction passes, and preserves unpainted component anatomy.
- bc2cb16: Give every moving glass selection one measurement contract (`useMeasuredTargets`): targets report their frames in an ancestor space keyed by their own identity, a structural change re-measures them before any surface moves, and a surface holds meanwhile, then travels when the target it sits on kept its frame and resets in place when that target moved or left, without unmounting (a remount lingers on iOS, where the native material fades out). The Sidebar's shell shape now measures its rows against an ancestor body, which native `measureLayout` requires: on iOS the rail's active row had jumped instead of travelling. The Sidebar rides through an accordion closing the section it left, and a non-collapsible section's open state no longer counts as structure. Pagination, Calendar, Navbar, Sidebar and Carousel share the hook.
- a4b1cc3: Hand the RowMenu glyph, the Popover button, the split ButtonGroup and the Command search bar to their panes the way the Dropdown hands its pill to its menu: under glass the trigger fades and hides its glass in place as the droplet forms on its frame, the pane blooms from there, and on close the pane re-forms the trigger before its label fades back. Solid mode and Reduce Motion keep the plain tree.
- e8e30f1: Name Slider, Progress and overlay form examples using existing component label APIs, including accessible names in intentional layout counterexamples.
- fc58cff: Gate named-import distribution sizes for Button, Input, DataTable, and StackedList alongside the existing whole-kit budget, independently for web, iOS and Android. Use pinned esbuild with supported export conditions and platform extension ordering, verify those choices with a real package fixture, preserve component exports, and fail on missing or invalid output. Required and optional peers are externalized; these measurements budget Canvas distribution code, not complete native application sizes. Stock Metro package validation remains separate.

  Record Bun 1.4.0 in packageManager and use that version throughout CI so build measurements and verification do not change when the latest runtime changes. Keep all existing gzip ceilings and document the measured sizes and bundler version beside them.

- 200b8f4: A hosted anchored card that opens ABOVE its trigger on iOS and Android now sits directly above it. The card's scrollport declared no growth of its own and inherited React Native's ScrollView `flexGrow: 1`; while Yoga measured the card's absolutely positioned wrapper (which has no height of its own) it turned the card's height cap into an at-most constraint, and under the legacy stretch errata React Native keeps on the growing scrollport filled it, so the wrapper came out as tall as the cap with the content-sized card at its top. Under a `top` anchor that was invisible; under the `bottom` anchor of a card opened above its trigger the card floated up to the top of the visible band, away from its trigger, on both a page-body host and a viewport host (the docs Page scroller, the `/testing/popup` Dropdown). The scrollport now declares `flexGrow: 0`, so the wrapper is exactly the card's height on every platform; nothing changes on the web, where a growing child never inflated a content-sized card.
- 397ac67: Add isolated native candidate smoke infrastructure with shared input and public-ref
  fixtures, on-device package identity checks and explicit screen-reader verification
  records. Verify the independent registry starter's saved state and nested menus
  across browser engines and touch input.
  Keep candidate-only routes outside the ordinary starter and offer a Canvas missing-page
  screen with navigation back to the workspace.
- 2d5f3e8: Measure the current native smoke Carousel card immediately before its paging gesture. Validate fresh, visible bounds and platform coordinate units, then perform one in-card drag across more than half the page while retaining painted-page and callback assertions. This corrects the test gesture without changing production Carousel behavior.
- 77dcdf0: Align native fixture test module maps and import validation with the package name
  used by the maintained starter fixtures, restoring the native regression gate.
  Keep the package-install documentation sources consistent with the published
  package name and their generated examples.
- 602d3e9: Validate native smoke flows with the pinned Maestro command parser before builds,
  use a supported measured Carousel gesture, and preserve separate test provenance
  when rerunning corrected tooling against an unchanged native candidate. Retain
  journey and appearance-restoration failures in fresh evidence directories.
- 2e1abfa: Exercise built-in Dialog messages and ActionSheet headers in the shared glass test scenario. Capture both surfaces from the sealed native consumer in light and dark appearance, keeping pixel evidence separate from automated visibility checks.
- 6ddc5e0: Correct native acceptance flows to dismiss a Drawer input with its iOS keyboard Return action and observe Command closure after selection. Verify the exact appended query before and after selection, and capture the visible final result before tapping it.
- 866d610: Verify native Carousel paging with a measured, strictly in-card drag. Preserve
  two sequential Maestro phases, reject stale geometry before the gesture, and
  parse their generated commands before execution without changing app layout.
- b88f204: Measure safe-area insets inside Drawer and ActionSheet native Modal windows, keeping content clear of the status bar, notch and home indicator. Apply only the insets for the edges each panel touches. The optional safe-area peer remains optional; without it the existing plain-view fallback is preserved. When an app has no root safe-area provider, modal content waits for its first native inset measurement before mounting.
- 90d3f1f: Honor iOS accessibility escape on existing overlay content hosts. Dismiss the
  foremost active child before its parent, preserve controlled cancellation policy,
  and share ownership with native modal close requests without changing layout.
  Route focused Input and Textarea Escape through the same overlay policy while
  preserving consumer handlers, local editing cancellation and IME candidates.
- 8f85af0: Preserve the original Android host background, borders, corners and overflow while sampling its native paint. Keep visible content separate from sampled paint to avoid doubled translucent fills, and require the complete optional native integration before enabling capture.
- a3809bd: Extend the shared native smoke fixtures and accessibility protocol to verify inactive Dropdown, Listbox, Tabs and Radio activation, including disabled controls and exact selection callbacks. Keep ordinary native tap results separate from actual spoken-feedback verification.
- 2a8b653: Wait for the native starter home screen before opening the first smoke route, preventing Android startup from dropping the verification link. Store screenshots under Maestro's run output directory using its supported relative paths. Select native Listbox rows by their accessible names between surrounding controls, supporting native layout flattening without relying on DOM ancestry.
- 19a9689: Make the native smoke navigator follow the Canvas background token so dark-mode fixtures retain readable text on every nested screen.
- 7dcd0b6: Handle iOS first-use app-link confirmation in native smoke checks and verify Command text entry without relying on an inaccurate iOS focused attribute. Require the last Command result to lie above its footer before tapping. Record actual keyboard events and overlay coordinates, and exercise Drawer input and ActionSheet action/cancel hit targets for native safe-area acceptance.
- 69a8d8b: Make AvatarMenu's hosted alignment regression wait for the actual mounted menu and positioned wrapper, preserving default, leading-edge and precedence assertions without relying on a fixed animation delay.
- 366f954: Correct optional-peer development guidance to preserve Metro optional resolution,
  keep public declarations independent of omitted peer types, and verify the exact
  sealed package in isolated consumers.
- 8f85af0: Resolve organism surfaces against their actual material capabilities: content
  panels use stable frost, while selected calendar days, carousel actions, navigation
  controls and overlays keep their functional material. Preserve complete solid
  fallbacks, unfilled step rings and underline tabs. Keep DataTable's content host
  stable so material changes retain an active editor, its draft, focus and caret.
  Restore the inherited DashboardGrid edit-cell fill when content frost is unavailable.
  Keep carousel arrow glyphs in the foreground above their decorative material,
  and dim their disabled or pressed ink without fading the liquid material.
- 04218cc: Split browser validation into four complete-suite shards and an independent registry-starter job so the growing suite can finish within its existing job limit. Preserve candidate verification in every shard and retain separately named reports without changing test coverage, retries, or per-test timeouts.
- 1f05b00: Glass popups now open the way the iOS 26 menu does: the Autocomplete list, the Dropdown menu, the Select and the other option lists start as a glass droplet at their anchor with the rows already inside it, grow a few percent past their resting size and settle while the rows scale up and sharpen, and on close the rows vanish at once while the pane shrinks back into the anchor edge. The corner eases from the droplet's to the skin's on the material's clip, its lens and rim, and the native iOS 26 glass. Solid mode and Reduce Motion are unchanged.
- bcd43a2: Anchored cards (the Dropdown and Select menus, the Autocomplete list, Popover, RowMenu, the calendar peek and every other hosted overlay) open sooner after their trigger is pressed. The overlay now measures its trigger, its outlet and the visible band together from a layout effect instead of one after another on the next animation frame, and the portal outlet re-renders in the same commit sequence as the owner that published into it, so the card mounts and reveals with fewer hops between the press and its first frame; on iOS and Android the trigger's box lands inside the opening render itself. A card that the fitter moves to the other side of its trigger (a list opened near the bottom of the screen) now waits for its layout under the new height cap before the glass opening starts, instead of beginning at the height it had on the first side and re-targeting mid-motion.
- fefceee: Run the liquid popup presentation (the droplet every glass option list and menu opens from, its travel, overshoot and close, and the Dropdown-class hand-off) on the native animation driver on iOS and Android. The material's frame is now a transform of its resting box plus a uniform corner radius instead of an animated width, height and offset, so the springs no longer commit a shadow tree per animated view per frame: the JS thread is idle between frames and the seed frame's flush costs nothing there. The corner stays exact on the seed shape's shorter side (a capsule at the droplet, the skin's own radius at rest) and follows the scale on the other; a closed pane now leaves on the animation frame after it has handed the pill back, so the trigger's glass is painting before the pane uncovers it. The web keeps the same graph on its JS driver; solid mode and Reduce Motion are unchanged.
- cd43ccd: Preserve GlassSurface landmark roles when Reduce Transparency or Increase Contrast replaces glass with an opaque surface. Navigation and banner landmarks remain available with either accessibility setting, including the iOS material implementation.
- 6444e69: Restore native screen-reader touch exploration and activation for Autocomplete suggestions and inactive Dropdown, Listbox, Tabs, and RadioGroup options. Keep browser roving tab stops and input focus behavior while preserving each control's disabled state.
- 2fd2318: Use the popover foreground for ordinary web ActionSheet actions and Cancel so their small text stays readable in both light and dark schemes. Destructive actions retain their semantic color.
- b1952e8: Correct five resting foreground pairs while preserving their intent fills: light muted-foreground #71717b to #6d6d77, light success-foreground #ffffff to #042812, light warning-foreground #ffffff to #451a03, dark primary-foreground #fafafa to #ffffff, and dark destructive-foreground #fafafa to #460809. Validate semantic foreground pairs at 4.5:1 with a one-channel rounding margin, plus rendered Button, Badge and Alert text. Transient press opacity and ripple behavior are unchanged; these checks cover resting text.

  Four-digit hex token overrides now parse correctly in alpha(). Validate hex shapes and finite opacity while preserving the existing opacity replacement for eight-digit hex and passthrough for functional colors. Document that mixOklab accepts opaque hex only and leaves translucent inputs unchanged.

  Deduplicate visual capture routes when a component appears in multiple navigation groups.

- c8cf5ff: Refresh Linux visual baselines after reviewing all 150 changed before/after pairs
  from workflow 34247263672 at source 18ec75fa. These capture corrected semantic
  foreground contrast, Carousel picker targets, Popover field labels and previously
  obstructed captures. No screenshot thresholds or assertions change.
- 137ccfa: Refresh the Linux visual baselines for the Riskora restyle from workflow 35048401372 at
  source 8809fa3a: all 226 component, overlay and token captures change (the new palette,
  Urbanist, the 12 / 16 / 20px corners and the ambient shadow ladder), and the two Container
  captures are minted for the first time. No screenshot thresholds or assertions change.
- 8809fa3: Restyle every web skin to the Riskora Dashboard UI Kit (the iOS and Android skins keep their
  HIG and Material 3 shapes and take only the brand).

  Controls: buttons are 12px-cornered and 44px tall (36 / 52) with matching icon squares;
  every field (Input, Textarea, Select, Autocomplete, InputOTP) is a white `card` box with
  the 12px corner, 48px tall (40 / 56), a 16px inset and the 3:1 `input` boundary; select
  lists, autocomplete lists, dropdowns, row menus, popovers and the command palette share a
  16px menu card with an 8px inset and 10px-cornered 40px rows. Checkbox is a 20px box with
  a 6px corner, Radio a 20px ring, Switch a 44x24 pill, Progress a 12px capsule on the soft
  panel, Slider an 8px rail with a 20px thumb, badges and chips fully rounded pills, Kbd a
  24px cap, Avatar's square a 12px tile.

  Surfaces: cards, stats tiles, empty states, description lists, feeds, stacked lists, media
  objects, calendars, board columns and the filter panel take the 20px card corner with the
  ambient standard shade; the stat tile carries a 16px muted label over a 36px regular
  headline. Dialogs and alert dialogs are 16px cards under a 60% scrim; toasts and the
  action sheet share the corner; drawers and sheets round their content-facing edge at 30px
  under the xl shade.

  Navigation and data: the DataTable has a soft 10px-cornered header band with sentence-case
  medium labels, 56px rows on dashed hairlines and 36px row-action tiles; the Navbar is a
  72px bar with 12px link pills; the Sidebar a 20px column with 12px-cornered 48px rows;
  pill Tabs become card tabs (a 16px bar of 12px-cornered hairline segments, the selected
  one on a sky tint); pagination tiles are 40px with the 12px corner. Charts sit on the 20px
  surface with 8px bar caps and dashed gridlines. The `--p-*` web block of `platforms.css`
  transcribes every value.

- 98043d0: Make internal verification routes reachable in native docs navigation and expose the running bundle's source, package, and runtime identity separately from the latest published release badge.
- 8f85af0: Share bounded geometry motion across segmented selection, slider thumbs, and switch thumbs while retaining each platform's skin and native material. Keep drag values and hit targets authoritative, preserve focus through material changes, and cancel deformation immediately for solid fallback, disabled controls, Reduce Motion, Reduce Transparency, or Increase Contrast.
- 8f85af0: Gate Android native smoke on the capture module's instrumentation tests using sealed installed production sources and test inputs from the same candidate revision. Preserve JUnit, native test configuration, source identity, and failure logs while keeping generated build output outside the installed package.
- 1f25555: Avoid React 18 server-rendering warnings from shared overlay, accessibility, and scrolling effects. Preserve synchronous layout-effect ordering in browsers and native apps while using passive effects during web server rendering.
- 2835ea3: Give each FilterPanel option one accessible checkbox and tab stop by sharing noninteractive Checkbox content. Space toggles on release, while Enter, row clicks, labels and counts retain one shared selection path and each platform keeps its existing visual anatomy.
- 8f85af0: Paint Android capture-host backgrounds once so translucent fills retain their authored appearance while remaining available to native blur sampling.
- d573737: Keep Spinner skin visuals decorative so screen readers encounter one named loading indicator, including the web ActivityIndicator renderer.
- be4d1cc: The composite Skeleton scaffolds (`card`, `list`, `table`) now announce their loading state to assistive technology once, the way the single shapes already do. Their wrapper kept the `progressbar` role and label but also carried the flags meant to hide the inner muted blocks, and react-native-web forwards only the `aria-hidden` alias, so the wrapper hid its own progressbar and a screen reader never heard that content was loading (Android's `no-hide-descendants` hid the host node the same way). The hide flags now sit on an inner wrapper around the blocks; the outer node keeps the role, the label and the busy state, and the rendered look is unchanged.
- a86ccfe: Keep native glass overlays visible when their entrance opens by retaining one animated transform graph and keeping ancestor opacity at one. Hold an unmeasured anchored surface outside hit testing and accessibility without remounting its content, preserve corner placement on resize, and coordinate descendant focus callbacks with inherited entrance readiness. Centered panels remain ready without requiring their own size event, and reduced motion settles at the final frame.

  Popover preserves the original opener while waiting for layout, then moves focus into its existing panel. Refitting the same panel retains its focus session, and closing restores focus through the existing controller.

  Native pixel and interaction verification used React Native 0.86. The declared React Native 0.74 floor is covered by public-API source review, types and native bundles, not a device run.

- 4348e87: Unblock the release pipeline after the npm scope migration. The ordinary starter stays on the scope the registry serves Canvas from today (`@nannier-com/canvas@2.62.1`) and compiles against it, the sealed native candidate is installed under that declared dependency name so the smoke fixtures exercise the candidate, and the starter journey resolves the declared package instead of a hardcoded scope.
- de2bdb2: Replace the placeholder boilerplate guide with runnable setup commands, real session flows, and accurate platform and package requirements.
- 4b56905: Correct the standalone starter's Expo JSI Swift 6.2 host-callback captures while preserving synchronous pointer lifetimes and the existing concurrency checks.
- 8f85af0: Keep editing wells and inline metadata on stable glass, reserve liquid materials for action controls, and preserve complete platform surfaces when materials cannot render. Keep outline metadata unfilled and retain field focus, text selection, and segmented-control state while changing surface modes.
- 8f85af0: Render avatar initials, avatar overflow counts, and emblems with static glass across platforms while preserving photos, platform geometry, and solid fallbacks. Give the account-menu capsule the functional Liquid Glass role without inferring a liquid identity material from pressability.
- 8f85af0: Resolve cards, metric tiles, bordered identity rows, alerts and phone fields to static frost with complete solid skins when a material is unavailable. Preserve live editors when changing appearance, and keep list/code/empty-state inner fills consistent with the actual material and accessibility preferences.
- 8f85af0: Preserve the Switch track's measured native host across solid and glass changes
  so its rounded background and thumb coordinate space survive material updates.
  Keep the segmented ButtonGroup coordinate host stable for the same reason,
  including when it has no test ID. Preserve control state and layout instead
  of replacing either host.
- acab4a1: Keep optional blur types out of public declarations so strict TypeScript consumers can omit expo-blur. Add isolated packed-consumer checks for React 18.0 on the web, the React Native 0.74 and React 18.2 host pair, and the current locked peers, including real web interaction checks and stock Metro iOS/Android bundles with optional peers absent.
- d357504: The web field skins rest on the same `field-border` hairline as the iOS ones.

  Input, Textarea, Select, Autocomplete, InputOTP, Stepper, PhoneInput and the Command
  trigger no longer draw their resting border with the 3:1 `input` boundary on the web,
  which read as a white frame around every field on the dark card; they rest on the
  `field-border` hairline (gray-300 light, systemGray4 dark) exactly as the iOS skins do,
  through the same `fieldBorder()` helper. Focus (`ring`) and error (`destructive`) borders
  are unchanged, the non-field controls (checkbox, radio, switch, pagination, the outline
  button) keep `input`, and Android's fields keep their Material underline. The
  accessibility trade-off disclosed for the iOS fields (a resting boundary below WCAG
  1.4.11's 3:1) now covers the web fields too, at the user's request.

- 578d52e: The web Liquid Glass lens keeps one filter definition while a popup's material animates.

  A glass popup's pane (Autocomplete, Select, Dropdown, AvatarMenu, Command, Popover, the
  calendar peek) is resized by its opening spring on almost every frame, and the lens layer
  used to acquire a fresh sized `<filter>` for each of those sizes: a new data-URI SVG
  document for Chromium to parse, plus two commits on the layer, per frame. The lens layer
  now takes the bounds the pane settles at (the card's measured size, carried with the
  material's frame through the material motion context) and holds that one definition
  through the travel and the close, so an opening costs one definition instead of thirty
  to forty and the resting definition is exactly the one the settled layout would have
  acquired. Surfaces that do not move keep measuring themselves on layout as before.

- ddd7bb4: The web Tabs and TabBar now share the iOS liquid-glass anatomy, and the iOS TabBar takes the iOS 26 floating bar.

  Tabs on the web are the iOS capsule segmented control: a gray capsule track with a raised pill in solid mode, and under glass the same track as a functional-layer pane with the selection travelling as the liquid-glass puck. Both the default and the pill looks take the anatomy (the underline rule and the hairlined segment bar are gone on web); Android keeps its Material underline. The browser's keyboard focus ring stays.

  TabBar on iOS and web is the iOS 26 floating tab bar: a capsule inset from the sides that hovers above the content (which scrolls beneath it), the safe-area inset kept under the capsule, and the selected destination raised as a capsule covering its whole cell, which under glass is the measured liquid surface that travels between destinations. The skin contract gained `fill`, `floating` and `pillCovers` for that; Android's docked Material 3 bar and its icon pill are unchanged.

## 2.62.3

### Patch Changes

- 3961897: Cache DataTable sorting, pagination, and selection summaries across draft edits and other local interactions. Document immutable row and column updates, and verify large-data work counts, sorted immutable edits, and stable original-row callback indices.

## 2.62.2

### Patch Changes

- deae1cd: Avoid constructing offscreen StackedList row elements when virtualization is active. Keep default, unbounded, and reorderable lists eager, and cover the distinction with large-data construction tests.

## 2.62.1

### Patch Changes

- 0f9bddc: Let drawers expose an accessible modal name, with the built-in trigger label as the default.

## 2.62.0

### Minor Changes

- 66a262b: Add Autocomplete's public `onValueChange` callback so controlled consumers can observe both option selection and clearing with an empty string. This additive public capability justifies the minor release; `onSelect` continues to report selections only.

  Add keyboard suggestion navigation and selection, accessible active-option relationships, and native ScrollView scrolling. Preserve IME composition and let an active suggestion consume Enter before a surrounding Form submits.

  Give the disclosure button a real platform-sized touch target, including a 44pt minimum height for small iOS fields.

### Patch Changes

- a9135a6: Record the reviewed Linux screenshots for Autocomplete's platform-sized disclosure targets in light and dark, with the list closed and open. The intended pixel changes are limited to the chevrons' positions; field dimensions and option-list layout are preserved.
- 6132645: Make manual visual baseline generation capture current screenshots for review, including intentional differences within the normal comparison tolerance. Keep the regular visual regression gate's tolerance unchanged.

## 2.61.0

### Minor Changes

- 2b2befb: Add `Listbox.accessibilityLabel` so applications can name single-select lists and multi-select checkbox groups for assistive technology.

  Correct multi-select semantics and remove hidden interactive checkbox indicators while preserving platform checkbox artwork and row actions. Ensure each complete Enter or Space press changes selection once.

### Patch Changes

- 95c8f23: Keep Drawer child overlays inside the drawer's Modal window so Dropdown and Select menus remain visible above its panel. Preserve measured anchoring, outside-tap dismissal, nested Escape handling, and safe blur targets without requiring an extra consumer OverlayProvider.
- 78225b8: Fix published native module resolution so stock Metro selects iOS and Android
  skins and material helpers. Preserve the web ESM build and public declarations,
  watch both outputs during local development, and verify the sealed package in an
  isolated native consumer with optional peers omitted before CI publication.
- 8c3b6c3: Update registered local consumer overlays with the package's native entry metadata after its compiled targets exist, preserving package versions and dependency ranges. Retain exact source-map positions when native builds shorten relative module requests for Metro platform resolution.

## 2.60.5

### Patch Changes

- 217f701: Install the Bun runtime in the Cloudflare delivery job so Wrangler can deploy the validated web archive using the docs workspace's package manager. This new patch follows the successful 2.60.4 npm publication and its failed web delivery.

## 2.60.4

### Patch Changes

- 1b59a50: Correct optional dependency fallbacks, source development instructions, repository links, privacy copy and the distinct licenses for the npm package and repository source. Document the validated artifact release process and route the release script through its CI-only guard.
- 023b721: Suppress Select option lists and interaction callbacks while disabled, including when a controlled or already-open Select becomes disabled. Preserve stored open and selection state across re-enabling and expose consistent native and web accessibility states.
- 212c559: Assign Escape to the foremost overlay with explicit ancestry preserved across hosted content. Keep local editor cancellation, TextInput event handling, held keys and native Modal dismissal coordinated so one key closes one layer and preserves its parent.
- ab0c66b: Remove Stepper's resolved accessibility exceptions so CI requires a clean scan. Give the real package-sealing integration test a bounded subprocess budget while preserving its full archive and artifact checks.
- eb53b18: Fit the browser viewport to each measured component preview before taking a screenshot. Cover tall Calendar and GridList previews as well as opened overlays so captures cannot introduce blank areas or transient responsive navigation inside the image.
- d3ca471: Activate dialog and popover focus management when their panels actually attach, including delayed portal and measurement mounts. Preserve the public object-ref API, modal Tab trapping, nonmodal focus behavior, and restoration across nested panels, reopening, replacement, and unmount.
- ce3e842: Clear inherited repository-selection variables from release subprocesses so their explicit working directory remains authoritative. Isolate release test fixtures from Git hook environments and verify that adversarial inherited settings cannot modify another repository's configuration, refs, index, staged content or remote.
- a906439: Normalize Stepper decimal arithmetic before storage and callbacks, preserving decimal offsets and exponent-form increments. Expose a named spinbutton with keyboard adjustment on web inside a noninteractive group, while retaining the existing native adjustable View actions and unchanged platform layouts.
- ae3336c: Provide Changesets its local main reference when CI checks out a pinned commit in detached mode. Keep the reference at the triggering source SHA and validate the resulting version candidate before delivery.
- 0129449: Preserve open overlays and form drafts when the docs navigation and playground change responsive layouts. Scope overlay checks to their own preview and size screenshot viewports from the actual opened stage so browser capture cannot trigger a transient responsive layout.
- 907de7f: Prepare release versions before validating a frozen candidate, then publish only its tested npm tarball and docs archive. Reject stale candidates without rebasing or publishing, enforce the major-version guard for manual and automatic runs, and report actual delivery outcomes. Prepare Cloudflare asset paths in the shared docs build before browser tests.
- fd4fe0e: Refresh 45 individually reviewed Linux screenshot baselines after fixing oversized element capture. The corrected images contain the full previews without transient navigation headers or blank clipped areas; the other 181 baselines are unchanged.
- a27b784: Break the Sidebar's require cycle by giving the nav row its own module.

  `sidebar.shared` builds the narrow drill-down from a skin, and the drill-down
  imported `SidebarItemBadge` back from `sidebar.shared`, so the two modules
  required each other. Metro allows that and warns on every app start ("Require
  cycle: sidebar.shared -> sidebar.drilldown -> sidebar.shared"), because
  whichever module loads second sees the first half-initialized. It was harmless
  only because every value crossing the cycle is read inside a render, by which
  time both modules have finished loading; a value read at module scope would
  have evaluated as `undefined` far from the line that caused it.

  `SidebarItem`, `SidebarSection` and `SidebarItemBadge` now live in
  `sidebar.item`, which both presentations import and which imports neither of
  them back. `sidebar.shared` re-exports the two types, so the public API is
  unchanged: `SidebarItem` and `SidebarSection` are still exported from the kit
  and from each per-OS entry point, and no consumer import changes.

  The warning is gone from the docs app on iOS, verified on the simulator.

## 2.60.3

### Patch Changes

- bbc6f2e: Restore guarded development hook installation in fresh and moved checkouts. Run local checks fail-fast against freshly built package output, and skip hook setup in CI, production, and published-package installs.

## 2.60.2

### Patch Changes

- 766f715: Autocomplete and Select: a capped option list now clips and scrolls instead of
  spilling out of its card.

  Both cap the open option list with a `maxHeight` on the popover card, but that cap
  bounded the card alone: React Native Views are `overflow: visible` and
  `flexShrink: 0` by default, so a list taller than the cap painted its extra rows
  past the card, onto the bare page, with no fill, border or shadow behind them. The
  iOS and Android skins hit this on a seven-option list (326pt and 344dp of rows
  under 260/280 caps); the web skin fit its 240 cap by eight pixels, so a single
  extra option would have tipped it over too.

  The card now clips to its own rounded corners, and the rows sit in a scrollport
  that may shrink to the capped card, so the options past the cap scroll into reach
  rather than being clipped away unreachable.

## 2.60.1

### Patch Changes

- 29d4c1a: Two corrections to changes made earlier in this batch, both found by reviewing them
  adversarially rather than by a user hitting them.

  **Autocomplete.** Escape closed the option list and left the field in a state it could
  not get out of: the caret stayed in the input, so the focus handler that opens the list
  could never fire again, and clicking the field it was already focused in did nothing.
  Recovery meant typing another character or finding the chevron. ArrowDown now reopens,
  which is the standard combobox key and the other half of the Escape contract, and a
  press on an already-focused field reopens it too. Escape is also gated on the list
  being open, so a closed field no longer reports a spurious close to a controlled parent.

  The chevron's touch target is reverted. It is 7pt wide, which is genuinely too small,
  but the editable text is its immediate neighbour: a symmetric 18pt of hit slop took the
  last 18pt of the field, which is exactly where you tap to put the caret at the end of
  what you typed. A too-small target is better than one that steals its neighbour's. The
  measurement and the reason are recorded in the touch-target coverage test; fixing it
  properly means widening the control, which is a layout decision of its own.

  **Badge.** A named status badge took `role="img"` so its label had a role to sit on.
  That is right for a bare dot standing in for a word, and wrong for a badge that also
  renders text, because img is a leaf role: it would have replaced the visible text in
  the accessibility tree with the label. A badge with text and an explicit label is a
  group now, which accepts a name and keeps its children readable. All three cases are
  covered by tests.

## 2.60.0

### Minor Changes

- 0d56788: The package now ships `DESIGN.md`: the kit's design system, written to be read.

  Minor justification: this is a new file in the published package, so a consumer, or
  more often an agent working in a consumer's repository, can read the kit's actual
  values and rules without a network round trip to the docs site.

  Its frontmatter follows the schema the design-md library uses for the production
  systems it documents, so a tool that can read one of those can read this: the colour
  tokens for both schemes, the ten type roles with size, weight and line height, the
  spacing, radius, elevation, motion, breakpoint and field-width scales, both platforms'
  touch minimums, and per-platform component recipes.

  All of that is GENERATED, from `src/style/tokens.ts` and `styles/tokens/*.css`, with a
  `--check` gate in CI, so it cannot describe a previous release. The prose is authored,
  because none of it is derivable from a number: that every visual variation is a
  semantic boolean prop and string enums do not exist, that there is no style escape
  hatch, that glass is a theming mode rather than a per-component look, that a control
  owns the text it labels, and that responsiveness goes intrinsic first, container
  second, viewport only for the shell.

## 2.59.1

### Patch Changes

- a273386: Three accessibility fixes found by running axe over every component page in a real
  browser.

  **RowMenu** rendered its rows as `menuitem` with no `menu` container around them. ARIA
  requires a menuitem to be owned by a menu, so each row was an orphan: axe files it as
  `aria-required-parent`, and a screen reader reads a loose control rather than "menu, N
  items". Dropdown has carried that container since it shipped. The panel now has one
  too, named from the section label or the trigger, so a screen-reader user hears which
  menu opened. A menu of links, which is what `links` makes, correctly gets no menu role.

  **A named status Badge** put its `accessibilityLabel` on a bare View. ARIA prohibits
  naming a generic element, so the label was being discarded rather than announced,
  which is the opposite of what a status dot standing in for a word needs. It now
  carries `role="img"`, the same role Swatch already uses for the same reason, and only
  when it actually has a name: a decorative dot beside its own text label stays silent.

  **Kbd** had the same problem by a different route: it set `accessibilityRole="text"`,
  which react-native-web maps to no DOM role at all, leaving the chord's name on a
  generic div. It is `role="img"` now, so "⌘+K" is announced once as a unit rather than
  cap by cap, which was always the intent.

## 2.59.0

### Minor Changes

- 20c830f: New public API: the touch-target helpers, and five controls that now use them.

  Minor justification: `TOUCH_TARGET`, `platformMinTarget()`, `minTargetSlop()`,
  `useMinTargetSlop()` and the `TouchTargetSkin` shape are exported from the package
  root, so an app building its own pressable can meet the platform minimum the same way
  the kit does:

  ```tsx
  const target = useMinTargetSlop(TOUCH_TARGET.ios);
  <Pressable {...target} onPress={onPress}>
    …
  </Pressable>;
  ```

  Apple asks for 44pt and Material 3 for 48dp, and plenty of controls are smaller than
  that on purpose: an icon square, a step circle, a chevron. Growing them would be the
  wrong fix, because the minimum is about the area a finger can hit, not the area the
  design should occupy. So the shape stays and the touch area grows: the skin declares
  its platform's minimum, the shell measures what actually rendered, and hitSlop makes
  up the shortfall symmetrically. Nothing moves.

  Five controls that were short now reach it. The Autocomplete's chevron was the worst,
  at 7pt wide. The Switch row is 28pt tall on iOS, the Steps circles are 32pt, the
  RowMenu trigger is 32pt on iOS and 40dp on Android, and the CodeBlock copy chip is
  26pt. None of them changes size or position.

  The pattern was Button's alone; it moves to `src/style/touch-target.ts` so every
  pressable can use it and so a coverage test has one thing to look for. That test
  enumerates every component with a pressable and requires each to declare a target, or
  to be listed with how else it reaches the minimum, or to be listed as a measured gap.
  Fourteen components are in that last list today, each with the size measured in a real
  browser against its own platform skin.

## 2.58.1

### Patch Changes

- 26c62bc: Autocomplete: Escape closes the option list when the caret is in the field.

  The shared `useEscapeKey` hook listens on the document, which covers focus on the
  chevron or anywhere else on the page. It never covered the case that matters most for
  a combobox: someone typing a query and pressing Escape to abandon it.
  react-native-web's TextInput does not let an Escape keydown out of the input, so the
  document listener never heard it and the list stayed open, with `aria-expanded` stuck
  at true.

  Two test suites asserted Escape worked and both passed, because both opened the list
  by clicking the chevron, which leaves focus on a button. Driving a real browser is
  what surfaced it. The field now handles the key itself through React Native's own
  `onKeyPress` channel, so every way in has a way out, on every platform that reports a
  key.

## 2.58.0

### Minor Changes

- 68ae11a: New public API: `tabularNums()`, a text style that gives every digit the same width.

  Minor justification: this adds a capability consumers can use directly, exported from
  the package root beside `shadow()` and `alpha()`. Spread it into any text style that
  shows numbers read down a column or watched as they change:
  `{ ...tabularNums(), fontSize: 14 }`.

  It exists because the two platforms spell this differently and the kit only knew one
  spelling. `fontVariant: ["tabular-nums"]` is React Native's API and works on iOS and
  Android; react-native-web DROPS it, emitting no inline style, no generated class and no
  warning, so the element renders exactly as if nothing had been asked for. Every
  tabular-figure call site in the kit, in Progress, Slider and seven charts, was therefore
  a no-op in a browser. The web branch emits `font-variant-numeric`, the CSS property that
  actually does this, and all eighteen call sites now go through the helper.

  Two components gain the treatment they were missing. A DataTable column marked `numeric`
  now uses tabular figures in its cells and in its inline editor, so a column of amounts
  lines up on the decimal instead of drifting, and opening a cell to edit does not reflow
  the number under the caret. Stats does the same for its headline value and its delta, so
  a figure that updates in place stops jittering as its digits change.

  Unrelated, in the same neighbourhood: the Heatmap's weekday gutter labels were 9px,
  below the 10px the platforms themselves treat as the floor and smaller than the month
  labels directly above them. They are 10px now, and the gutter widened to fit them.

## 2.57.4

### Patch Changes

- 91a92bd: The web CSS hand-off no longer paints Stats and EmptyState with iOS values.

  An iOS fragment had been pasted inside the web block of `styles/tokens/platforms.css`,
  several lines below the correct web declarations, where the cascade silently replaced
  them. A web surface reading the hand-off got the iOS Stats card (12px radius, 18px
  inset, no shadow, 12px gap, SF tracking) instead of the web one (8px, 20px, shadow-sm,
  14px), the iOS EmptyState corner instead of the web one, and `--p-min-target: 44px` on
  the one platform whose minimum is zero. The iOS block, meanwhile, declared none of
  them and inherited the values that had been misfiled into web, so both blocks resolved
  to plausible numbers and nothing failed. Each side now declares its own.

  Two smaller repairs in the same layer: the spacing scale ships its top five steps
  (`--space-36` through `--space-64`), which `src/style/tokens.ts` has always carried and
  the CSS stopped short of; and the Android block no longer declares `--p-nav-rule` twice.

  The Icon stroke weight now comes from one exported constant that the native menu-glyph
  raster generator reads too, so the baked PNGs beside a live Icon cannot drift to a
  second weight.

## 2.57.3

### Patch Changes

- 2b113c7: TabBar: draw Material 3's active-indicator pill on web, not only on Android. Web
  is the one platform where a bottom bar and a nav rail are the same app seen at two
  widths, since an app shell swaps `Sidebar` for `TabBar` across a breakpoint and the
  user carries the memory of one across the resize. `Sidebar` marks its active row
  with a filled surface behind the row, so a tint-only bar made "you are here" change
  species at that breakpoint, which a consumer's visual audit caught. The pill is the
  M3 one by construction, tonal brand tint and all, scaled to 48x28 for the 12pt
  shorter web bar. The iOS skin is untouched: a phone app has no rail to disagree
  with, so this stays a web departure rather than a change to the HIG look.

## 2.57.2

### Patch Changes

- 133dcfd: DashboardGrid: a board's rows sit flush, and a grown Card fills the box it is given

  DashboardGrid laid its cells out with `alignItems: "flex-start"`, so every cell
  hugged its own content. That is invisible while the widgets in a row happen to
  run the same height and reads as a hole in the board the moment one of them is
  short: on the Ionize console's overview, a widget with nothing to draw sat in a
  158px tile beside a 427px neighbour, and the 269px underneath it was page
  background rather than anything the board had put there. Cells now stretch to
  the height of the row they wrapped onto, and the customize-mode chrome (the drag
  wrapper and the dashed edit ring) passes that height on, so a tile that fills its
  cell keeps filling it while the board is unlocked.

  Stretching the CELL is all that changed. The widget inside still sizes itself, so
  a board of widgets that hug their content renders exactly as it did before; a
  widget that wants the tile's full height now has a box to grow into, which was
  not reachable from outside the kit.

  `Card`'s `grow` reached the surface alone, which made it the wrong half of that
  pair: a card stretched to stand beside a taller neighbour drew its content in a
  box it did not fill and left its footer floating in the middle of the surface.
  The body section now takes up whatever slack the growth won. This is inert on a
  card that is already exactly as tall as its sections, which is every card that
  was not asked to grow.

## 2.57.1

### Patch Changes

- 6355a6e: DataTable: a pressable row is a row, not a button

  `onRowPress` rolled the data row `button`, which react-native-web renders as a
  real `<button>`. That stranded every `role="cell"` outside a row, so the table
  stopped reading as a table (`aria-required-parent`, `aria-required-children`),
  and it swallowed every control the row carried: the kit's own select checkbox,
  and anything a caller put in a cell. A row menu in a cell became a `<button>`
  inside a `<button>`, which is invalid DOM and logs a React nesting error on
  every row (found on the Ionize console's Identities table).

  The row is now always `role="row"` and keeps the press as a pointer convenience
  outside the accessibility tree, so clicking anywhere on it still acts. The
  action itself moves to a real button inside the row's first data cell, named
  from the row's plain text cells (or, when the row has none, by that cell's own
  content). It is a sibling of everything else in the row, so nothing nests, the
  row is one tab stop, and a screen reader announces it as the row's action.

  Callers passing `onRowPress` should keep their own controls out of the first
  column, which is where the activator goes.

## 2.57.0

### Minor Changes

- b8d5d4a: `Stats` can frame every metric's strip, so a row of them scans evenly.

  Without a frame each strip draws only its marks, and in a row that is not a
  neutral choice: a metric whose series is quiet draws a few faint marks beside a
  metric whose bar is full, so the densities run from blank to solid with nothing
  tying them together. The eye lands on the fullest tile rather than scanning the
  row, and the quiet tiles read as charts that failed to load rather than as
  charts sitting at zero.

  - `Stats.framed` puts every strip on the muted track, in one bounded band: the
    frame is the same whatever the data, so the fill is the only thing that
    varies, and a flat series reads as a chart at zero.
  - `Sparkline.track` paints the plot area with the muted track. Use it where
    strips sit side by side; a lone sparkline in running text is usually better
    without.
  - `StackedBar.tall` takes the 24 band a Sparkline plots in, with the chart bar
    radius rather than the pill, so a composition and a trend draw as one family.
  - `StackedBar.subtle` washes the segments to the density a Sparkline gives its
    own marks, for a bar that supports a headline rather than being one. `Stats`
    now draws a tile's `share` this way, framed or not: inside a tile the strip
    supports the value rather than being it.

  All additive, and every default is unchanged. Found on the Ionize dashboard's
  four-metric row, where the solid composition bar outweighed its three quiet
  neighbours.

## 2.56.1

### Patch Changes

- ae6a2f0: Stand a `Stats` composition strip on the trend strip's floor.

  `share` reserved the trend strip's height and centred its bar in it, so in a row
  that mixes the two the composition bar floated seven pixels above its
  neighbours' baseline: a Sparkline's bars sit on the bottom of the band
  (`alignItems: "flex-end"`). The strip is bottom-aligned now, so every tile in a
  row shares one floor. Seen on the Ionize dashboard's four-metric row.

## 2.56.0

### Minor Changes

- 3012c30: `Stats` can draw a metric's composition where it has no trend, so a row of tiles
  fills to one line.

  New capability, three additions, all backward compatible:

  - `StatItem.share` takes `StackedSegment[]` and draws them in the SAME slot as
    `spark`, as a proportional strip over a muted rail. Ignored when `spark` is
    set.
  - `StatItem.sparkLabel` names what the trend strip plots. It still defaults to
    `"<label> trend"`, which is right whenever the strip is the value's own
    history; set it when the strip plots the supporting line instead, so a screen
    reader is not told the strip is something it is not.
  - `StackedBar.track` paints the unfilled remainder as a muted rail, which is
    what lets a composition of nothing (a queue at zero, a metric at zero) read as
    a bar rather than as blank space. The all-zero dev warning now speaks only for
    the trackless case, since the track is how that state is drawn on purpose.

  Why it was needed: cards in a Stats row stretch to the tallest sibling, so a row
  where one metric carries a strip left every other tile with a band of blank space
  under its text and read as an unfinished component rather than a deliberate one.
  The kit offered no honest way to fill the rest, because a metric with no trend
  had nothing to put in the slot. Most such metrics still decompose, and `share`
  draws that decomposition, at the trend strip's reserved height so the row keeps
  one card height. Found on the Ionize dashboard's four-metric row, where only
  "Total identities" had a series to plot.

## 2.55.2

### Patch Changes

- 04e7f0a: Trim the `--input` note in `styles/tokens/colors.css` back under its size budget.

  2.55.1 explained the `input` / `border` split in a long comment inside the
  shipped stylesheet, which pushed `styles/tokens/colors.css` from 1856B to 2368B
  gzipped, past the 2048B per-file budget `check-size` enforces. `styles/` ships
  to consumers, so those were real bytes on every install.

  The note is now three lines and the full reasoning moved to `src/style/tokens.ts`
  beside the values themselves, where it costs no shipped CSS. No token value
  changes: `validate-tokens` and the render-parity check both still pass.

## 2.55.1

### Patch Changes

- 8a65d9e: `input` now meets the WCAG 3:1 control-boundary minimum in both schemes.

  `input` and `border` shipped the same hairline value (`#e4e4e7` light,
  `#27272a` dark). That is right for `border`, which separates two surfaces of
  differing fill and is read against that difference, but wrong for `input`: it
  is the boundary an unfilled control draws itself with, and WCAG 2.2 SC 1.4.11
  holds exactly that to 3:1 against its surroundings. On the page background the
  shared value measured 1.27:1 in light and 1.34:1 in dark, so an outline Button
  or a bare text field had a silhouette the eye could not find, and an unchecked
  switch hid its own thumb.

  `input` becomes `#88888b` in light and `#747478` in dark, the lightest values
  on the existing border hue that still clear 3:1 against all three surfaces a
  control is placed on: the page, a card or popover, and a muted panel. `border`
  is unchanged, so card edges, dividers and table rules keep their hairline
  weight. Every control that reads `input` gains the visible boundary: Button
  (outline), Input, Textarea, TextInput, Select, Autocomplete, Checkbox, Radio,
  Switch, InputOTP, Stepper, Pagination and Avatar.

  The iOS Switch keeps its hard-coded systemGray3 off-track. That constant is a
  HIG fidelity match to the real UISwitch rather than a contrast choice, and it
  still sits under the 3:1 floor; moving it is a platform question, not a token
  one.

## 2.55.0

### Minor Changes

- 4a4efdc: Row and Column gain a `shrink` modifier, the new public option this release adds.

  React Native gives every box `flexShrink: 0`, the opposite of the web's flex
  default, so a Row child sized by a long sentence keeps that sentence's full
  single-line width and spills past the row's edge, where the nearest clipping
  ancestor cuts it mid-word. Until now the layout primitives had no way to say
  "this child may give way": `fill` (flex: 1) also zeroes the flex basis, which
  pulls every child of a `wrap` row back onto one line, and the kit's own
  molecules reached for a raw `flexShrink: 1` internally instead.

  `<Column shrink>` sets `flexShrink: 1` and leaves the basis at the content size,
  so a wrapping row still breaks where it did and only the over-wide child gives
  way. Opt-in and backward compatible: nothing changes for layouts that do not
  pass it.

## 2.54.1

### Patch Changes

- 2777522: The kit declares its own client boundary: `dist/index.js` now carries a `"use client"` prologue. Canvas is a react-native-web kit whose exports reach React context on module evaluation, so importing it from a React Server Component (Next.js App Router) threw `createContext only works in Client Components` and every consumer had to remember its own wrapper. Patch, not minor: no new component, prop, or API, only the packaging declaration that makes the existing surface importable from a server component.

## 2.54.0

### Minor Changes

- eacab35: `Typography` gains `href`, `hrefAttrs`, and `onPress`: inline text renders as a real browser link on the web (announced as a link; the link role wins over a heading role), native navigates through `onPress`, and `underline` remains the link look. Minor because it is new public Typography API, completing the link affordance `Button` gained for button-shaped navigation.

## 2.53.0

### Minor Changes

- 039d0c8: `Button` gains `href` and `hrefAttrs`: with `href` the web render is a real `<a>` (middle-click, new tab, crawlable, announced as a link), `hrefAttrs` forwards `target`/`rel`/`download`, native keeps `onPress` navigation, and a disabled or loading button suppresses the anchor. Minor because it is new public Button API: the kit's first link affordance, closing the long-standing "no link affordance" consumer gap.
- df066b9: GeoMap: `zoomable`, with bubbles that aggregate and split as you zoom.

  Minor because it adds user-visible capability: a new `zoomable` boolean and the
  `zoom` / `defaultZoom` / `onZoomChange` controlled trio, plus `onSelectPlaces`.
  The wheel zooms about the pointer, two fingers pinch, a drag pans once zoomed,
  and a zoom control pair plus the arrow keys give the same reach with no pointer
  at all. Places too close together to draw separately merge into one bubble
  carrying their summed count, and that bubble splits into its members as the map
  is driven in.

  Opt-in, so nothing changes for anyone who does not ask for it. Without
  `zoomable` the camera is the world verbatim, there are no links, and the cut
  degenerates to one bubble per point in input order, taking the identical code
  path it always did.

  `onSelect` keeps its exact meaning, a point index. Pressing a group reports its
  largest member, which for a single place is today's value, and `onSelectPlaces`
  carries every member alongside.

  Aggregation cuts one minimum spanning tree, built per data set, at a threshold
  that halves per zoom level. That makes each grouping a refinement of the one
  above it, so a group can only split and no place can migrate: the anti-flicker
  guarantee is structural rather than tuned. The threshold is exactly zero at the
  deepest zoom, so any data separates fully.

  Also fixes a pre-existing bug: React Native Web gives every Pressable
  tabIndex 0 regardless of `accessible={false}`, so the map carried a second,
  nameless tab stop that announced nothing.

## 2.52.6

### Patch Changes

- 0556bc1: GeoMap: sharper coastlines and country borders.

  The world data now comes from Natural Earth 1:50m instead of 1:110m, and the
  generator emits a second path holding the mesh of boundaries that two countries
  share (coastlines excluded, since the land path already draws those). The viewBox
  widens from 1000 to 2000 units, so the geometry stays crisp when the map is drawn
  wider than 1000px, which it is wherever a caller passes `width: "100%"`.

  Land now carries a coastline stroke and borders a fainter one, both in the muted
  foreground token, so the silhouette reads against the card surface in both schemes
  instead of sitting a couple of steps away from it on the ramp.

  No API change: every prop keeps its meaning, so no call site needs touching. The
  bubble radius constants doubled because they are expressed in viewBox units and the
  box did, which leaves every bubble exactly the size it was.

  This costs bytes: `geo-map.world.ts` goes from 5.6KB to 23.8KB gzip and the measured
  bundle from 161.5KB to 180.8KB, so the JS size budget moves from 160KB to 192KB. The
  whole increase is one generated data module of two string constants, and it
  tree-shakes out for consumers who never import GeoMap.

## 2.52.5

### Patch Changes

- c32e642: `check-parity` now fails on a DEAD RECORD: a divergence recorded for a prop the
  kit already declares under the hand-off's own name, or for a component the kit
  already exports.

  A divergence is consulted only while the hand-off prop is ABSENT from the kit, so
  shipping the capability makes its record unreadable, and the report row simply
  disappears on the next regeneration with nothing pointing at the record left
  behind. Six have been deleted by hand in two sweeps (`Tooltip.children`,
  `Input.onBlur`, `Input.maxLength`, `Textarea.maxLength`, `ActionPanel.children`,
  `Navbar.actions`), and every one had gone false by the time it was found, still
  denying a capability the kit shipped. This is the same guarantee the broken
  redirect guard gives from the other end: that one catches a record naming a prop
  the kit lacks, this one a record about a prop the kit has.

  Scoped to claims about the kit. A `global` record is the fallback for any
  component prop no component-level record claims, so being unread is its resting
  state, and a record under a component the kit has not shipped stays live for the
  day it lands.

  Tooling only, with no runtime effect. `HANDOFF-PARITY.md` picks up a paragraph
  describing the new guard; the counts are unchanged (733 props compared, 526
  present, 151 settled, 56 open gaps).

## 2.52.4

### Patch Changes

- a69821f: Two more dead records leave the hand-off parity ledger
  (`tools/handoff-parity/divergences.json`): `ActionPanel.children` and
  `Navbar.actions`.

  Both were `renamed` records, and both are false about the kit as it ships today.
  `ActionPanel.children` claimed the hand-off's body slot is spelled `description`
  here; ActionPanel has carried its own `children` slot since
  "feat(action-panel): support embedded children", under the hand-off's own name
  and for the hand-off's own purpose. `Navbar.actions` claimed the trailing
  controls arrive as `actionLabel` plus `onAction` "rather than a ReactNode";
  `NavbarProps.actions` is a ReactNode slot, added in "feat(navbar): brand element
  and trailing actions slots". The report was refreshed when those props landed,
  which dropped their two rows, but the records themselves stayed behind: the check
  consults a divergence only when the hand-off prop is ABSENT from the kit, so
  neither had been read since.

  This is the same rot the four dead open-gap records carried, in the settled half
  of the ledger: a record nothing reads still reads as current adjudication to
  anyone opening the file. Tooling data only, with no runtime effect.
  `HANDOFF-PARITY.md` regenerates byte-identical (733 props compared, 526 present,
  151 settled, 56 open gaps), because a record the check never reads never reached
  the report.

  The two remaining unreachable records, `global.value` and `global.defaultValue`,
  stay: a global record is a fallback for any component whose hand-off prop no
  component-level record claims, so it is unread whenever every such prop happens
  to be adjudicated elsewhere, not wrong.

## 2.52.3

### Patch Changes

- 21991ad: Drag & drop now reads in the layout direction: a `horizontal` DropZone mirrors
  under RTL, and the two horizontal arrows swap roles.

  The geometry had no direction handling at all. `insertionIndexFor` treated a
  larger X as a later data index, so a pointer dropped visually to the RIGHT of a
  target in a right-to-left locale resolved to "after" in data order, which renders
  to its LEFT; `zonesInReadingOrder` sorted each row by ascending X, the reverse of
  how the zones actually read. The two agreed with each other, so the keyboard
  cursor was consistently wrong rather than inconsistently wrong.

  The axis is now mirrored, following how the rest of the kit resolves direction:
  `flexDirection: "row"` mirrors under RTL, so data index 0 is the RIGHTMOST card
  and "later in data order" means further left. `sortByMainAxis` returns the row in
  data order (descending X under RTL), `insertionIndexFor` counts the midpoints the
  pointer has passed along the reading direction, and `zonesInReadingOrder`
  reverses only the within-row sweep (rows still run top to bottom, because the
  block axis never mirrors). Vertical zones and the up/down arrows are unchanged in
  both directions.

  `insertionOffset` now measures from the zone's LEADING edge along the reading
  axis, and the horizontal indicator anchors with the logical `insetInlineStart`
  rather than a physical `left`, so the line meets the correct edge of a mirrored
  row on every platform with no branch. In a left-to-right layout that resolves to
  the same `left` inset as before, so nothing about the existing look changes.

  ArrowLeft and ArrowRight swap roles under RTL (Right Arrow moves to the previous
  item, per the WAI-ARIA practices), the same flip `useRovingFocus` applies to a
  tablist and the Slider applies to its step. The keyboard cursor's opening
  position, which already reads a `horizontal` zone along its own axis, now reads
  that axis in the layout direction too.

  Direction reaches the geometry as a parameter, never a global read: the shell
  reads `isRTL()` and hands the answer in, which keeps the whole module unit-
  testable with no renderer.

## 2.52.2

### Patch Changes

- c9789b7: Four dead open-gap records are removed from the hand-off parity ledger
  (`tools/handoff-parity/divergences.json`): `Tooltip.children`, `Input.onBlur`,
  `Input.maxLength` and `Textarea.maxLength`.

  Every one of those props is declared on its component's props interface today.
  `Tooltip.children` shipped in "feat(tooltip): arbitrary element trigger", and the
  three field props come in through the `TextEntryProps` slice that `InputProps`
  and `TextareaProps` both extend. `check-handoff-parity.ts` reads a divergence
  record only when the hand-off prop is ABSENT from the kit, so none of the four
  had been consulted for some time; they sat in the file claiming a capability was
  missing while the kit shipped it. `Tooltip.children`'s reason was the loudest
  about it, asserting that Canvas's Tooltip "cannot attach to a caller's node".

  Tooling data only, with no runtime effect: `HANDOFF-PARITY.md` regenerates
  byte-identical, because a record the check never reads never reached the report.
  Phase 4's validation work stays tracked through the props that really are absent
  (`Input.validate`, `validateOn`, `messages`, `submitted`, `showValid`,
  `onValidate`, `minLength`, `pattern`, `min`, `max`). The now-empty `Textarea`
  component key goes with its last record.

## 2.52.1

### Patch Changes

- 1a6bc36: Grabbing a card by keyboard inside a horizontal `DropZone` now starts the cursor
  at the card's left-to-right position instead of a position read down the wrong
  axis.

  `startKeyboardDrag` sorted the source zone's measured cards along the VERTICAL
  axis unconditionally, even when that zone was registered `horizontal`. Ranking a
  row's cards by their top edge says nothing about the order it reads in: cards of
  unequal height rank by how they sit on the cross axis rather than left to right,
  and a row whose tops all agree is a pile of ties that resolves to whatever order
  the measurement pass happened to build. The cursor therefore opened on the wrong
  card, announced the wrong position, and dropped at the wrong index. Everything
  else in the drag layer already read the zone's own `horizontal` flag through
  `zoneCards`, so the pointer path and the insertion indicator were never affected,
  which again left the accessible path as the broken one.

  The starting index now reads that same flag, so both paths agree on one axis per
  zone. No kit component reached this: DashboardGrid's horizontal zones hold a
  single card each and Board's lanes are vertical, so the fix lands for consumers
  composing a horizontal `DropZone` with several `Draggable`s of their own.

## 2.52.0

### Minor Changes

- 77345e2: Icon: the `blend` glyph joins the curated set (414 glyphs).

  Minor justification (new public API): `<Icon blend />` is a boolean that did not
  exist before, and `"blend"` becomes a new member of the exported `IconName` union,
  so every data-driven `icon` slot that types against it (Dropdown, Command, Sidebar,
  RowMenu, Feed, ButtonGroup, Stats) can now name it. Code written against 2.51.x
  does not compile against the glyph today; after this release it does.

  Lucide's `blend` is two overlapping circles, the standard glyph for mixing two
  surfaces. It is the honest icon for a glass/solid surface toggle, which previously
  had to borrow `sparkles` (a magic-wand metaphor that names an effect, not a
  material). Nothing existing changes: the set only grows, every prior glyph name is
  untouched, and the `<Icon set />` gallery picks the new entry up automatically
  because it renders from the generated `NAMES` table.

  The set is generated, so the change is one name added to `tools/icongen/icons.ts`
  plus a `bun run icons:gen`; `src/atoms/icon/icon.glyphs.ts` is the regenerated
  output. The new `test/icon-glyphs.test.ts` pins that output against both of its
  inputs (the curated source list and lucide-static's `icon-nodes.json`), so a name
  added without a regenerate now fails a test instead of surfacing as a missing
  glyph at a call site.

## 2.51.3

### Patch Changes

- 79b1113: `DashboardWidget.title` is documented for what it actually is: the widget's
  accessible name, not a header the grid paints.

  It was described as "Widget header label", which promised chrome that has never
  existed. Cells render bare on purpose (a widget arrives with its own surface, so
  a second heading would double up), and the field is what customize mode uses to
  name the widget's drag grip and its drop zone, which is what the drag
  announcements read out. Locked, nothing reads it at all, though it stays required
  because the board can be unlocked at any time.

  Documentation only: the behaviour was right and is unchanged, so no board looks
  or acts differently. The `.md` gains a Do/Don't pair teaching it, since a slug or
  placeholder title costs nothing visually and leaves keyboard and screen-reader
  users dragging "widget-2" around.

## 2.51.2

### Patch Changes

- 1f165dc: A `storageKey` DashboardGrid no longer breaks hydration: the saved layout is
  adopted in the first commit after hydration instead of during it.

  The stored order was read on the FIRST render, which on the web is the hydration
  render. A user who had reordered their board therefore hydrated markup the server
  never sent, and React does not patch that up: it discards the server tree and
  rebuilds the whole subtree on the client. The first render now seeds from the
  declared `defaultOrder` (or the `items` order), which is exactly what the server
  shipped, and the saved layout takes over one render later.

  The gate is the kit's existing hydration helper, lifted out of
  `src/style/container.ts` into `src/style/use-hydrated.ts` so both callers share
  one implementation: false for the server render and the hydration render, true
  from the first commit on. It stays kit-internal, imported by path, and the window
  fallback in `useContainerWidth` / `useContainerBreakpoint` behaves exactly as
  before. `ThemeProvider`'s `ssrScheme` gives the colour axis the same contract.

  Client-only apps are unchanged: with no hydration pass the first render already
  reads storage.

## 2.51.1

### Patch Changes

- 6f0dd12: Keyboard reordering now follows the arrangement on screen, so a second move on
  an already-reordered DashboardGrid goes the way the arrow key points.

  The drag layer's keyboard cursor walked the zone REGISTRY, which is mount order
  and never changes again: React reuses a keyed DropZone when the board reorders,
  so nothing re-registers. From the second keyboard reorder onward, the cursor
  therefore stepped to whichever zone had mounted next rather than to the one
  beside the widget on screen, and the widget landed somewhere the user did not
  ask for. The pointer path was never affected, because it hit-tests the measured
  rects, which meant the broken path was the accessible one.

  The measurement pass already measures every zone's rect at grab time, so the
  cursor now sorts those rects into reading order (row by row, left to right;
  `zonesInReadingOrder` in `drag-drop.geometry.ts`) and walks that instead. Rows
  are swept rather than bucketed by a tolerance, so lanes of very different heights
  still read as one row, and an unmeasured board degrades to the order it was given.
  Zone hit-testing keeps using registration order, which is paint order: that is
  what decides the top-most zone when two overlap.

  Board carried the same latent bug (a consumer that reorders its `columns` moved
  the lanes without re-registering them) and is fixed by the same change.

## 2.51.0

### Minor Changes

- 64876a4: DashboardGrid spells its collection prop `items`, matching every other
  collection-taking component in the kit; `widgets` keeps working as a deprecated
  alias.

  Minor justification: `items` is a new public prop on `DashboardGridProps`, so
  this adds a user-visible capability rather than fixing one. Nothing that already
  shipped changes shape: `widgets` still renders exactly the board it always did.

  Sidebar, StackedList, DescriptionList, Feed, Stats, GridList, Board, Carousel,
  Command, Dropdown, Listbox, TabBar and the rest all take their collection as
  `items`, and DashboardGrid was the single outlier. It now accepts `items` as the
  documented, canonical spelling, and `widgets` resolves to the same board while
  warning once in development toward the new name. `items` wins if both are passed,
  which also warns. Both props are optional, so a board configured with neither
  renders empty rather than throwing.

  The docs, the `.md` examples, and the tests all teach `items` now; the generated
  prop table carries `widgets` with its deprecation note beside it.

## 2.50.0

### Minor Changes

- f1f1627: New GeoMap chart: a token-themed world map with area-encoded coordinate bubbles,
  press to inspect, and a build-time precomputed land silhouette (no runtime fetch,
  no runtime dependencies).

  Minor justification: this adds a new public component to `@nannier/canvas`
  (`GeoMap`, with the `GeoMapPoint` and `GeoMapProps` types), which is a new
  user-visible capability rather than a fix to an existing one. Nothing that
  already shipped changes shape.

  The land is one muted path in the Natural Earth I projection, baked at build time
  by `bun run geomap:gen` and drawn in the generated viewBox's own units, so the
  coastlines, the bubble centers, and the bubble radii share one coordinate space
  and every size follows the rendered width. Each point's AREA carries its count
  (radius proportional to the square root of the share, with a floor that keeps a
  tiny count visible), circles take the primary token over a surface-colored ring so
  overlapping bubbles stay readable, and every colour comes from `useTheme()`, so
  light and dark work with no per-scheme code. Pure react-native-svg: no DOM and no
  `Platform.OS` branch.

  Single-identity encoding, so there is no tone axis and no legend; density
  (`compact`) is the only style axis. The map holds the projection's aspect ratio
  from its own measured width, never the window. Pressing a bubble sets the
  controlled selection, flags the label and formatted count through the shared chart
  value flag, and announces it; the plot's accessible name folds in the title, the
  place count, the biggest places with their values, and a "+N more" tail, because a
  screen reader user cannot see bubbles.

## 2.49.1

### Patch Changes

- 5fba543: Groundwork for the GeoMap chart: the renderer-free Natural Earth I forward
  projection (`projectNaturalEarth`, `NATURAL_EARTH_ASPECT`, `naturalEarthHeight`)
  plus the pre-projected world land silhouette it generates, with unit tests.

  Patch, not minor: nothing is exported from the charts barrel yet, so the
  package's public API is unchanged. The component that consumes this lands next.

  The land data is computed at BUILD time by `bun run geomap:gen`, which reads the
  Natural Earth 1:110m land topology (public domain data, ISC packaging via the
  `world-atlas` devDependency) and projects it through the very same projection
  module the chart will place its bubbles with, so the coastlines and the data
  points cannot drift out of register. The kit therefore ships map geometry with
  no runtime dependency, no network fetch, and no parsing at import: one 5.7KB
  gzip string constant.

## 2.49.0

### Minor Changes

- 54ddb7e: New DashboardGrid organism: a 12-column, container-responsive widget board with locked
  and customize modes, controlled or uncontrolled order, and kit drag reordering (pointer,
  keyboard, screen reader).

  Minor justification: this adds a new public component to `@nannier/canvas`
  (`DashboardGrid`, plus the `clearStoredDashboardOrder` helper and the `DashboardGrid`
  ordering and span exports), which is a new user-visible capability rather than a fix to
  an existing one. Nothing that already shipped changes shape.

## 2.48.1

### Patch Changes

- f705d03: Groundwork for the DashboardGrid organism: the renderer-free layout logic module
  (`orderedWidgets`, `moveWidget`, `effectiveSpan`) plus the `DashboardWidget` and
  `DashboardTier` types, with unit tests.

  Patch, not minor: nothing is exported from the organisms barrel yet, so the
  package's public API is unchanged. The component that consumes this lands next.

  The order functions reconcile rather than throw, because a consuming app persists
  the widget order server-side as ids alone and that array outlives the widget list
  it was captured from: ids missing from the stored order append in their declared
  order, ids matching no widget drop out, and a move naming an unknown id is a
  no-op.

## 2.48.0

### Minor Changes

- 77b479c: Tooltip accepts an arbitrary element trigger (children), showing on the child's
  hover and focus without claiming its press.

  Minor justification (new public capability): `TooltipProps` gains `children`, so
  a tip can hang off a control the app ALREADY has (an icon `Button`, a `Chip`)
  instead of one of Tooltip's three built-in triggers. A console's "Glass on" /
  "Glass off" icon Button can carry a tooltip and keep its own `onPress`, which
  was impossible before: the tip had to be a separate trigger beside the control.

  Trigger precedence, first match wins: `children`, then `iconTrigger`, then
  `textTrigger`, then the default text Button. Passing no children leaves every
  existing call site rendering exactly as before.

  The child renders as-is. The only node Tooltip adds is its root view, which
  takes NO accessibility role and no tab stop, installs no press responder, and
  listens only for hover (RN's `onPointerEnter` / `onPointerLeave`, touch pointers
  skipped) and focus. So the child stays the single interactive, labelled element:
  a `Button` child keeps its `onPress`, never lands inside a second button (which
  would be invalid markup and an ambiguous control), and the tip does not toggle
  on tap.

  The disclosure sits on the ROOT view rather than a wrapper hugging the child on
  purpose. The bubble renders in flow, so opening it pushes the trigger over by
  the bubble's height; a child-hugging hover region would be shoved out from under
  a stationary pointer and the browser's post-layout hover recompute would close
  the tip the instant it appeared (observed in Chrome). The root spans the bubble
  too, so the tip stays up and hovering the bubble itself keeps it up.

  Native fires no hover at all and an element trigger has no tap toggle, so the
  controlled `open` prop remains the native and touch path; the `.md` says so.

## 2.47.0

### Minor Changes

- cd3ac70: Chart: stacked grouped columns.

  Minor justification (new public capability): Chart grouped mode gains stacked
  columns (categories by series accumulating per column) for composition-over-time
  bars. Passing `stacked` alongside `labels` + `series` turns each category's
  cluster into ONE column whose segments accumulate, so "token issuance by client,
  split by grant type" reads as each client's total and its composition instead of
  four bars to compare by eye. AreaChart already had `stacked`; this is the same
  idiom for bars.

  The axis follows the per-category TOTALS rather than the largest single value,
  since the column now encodes the sum. Segments abut with no gap (StackedBar's
  convention) and sit bottom-series-first (the stacked AreaChart's band order);
  only the topmost non-empty segment takes the bar's rounded cap, an empty segment
  paints nothing rather than the 2px minimum a clustered bar keeps, a negative
  value counts as 0, and the running sum is clamped to the plot so a `max` below
  the true total cannot overflow the column. The legend, the press/scrub value
  flag, and the dimming all keep working, and a stacked column's accessible item
  names the category total after its segments (the column height is the total's
  only visual channel).

  `stacked` is grouped-mode only: passing it to a single-series `data` chart
  devWarns and renders unchanged. Omitting it renders the existing clustered
  columns byte-identically.

## 2.46.0

### Minor Changes

- 4c29dd4: Charts: per-series semantic tones.

  Minor justification (new public capability): `ChartSeries` accepts per-series
  `success` and `destructive` tones so semantic multi-series charts colour by
  meaning; unset series keep the chart-1..8 ramp. A sign-ins chart can now paint
  "Granted" green and "Denied" red instead of handing both series the next two
  positions in the categorical ramp, which said nothing about what either series
  means.

  The resolution lives in ONE place, `seriesColor(tokens, series, i, tone)` in
  `charts.styles.ts`: a series' own tone first (success > destructive, the
  chart-level precedence), then the chart-level tone for a single-series chart,
  then the ramp position. It mirrors `rowFill`'s slot > tone > ramp resolution,
  and every consumer of a `ChartSeries` colour routes through it: the shared
  cartesian `colorOf` (LineChart, AreaChart, ComposedChart, RangeAreaChart), the
  grouped Chart's bars, value flag, and legend, RadarChart's polygons, and
  CandlestickChart's overlays. So the plot, the flag, and the legend cannot drift
  apart.

  A series that sets neither boolean renders byte-identically to before, and the
  chart-LEVEL tone props stay single-series-only: their dev warning now points at
  the per-series booleans, which are the sanctioned channel for that intent.

## 2.45.0

### Minor Changes

- 7e6910a: Navbar: brand element and trailing actions slots.

  Minor justification (new public capability): Navbar gains brandContent and a
  free-form trailing actions slot for console topbars; existing
  brand/links/actionLabel/avatar API unchanged. `brandContent` takes any ReactNode
  and LEADS the existing left cluster, so a logo mark renders ahead of the `brand`
  wordmark or stands in for it entirely; `actions` takes any ReactNode and LEADS
  the existing right cluster, ahead of the built-in `actionLabel` button and
  `avatar`, so a bar can carry a ghost search button with a Kbd chip, ghost icon
  buttons, a notification dropdown and an AvatarMenu. Both slots are direct
  children of the group rows already in the skins, so they take those rows' own
  gap and no skin field changed.

  `links` and `brand` are now optional. A bar with no middle nav renders neither
  the links row nor the narrow menu button that stands in for it, so the automatic
  at-and-below-`sm` collapse can no longer produce a hamburger opening an empty
  menu; with links present the collapse is unchanged, and a trailing `actions`
  slot never folds into that menu. Every existing call site renders exactly as
  before.

## 2.44.0

### Minor Changes

- 8753225: Sidebar: error-tone item badges.

  Minor justification (new public capability): Sidebar item badges can carry the
  error status tone for alert counts (badgeError), defaulting to secondary. A
  `SidebarItem` may now set `badgeError` beside its `badge`, and the row renders
  that count through the Badge atom's error status pill (`<Badge status error>`,
  the red dot-and-label form) instead of the default secondary metadata pill, so a
  Security row can report lockouts as a problem rather than a volume. The flag is
  an item-level boolean on the data object, following `RowMenuItem.destructive`,
  and it reaches both presentations of the same row: the rail and the narrow
  drill-down leaf. A row that omits it, or sets it with no `badge`, renders exactly
  as before, and the collapsed rail still folds the count into the row's accessible
  name.

## 2.43.0

### Minor Changes

- 39f1acd: ActionPanel: embedded children between the copy and the action.

  Minor justification (new public capability): ActionPanel accepts children
  rendered between its copy and action, so settings panels can embed field rows.
  `ActionPanelProps.children` is a `ReactNode`, and where it lands follows the
  layout the panel already resolves: stacked, it joins the panel's gap column
  between the copy and the action, so each element of the block is spaced by the
  skin's stacked gap; inline and in toggle mode the action stays pinned beside the
  copy, so the block renders full width below that row on the same rhythm. No skin
  field was added, and a panel that passes no children renders exactly as before.

## 2.42.0

### Minor Changes

- d2d6fa2: Feed: optional icon lead in the connector node.

  Minor justification (new public capability): Feed items may lead with a kit icon
  glyph in the connector node (icon over initials over dot); actor/action/target
  untouched. `FeedItem.icon` names a glyph from the kit icon set
  (`items={[{ icon: "shieldCheck", ... }]}`, typed `IconName`) and renders through
  the `Icon` atom, muted and decorative at 16pt inside the existing 28pt node, so
  an audit or automation stream no longer has to spell a system event as a pair of
  initials. The avatar lead ignores `icon` and keeps leading with the person; items
  that pass no `icon` render exactly as before.

## 2.41.1

### Patch Changes

- 871beea: Container measurement stops breaking hydration in server-rendered apps.

  `useContainerWidth` fell back to the window width before the first layout, and
  `useContainerBreakpoint({ seedViewport: true })` seeded from it the same way.
  On a server render there is no window, so the fallback resolved to 0 and a
  `Grid` shipped its cells with no width; on the client the window is available
  during the hydration render, so the very same cells resolved an explicit pixel
  width. React reported a hydration mismatch and, as it warns, did not patch the
  attributes up.

  Both now withhold the window value for one render, so the hydration pass is
  byte-identical to the server markup and the real width lands in the commit
  immediately after. This is the contract `ThemeProvider`'s `ssrScheme` already
  gives the colour axis. Client-only apps are unaffected: they see the fallback
  from their first commit exactly as before.

## 2.41.0

### Minor Changes

- 6129f30: `BreakpointOverride`: pin the viewport tier for a subtree.

  Minor justification (new public API): wrapping a subtree in
  `<BreakpointOverride value="sm">` makes the `useBreakpoint` / `useResponsive`
  / `useFormFactor` consumers under the provider resolve that bucket instead of
  the real window, so a preview stage or a test can exercise a phone or tablet
  branch inside a desktop window; `value={null}` clears the simulation. Two
  boundaries: mount it ABOVE your OverlayProvider when portaled overlay content
  should simulate too (the kit Portal renders overlays at the provider's
  outlet), and pair it with a width constraint on the same subtree, since
  container-measured components follow their real measured width (the docs
  playground's form-factor switcher does both).

- 9e1f1a7: ButtonGroup: icon segments.

  Minor justification (new public capability): an item may pair its label with a
  kit glyph (`items={[{ label, icon }]}`, new `ButtonGroupItem` type; strings
  keep working untouched), and the group-level `iconsOnly` boolean renders each
  segment as its glyph alone with the label as the segment's ACCESSIBLE name, so
  an icon-only segmented control (a view switcher, the docs' form-factor
  switcher) needs no hand-rolled look-alike. Glyph color tracks each platform
  skin's segment label treatment (new `segmentIconColor` skin field); the
  stepper and split kinds cycle the labels and ignore icons, with dev-only
  warnings on misuse.

## 2.40.5

### Patch Changes

- 43a4d67: Fix Steps `stacks` falsely stacking inside a row parent. The `stacks`
  measurement was attached to the horizontal root itself, but in a row parent
  that root hugs its content, so the first real layout measured the hugged width
  (well below the `sm` breakpoint) and Steps stacked vertically even in a wide
  container, flickering back through the horizontal layout on every relayout
  because the stacked branch spans full width. The measurement now rides the
  out-of-flow `containerProbe` sibling (the Tabs `responsive` fix's mechanism),
  rendered in both states so the stacked branch also tracks the real container
  width and un-stacks when the container widens.

## 2.40.4

### Patch Changes

- 712a574: The web hand-off's base rules move into `@layer base`, so an app can override them.

  `styles/tokens/base.css` emitted `*`, `body`, `a`, and `a:hover` unlayered. An
  unlayered rule outranks every layered one regardless of order, so a consuming
  app could not restyle an anchor at all: `a{color:var(--primary)}` beat
  `.text-primary-foreground`, and an anchor carrying a button's fill painted its
  label in the link colour, which is invisible on a primary fill. The same rules
  inside `@layer base` still win over a framework reset (they are imported after
  it, and layers resolve in declaration order) while losing to the component and
  utility layers. The `@keyframes` blocks stay unlayered, since layers order
  keyframe-name resolution too.

## 2.40.3

### Patch Changes

- f77ad4f: Fix responsive vertical Tabs flattening in wide containers. The `responsive`
  measurement was attached to the vertical rail itself, but the rail hugs its
  content (~180px), so the first real layout always measured at or below the `sm`
  breakpoint and the rail latched into the horizontal underline look even in a
  wide desktop container. The measurement now rides an out-of-flow container
  probe (a zero-height, absolutely positioned sibling that spans the parent,
  shared as `containerProbe` in the style layer), rendered in both states so the
  flattened row also tracks the real container width and restores the rail when
  the container widens.

## 2.40.2

### Patch Changes

- 2ae18cb: Tabs: a non-block underline/pills row longer than its container now pans
  horizontally instead of clipping (an inert-when-fitting horizontal scroller,
  no new prop; effective at any container width). Selecting a tab by press,
  roving arrow key, or a controlled `active` change scrolls it fully into view
  with a neighbor peek, honoring reduced motion. `block` and `vertical` are
  unchanged, and a flattened `responsive` vertical gains the same treatment.

## 2.40.1

### Patch Changes

- dc7bdc8: Docs examples never overflow a phone: 29 fixed-width example style objects
  across 7 component `.md` files (reveal, skeleton, divider, card, slider, chart,
  stacked-bar) now carry `maxWidth:"100%"`, and a new docgen guardrail hard-fails
  any future fence that pins a numeric width of 280 or more without a `maxWidth`
  (the existing `docgen-allow-style` opt-out still applies).

## 2.40.0

### Minor Changes

- ca16126: Pointer-capability hooks: `usePointerCoarse()` and `useHoverCapable()`.

  Minor justification (new public API): the input half of the desktop form
  factor (macOS via the web skin, and desktop web). Native iOS/Android resolve
  as touch-first constants; the web reads the standard `(pointer: coarse)` and
  `(hover: hover)` media features live, so an iPad browser, a touch laptop, and
  a mouse plugged into a tablet all resolve correctly. SSR and the pre-effect
  first frame default desktop-first (fine pointer, hover-capable). Capability
  only: no component behavior changes.

## 2.39.0

### Minor Changes

- d40e41b: Structural narrow modes: Navbar auto-collapse, Steps `stacks`, Tabs
  `responsive`, FilterPanel `responsive` drawer, GridList container basis.

  Minor justification (new capabilities and props):

  - Navbar now collapses AUTOMATICALLY at and below the `sm` container width: the
    links row swaps for a kit-owned menu button opening the platform Dropdown
    (active link checkmarked; `active`/`onSelect` unchanged). No new prop, and a
    deliberate default-behavior decision: the previous narrow rendering was a
    plain row clipping links off-screen, so there was no working behavior to
    preserve. GlassSurface gained an `onLayout` passthrough to support the bar
    measuring itself.
  - Steps: opt-in `stacks` + `stackBreakpoint` (default `sm`) renders the
    EXISTING vertical layout when the component's own container is narrow
    (horizontal layout only; `vertical`/`progress` unaffected).
  - Tabs: opt-in `responsive` renders a vertical rail as the existing horizontal
    underline look at and below `sm` container width.
  - FilterPanel: opt-in `responsive` + `drawerBreakpoint` (default `sm`) collapse
    the docked panel to a kit-owned "Filters (n)" outline Button opening the
    panel in a start-edge Drawer; `open`/`defaultOpen`/`onOpenChange` drive it
    for controlled use.
  - GridList's narrow collapse now measures its OWN container (viewport-seeded)
    instead of the window, so grids inside narrow desktop columns collapse too.

## 2.38.0

### Minor Changes

- 6c6147b: Responsive layout primitives: Row `stacks` and the new `Grid`.

  Minor justification (new public capability):

  - `Row` gains `stacks` (+ `stackBreakpoint`, default `sm`): the row renders as
    a Column when its OWN container is at or below the breakpoint,
    container-measured with a viewport seed, so it stacks inside a narrow desktop
    column too. When stacked the Row is exactly the Column with the same props
    (gap/justify/align/padding apply to the new axes, `wrap` is inert). Children
    keep their own sizing, which makes `stacks` the tool for content-sized rows
    (toolbars); ignored with a DEV warning on Column.
  - New `Grid` + `GridItem`: the container-measured auto-fit tile grid.
    `minTileWidth` (default 240) sets the floor, `columns` caps the desktop
    count, the gap scale is Row/Column's own booleans, and `GridItem wide` spans
    two cells. Pure math (`gridColumns` / `gridCellWidth`, exported) resolves the
    count from the measured container: no breakpoints at the call site, one
    measurement per grid, zero hooks per tile.

## 2.37.4

### Patch Changes

- da32627: Form `twoColumn` stacks by CONTAINER width; Sidebar warns on phone-width rails.

  - Form's two-column collapse now measures the form's own row wrapper instead of
    the window, with a threshold of one `wide` field (480px): a two-up split
    narrower than that cannot give each column a usable field. Behavior change,
    flagged: a `twoColumn` form inside a narrow desktop column (a split pane, a
    docs 3-up) now stacks where it previously stayed two-up and crushed; forms
    560px and wider keep their two-up layout everywhere. New public hook riding
    along: `useContainerWidth()` (own width with a window fallback until the
    first layout).
  - Sidebar: a non-`responsive` sidebar rendering at a phone-width viewport now
    logs a one-time DEV warning pointing at the `responsive` prop; the rail is
    unusable chrome there and the drawer needs a consumer-wired hamburger, so the
    gap is surfaced instead of silently rendering a 240px column.

## 2.37.3

### Patch Changes

- 0c5e629: GridList: the virtualized path now collapses to one full-width column at phone
  widths, matching the eager path; previously it kept 2-3 FlatList columns of
  100%-wide tiles. The grid also resolves its responsive tile width once at the
  parent instead of once per tile, so an N-tile grid carries one viewport
  subscription instead of N.

## 2.37.2

### Patch Changes

- 62ff81b: Measured narrow-container fixes for Calendar, DataTable, DescriptionList, and
  Board.

  - Calendar month: the grid container is now capped at 100% of its parent and
    the seven day cells shrink fluidly (32px floor) when the measured container
    is narrower than the natural grid, so a month calendar fits a 320pt phone
    instead of overflowing. Week/day timelines are untouched (they already
    flexed).
  - DataTable: the 320px minimum-width floor now drops once the table has
    measured a container narrower than the sm breakpoint, where the existing
    collapse/pan machinery guarantees readability; previously the floor clipped
    on 320pt devices.
  - DescriptionList `twoColumn`: the fixed 160px term column narrows to 120px at
    phone widths (restores the pre-refactor behavior lost when Field's display
    rows moved here), keeping the value column readable.
  - Board: lanes now fit a measured narrow board (lane fills the width minus a
    32pt peek of the next lane, 240px floor) instead of staying at the configured
    300px regardless of screen size; `columnWidth` still sets the desktop lane.

## 2.37.1

### Patch Changes

- 29b6955: Fix fixed-width surfaces overflowing narrow containers (phone screens).

  - AnchoredOverlay now clamps a width-aware card to its outlet: when the outlet
    is narrower than the card plus its edge insets, the card renders at outlet
    width minus the insets instead of running off-screen. Popover passes its card
    width through (new `cardWidth` field on `PopoverSkin`), so popover cards and
    the calendar peek both fit phone-width outlets; the popover skins also carry
    `maxWidth:"100%"` for the inline mode.
  - FilterPanel's fixed panel (280 web/iOS, 256 Android) and the Sidebar rail
    (240) gain `maxWidth:"100%"`, so they shrink inside narrower parents.
  - Vertical Tabs' fixed 180px rail now flexes down (96px floor, never past 40%
    of the row) and its labels truncate to one line, keeping the panel usable in
    narrow containers.

## 2.37.0

### Minor Changes

- 9a2f024: Container-measurement primitives: `useMeasuredWidth()` and
  `useContainerBreakpoint()`.

  Minor justification (new public API): the middle tier of the responsiveness
  system. `useMeasuredWidth` measures the element its `onLayout` is attached to
  (stable handler, re-renders only on rounded-width changes);
  `useContainerBreakpoint` resolves a `Responsive` map against the element's OWN
  width instead of the window, with an optional `seedViewport` for
  above-the-fold grids. Components that switch layout should measure their
  container, not the viewport: a component cannot know whether it is on a phone
  or in a 320px desktop panel.

  Internal adoption, no behavior change: the six components that hand-rolled the
  identical trigger-measurement handler (Dropdown, Select, Autocomplete,
  Popover, RowMenu, ButtonGroup) and DataTable's own-width measurement now ride
  these hooks.

## 2.36.0

### Minor Changes

- fb55fe0: Responsive core: shared viewport breakpoint store and form-factor tier.

  Minor justification (new public API): `useBreakpoint()` (the active viewport
  bucket), `FormFactor` / `formFactor(width)` / `useFormFactor()` (the semantic
  phone / tablet / desktop tier over the breakpoints, where desktop covers macOS
  and desktop web), and a `ssrBreakpoint` prop on `ThemeProvider` (the
  `ssrScheme` contract applied to the viewport axis).

  Behavior fixes riding along:

  - `responsive()` / `useResponsive()` now resolve a non-positive width (SSR and
    the pre-layout first frame, where react-native-web reports 0) to `base`, the
    desktop variant. Previously width 0 matched the smallest declared breakpoint,
    so servers and first frames rendered the PHONE branch of every consumer on
    desktop. The kit is desktop-first; unknown viewport now means desktop.
  - All viewport hooks share ONE Dimensions subscription and re-render consumers
    only when the active breakpoint bucket changes, not on every resize event.
  - `breakpoints` is now typed `Record<BreakpointKey, number>`, so indexing it
    with an arbitrary string is a compile-time error instead of a silent
    `undefined`.

## 2.35.3

### Patch Changes

- 53b9aef: InputOTP no longer paints its raw code across the middle of the row on Android.

  The single text input that captures the keystrokes sits over the whole segmented
  row and is meant to be invisible, with the cells doing the drawing. It was
  hidden with `color: "transparent"`, which Android does not honour, so the code
  being typed was painted in the default text colour across the centre of the
  field, on top of the cells. It is hidden with `opacity: 0` now, which every
  platform honours and which changes nothing else: an opacity-0 view still takes
  touches, still focuses, and is still read by assistive tech.

  Caught by the landing-page hero capture, which had been shipping the artefact in
  `input-otp-android.webp` for as long as those shots have existed.

## 2.35.2

### Patch Changes

- 36e45ce: Add a render-based colour check: `bun run check-render`.

  The kit already had two colour guards and both were structurally blind to the same class of error.
  `validate-tokens` compares `styles/tokens/colors.css` to `src/style/tokens.ts`, and `check-parity`
  compares the built types to a committed snapshot. Both sides of both checks live behind this
  commit, so they can only prove the kit is internally consistent. That is exactly how the `--ring`
  error survived: the CSS and the JS agreed with each other while both diverged from the design
  source, and nothing could see it for as long as they agreed.

  This check renders the hand-off's own colour guideline cards in chromium and reads the painted
  pixels, then compares them against the shipped tokens. One side of the comparison is an input
  nobody here can edit into agreement. Rendering rather than parsing also matters: a text parse gets
  `var()` chains, `color-mix(in oklab, …)` and out-of-gamut clipping wrong, while the browser
  resolves all three and a painted pixel is what a user actually sees.

  Verified by faithfully reproducing the ring bug — putting the wrong light `--ring` into BOTH the
  CSS and the JS so they agree, as they historically did. `validate-tokens` passes, `check-parity`
  passes, and this check reports the drift.

  Two legs, and the difference is stated wherever a reader will meet it. `--handoff <path>` renders
  the real export and is authoritative; the default renders a vendored copy under
  `tools/render-parity/handoff/` so CI can run at all. The vendored copy is itself behind this commit,
  so a green CI proves only that the kit has not drifted from the snapshot — refreshing that copy when
  the hand-off changes is where the real comparison happens.

  One accepted difference is recorded in `baseline.json`: dark `warning` is one step apart on the blue
  channel at a rounding boundary, where the exact conversion gives 9.4506 and Chrome paints 10.

  Patch: repository tooling, no change to the published package.

## 2.35.1

### Patch Changes

- 0311e9b: StackedList: the row divider takes `pointerEvents` from a style rather than the
  deprecated prop.

  The hairline between ruled rows was rendered as `<View pointerEvents="none">`.
  React Native has deprecated that prop in favour of `style.pointerEvents`, so
  every render of a ruled list logged "props.pointerEvents is deprecated. Use
  style.pointerEvents", including once per run of the kit's own console gate in
  `test/no-console-violations.test.tsx`.

  The declaration now comes from a module-level `StyleSheet.create`, composed onto
  the divider alongside the skin's hairline style. That is the kit's existing
  convention for this property (see `src/charts/shared/chart-inspect.tsx` and
  `src/organisms/toast/toast.shared.tsx`): react-native-web compiles
  `pointerEvents` into an atomic class only from a registered stylesheet entry and
  silently drops it from an inline style literal, so the registered form is the one
  that keeps the hairline inert to touch on web.

  Behaviour is unchanged on every platform. The divider still carries
  pointer-events none, verified in the rendered web output.

## 2.35.0

### Minor Changes

- ab32446: Backdrop: `twinkle` now scintillates individual bodies instead of fading the whole
  layer, and adds a `scintillate` channel to the exported clock.

  The minor is for the new public API: `BackdropClock` gains `scintillate`, a linear
  0..1 sawtooth at the flare period that an application's own `Backdrop.Custom` art
  can bind to the same way it already binds `flight`, `drift` and `event`.

  The effect itself was close to invisible, and the reason was structural rather than
  a matter of tuning. A twinkling layer multiplied ONE shimmer value into its single
  wrapper, so every body in the field rose and fell together over a 0.55..0.95 range.
  A field that changes brightness as a unit is a global luminance change, and the eye
  adapts straight through it; widening the range would only have made the whole sky
  pulse.

  Twinkling is now differential. A twinkling field is dealt into nine phase buckets by
  a hash of the body index (a hash, not `i % k`, because fields are generated on
  lattices and every k-th body would otherwise land on a regular sub-grid that flashes
  as a pattern). Each bucket is its own Animated.View over its own static Svg, riding
  the shared `scintillate` ramp at its own offset, through a flare curve with a fast
  attack and a long rest. Neighbouring bodies therefore flare at unrelated moments.
  Bodies big and bright enough to have earned one also carry a diffraction glint and a
  white core that ride the same curve, so a flaring star briefly grows spikes and goes
  hot rather than merely getting less transparent, which is what makes the effect read
  at two or three pixels across.

  The flare peaks exactly AT the layer's prominence cap rather than above it, so the
  legibility budget in `backdrop.styles.ts` still means what it says; the added
  contrast comes from the resting floor. Aggregate luminance behind text goes down,
  not up. The Reduce Motion poster still fans the buckets across the flare curve, so
  the still frame is a sky of bright and faint stars rather than one flat field.

## 2.34.0

### Minor Changes

- b38abbf: InputOTP matches the design hand-off's contract and pins its caret.

  Minor because it adds four public capabilities to `InputOTP`:

  - `groups`: split the run into dash-separated chunks (`length={6} groups={3}`
    reads 123-456). On the web skin, which connects cells within a run, each
    chunk now closes and rounds its own ends.
  - `alphanumeric`: accept letters as well as digits and ask for the text
    keyboard. The hand-off spells this as `numeric` with a `true` default, which
    reads backwards against the semantic-prop rule that passing a prop turns it
    on, so Canvas names the inverse (the `hideLegend` / `hideGrid` precedent).
    The default is unchanged: digits only.
  - `defaultValue`: seed the uncontrolled field, cleaned exactly as typed input
    is. Brings InputOTP in line with Input, Select, Autocomplete and Accordion.
  - `autoFocus`: focus on mount, matching Input's `TextEntryProps`.

  It also fixes a behaviour bug. One invisible text input spans the whole
  segmented row, so a tap dropped the native caret wherever the pointer landed,
  which on a partly-entered code is the middle of the string: typing 12, clicking
  the first cell and typing 9 produced 912 rather than 129. The selection now
  sits at the end of the code, so a keystroke always lands in the first unfilled
  cell. A full-range select-all is left alone, so pasting still replaces a
  complete code.

## 2.33.3

### Patch Changes

- fa3f618: ActionSheet: the backdrop now fades in instead of sliding up with the sheet.

  React Native's `Modal animationType="slide"` transforms the whole modal window,
  and the dimmed scrim lives inside it, so the backdrop used to travel up from the
  bottom edge along with the sheet. ActionSheet now drives the motion itself
  (`animationType="none"`, matching Drawer): a stationary full-screen dim layer
  fades from transparent to the skin's alpha while only the sheet slides on
  translateY. The Modal stays mounted through the exit so the slide-out is visible,
  then unmounts, and reduced-motion settings collapse both to zero duration.

## 2.33.2

### Patch Changes

- a177642: Move `Sparkline` from atoms to charts. It is the only component in `atoms` that took a data series
  (`values: number[]`); everything else there renders a single value or none. It now sits beside the
  other bare marks it belongs with, `StackedBar` and `BarList`.

  No API change: the export, its props and its rendering are untouched, and `/components/sparkline`
  is still its docs URL. What moves is the source directory, the barrel it exports from, and its
  grouping in the docs sidebar.

  This is a deliberate divergence from the design hand-off, which files `Sparkline` under atoms. The
  hand-off's own line puts anything needing a legend to be read (`StackedBar`, `Gauge`, `Heatmap`) in
  charts and leaves `Sparkline` out because it shows shape rather than values. That line is
  defensible, but plotting a series is the stronger signal, and grouping the kit's only
  series-plotting atom away from every other series-plotting component made it hard to find. Note
  that `check-parity` compares the prop surface only and does not compare tiers, so this divergence is
  recorded here rather than in `HANDOFF-PARITY.md`.

## 2.33.1

### Patch Changes

- 73e0024: `CodeBlock` paints an opaque surface.

  Its fill was `alpha(muted, 0.5)`, translated literally from a Tailwind `bg-muted/50` in the kit's shadcn-era origins, so a code block sitting over any backdrop showed it straight through the code: a photo, a gradient, or the glass surface mode's own aurora wash. The design hand-off paints this surface flat `var(--muted)`, and a code block is a content surface, which the kit's glass model deliberately leaves solid. The zebra tints, the inline code chip and the terminal chrome are unchanged; only the block's own panel fill moved.

## 2.33.0

### Minor Changes

- 6a57060: Menus are opaque cards in glass mode, and glass stops rewriting a semantic token.

  Glass used to work by overriding one semantic token: `popover` became translucent, and since `GlassSurface` takes its under-fill from that token and `AnchoredOverlay` renders every anchored card through it, every option-list menu in the kit inherited the translucency. Measured on a rendered page, a menu painted `rgba(255, 255, 255, 0.72)` over an SVG lens that deliberately keeps its centre optically flat, so the page behind read straight through between the rows. The design hand-off never did this: its `--popover` is opaque in both schemes, and glass paints from a separate `--glass-tint`.

  So the material now carries its own fill. `glassByScheme` publishes `glass-tint` (`rgba(255, 255, 255, 0.20)` light, `rgba(22, 22, 28, 0.30)` dark, both read from `styles/tokens/colors.css` and cross-checked by `validate-tokens` so the two layers cannot drift), `GlassSurface` defaults its under-fill to that instead of to `popover`, and `popover` and `card` keep their opaque values in every mode. The shipped CSS matches: the `[data-surface="glass"]` popover swap in `styles/tokens/surface.css` is gone, which also means the reduced-transparency and increased-contrast fallbacks genuinely turn the material off now, where before they resolved back to the translucent value.

  Which surfaces take the material follows the hand-off. Popovers, dialogs, action sheets, the command palette, navbars, tab bars and the sidebar are glass. The option-list menus (Dropdown, Select, Autocomplete, AvatarMenu, SplitButton's overflow menu), alert dialogs, toasts and chart tooltips are opaque cards, because a surface a reader picks rows from has to stay legible over whatever is behind it. Content surfaces stay solid as before.

  Breaking for one caller shape, which is why this is a minor rather than a patch: `glassByScheme` changed from `Record<ColorScheme, Partial<ColorTokens>>` to a `GlassTokens` family, so code reading `glassByScheme.light.popover` should read the opaque `colorsByScheme.light.popover` instead. The internal `ToastSkin.solidSurface` flag was removed, which is a skin field rather than a component prop.

## 2.32.0

### Minor Changes

- 072a91e: `Avatar` gains a `tiny` size step, and `AvatarMenu` now hangs its menu from the pill's trailing edge by default.

  New user-visible capabilities (the reason this is a minor, not a patch): a fourth boolean on Avatar's size axis, and a new `alignStart` boolean on AvatarMenu.

  - **`Avatar tiny`** is the 24px disc, joining `small` (28), the default (40), and `large` (48). Precedence on the axis is `tiny` > `small` > `large`. It keeps the 12px initials rather than scaling on down, because a proportional 10px pair of initials stops reading at that diameter. `AvatarGroup` takes it too, with its own overlap row, so a stack of tiny avatars stays uniform.
  - **`AvatarMenu` uses it for the pill's disc**, which is the fix this step exists for: the capsule is 32 / 36 / 40 tall on web / iOS / Android, so a 24px disc restores the intended 4 / 6 / 8 inset. With the 28px `small` disc it had been using, the web pill left only 2 and read as a tight ring around the photo.
  - **`AvatarMenu alignStart`** hangs the menu from the pill's leading edge.

  Behavior change to note when upgrading: `AvatarMenu`'s alignment default is now the TRAILING edge, where it was the leading edge in 2.30.0. A topbar parks the account pill at the trailing edge, and a leading-aligned menu there runs off the surface, so the trailing edge is what the design calls for and what almost every call site was already passing `alignEnd` to get. `alignEnd` still works and still means the same thing (it now spells out the default); pass the new `alignStart` for the old behavior. Plain `Dropdown` is untouched and still defaults to its leading edge.

  Also corrected against the design hand-off: the pill's open fill on web is now the real `color-mix(in oklab, ...)` (computed through a new `mixOklab` colour helper) instead of an sRGB channel lerp that landed 2/255 per channel too light in both schemes, and a disabled `Dropdown` trigger or row on iOS now dims to 0.4, the platform's own disabled opacity, instead of the web's 0.5.

### Patch Changes

- 072a91e: `AvatarMenu` opens its own platform's menu, and stands off by the hand-off's 6px.

  The pill rendered per platform while the menu under it did not: `avatar-menu.shared.tsx` imported `Dropdown` from the barrel, and a bare import resolves the web module in a browser bundler, so the docs' iOS and Android rows opened the web menu. Measured on the page before the fix, all three rows reported the web row metrics (0 min-height, 6px/8px padding, 2px row radius) where the plain Dropdown page reported three distinct skins (44pt iOS rows, 48dp Android, web). `createAvatarMenu` now takes the Dropdown to render, and each avatar platform entry builds it from that platform's own dropdown skin, the same injection `createEmptyState(iosSkin, ButtonIOS)` already uses. On a device Metro resolved this correctly either way, so this was the web preview and any web consumer, not native.

  The menu's standoff moves onto `DropdownSkin` as `menuGap` (4 on all three skins, the value the shell used to hard-code) and the account pill's menu is built at the hand-off's 6. It is skin-owned deliberately: a caller-facing pixel spacing prop on a public component is the re-spacing escape hatch the kit bans, and "6 instead of 4" has no honest boolean name.

- 072a91e: `Dropdown`: the menu takes focus when it opens, hands it back when it closes, and names itself.

  Three accessibility defects in the WAI-ARIA menu pattern, all on the path every app and docs page runs (an overlay host is mounted, so the menu is portaled):

  - **Focus never entered the open menu.** Focus was moved in the same commit that flipped `open`, but a portaled card is held back until the trigger measurement lands, so there was no row to focus yet: the roving arrow keys were dead and the menu sat at the end of the tab order. `AnchoredOverlay` now reports `onCardMount`, fired from an effect inside the card's own subtree (after every row's ref is attached) on both the portaled and inline paths, and the first enabled row takes focus there. No polling, no timeout guess. The focus move passes `preventScroll`, so a menu that renders open from its first commit never yanks the page to itself.
  - **Closing dropped focus on `document.body`.** Escape, a row press, an outside tap, and a controlled close now all return focus to the trigger. A close that did not orphan focus (the app closes the menu after the user has tabbed on) leaves focus exactly where the user put it.
  - **The menu had no accessible name and its identity header was loose generic text.** The menu is named from the header's title, falling back to the section `label`; the header is a `group` (a valid child of `menu`) named from its two lines. It stays unfocusable and out of the roving-focus count, which is still `items.length`.

## 2.31.1

### Patch Changes

- 1612852: Hand-off parity: record the differences the check cannot detect, and state that limit in the report.

  `check-parity` compares the prop SURFACE. Where a name exists on both sides it counts the prop
  satisfied no matter what that name resolves to, so a scale or spacing drift passes silently. That
  blind spot is now written into the report rather than left implied, alongside the second limit:
  the check reads a committed snapshot of the hand-off, so it cannot tell you the snapshot itself has
  fallen behind the design source.

  The first entry is Avatar. The kit's diameters are `tiny 24 / small 28 / default 40 / large 48`
  against the hand-off's `small 24 / default 32 / large 40`, so the scale sits one step high
  throughout. `small` and `large` exist on both sides, which is exactly why the prop check reported
  them satisfied. It was found by measuring rendered avatars, not by this tool. Re-scaling shipped
  avatars would be a visual break for every consumer, so it is tracked rather than done.

  Patch: repository tooling and a generated report, no change to the published package.

## 2.31.0

### Minor Changes

- 2ee9b65: `Dropdown` takes `triggerLabel`, the accessible name for a custom trigger.

  New user-visible capability (the reason this is a minor, not a patch): a custom trigger passed as `children` is a View, so nothing named the button that wraps it. The browser then names it from its contents, which reads the trigger's text nodes back to back with no punctuation and repeats the label of anything nested inside. An account pill announced as "Rachel Chenrachel.chen@example.com Rachel Chen" rather than "Rachel Chen, rachel.chen@example.com". `triggerLabel` puts the name on the button itself, where assistive tech reads it; omit it and the platform's own name-from-contents still applies, so triggers that read fine on their own are unchanged. The default `trigger` button is unaffected: its own text names it.

  `AvatarMenu` now passes its account name through this prop instead of labelling the capsule inside the button, which fixes the same announcement on every AvatarMenu.

## 2.30.0

### Minor Changes

- 914d333: Add `Field`, the form row that owns the message no control renders on its own.

  New user-visible capability (the reason this is a minor, not a patch): nothing in the kit could
  display a validation message. Every field family already owns its label, but helper and error text
  had no home, so callers hand-stacked a `Text` under an `Input` and drifted on the caption scale,
  the destructive tone, and the announcement. `Field` owns that slot: `helper` for the muted hint,
  `error` for the message, and `error` replaces `helper` in place so the row never changes height and
  nothing below it jumps.

  The load-bearing behavior is label delegation. When the row wraps a single field-family control
  (`Input`, `Textarea`, `Select`, `Autocomplete`) that carries no label of its own, `Field` hands the
  label and `required` down to it rather than drawing one alongside, so each platform still places it
  per its own contract: a static title above on web and iOS, the Material 3 in-container floating
  label on Android. A label rendered beside such a control could never float, which is exactly the
  Android divergence this avoids. Any other child (a `Switch`, a group) keeps the static label above.
  Delegation needs all three conditions — one element child, a label-owning control, and no label of
  its own — so two children, a plain view, or a control that already names itself all fall back
  safely rather than being clobbered.

  `Field` also delegates the error STATE, not just the text, so the control paints its destructive
  border while the row paints the message under it; an errored field that showed red text under a
  neutral box read as unfinished. The message is wired to the control with `aria-describedby` and
  announced with `role="alert"`, which the hand-off's own Field does not do.

  This supersedes 93dd68a9 ("remove Field and Fieldset"), and deliberately so. That removal was right
  about the component it removed: the old `Field` wrapped `Input` directly and carried a `rows`
  display mode that `DescriptionList` had already absorbed. This is a different component with a
  different reason to exist — the message slot and the label delegation, neither of which the removed
  one had. `Fieldset` stays removed.

## 2.29.0

### Minor Changes

- 8be8ba3: Add `AvatarMenu`, the account identity pill, to the Avatar family.

  New user-visible capability (the reason this is a minor, not a patch): the kit
  now ships the account-menu anatomy itself, so no app or topbar hand-composes one
  out of an `Avatar`, a hand-rolled name column, and a chevron. `AvatarMenu` is a
  single capsule trigger holding the avatar, the person's name over their email,
  and a trailing chevron that rotates while the menu is open, wired to the kit's
  own `Dropdown` for the menu (including its new identity header, so the name and
  email repeat above the rows).

  Boolean props follow the kit's semantic grammar: `compact` drops the name block
  for a topbar, `alignEnd` hangs the menu off the pill's trailing edge, and
  `disabled` makes the pill inert. The open state has the usual controlled and
  uncontrolled duality (`open` plus `onOpenChange`, interactive out of the box with
  neither), and `items` reuses the existing `DropdownItem` type.

  Per-OS metrics come from the platform skins: a 32px `secondary` capsule on web
  (with an `input`-coloured hairline and a 6% lifted fill when open), a 36pt
  hairline-outlined capsule on iOS that fills with `secondary` when open, and a
  40dp Material 3 tonal pill on Android (`primary` at 12%, 20% when open). The
  trigger announces itself as a menu button and takes its accessible name from the
  account holder, so a screen reader hears the person, not "button".

- 8be8ba3: Alert, Chip, and Toast take `destructive` for the danger tone.

  New user-visible capability (the reason this is a minor, not a patch): `destructive` is the name the intent axis already uses everywhere else in the kit, on Button, on AlertDialog, and on every chart, and it is the name the design hand-off uses on these three components too. Until now these three alone spelled it `error`, so a call site moving between a destructive Button and a destructive Alert had to change vocabulary mid-form.

  `error` keeps working, marked deprecated, and resolves through the same branch as `destructive`, so it paints exactly the same tone and no existing call site changes. Passing both is redundant rather than ambiguous: they share one branch, so the result is the danger tone either way, and the rest of the axis (`success`, `warning`, `info`, and the neutral default) is untouched.

- 8be8ba3: Give `Dropdown` an identity header, trailing-edge alignment, and a disabled trigger.

  Three new user-visible capabilities (the reason this is a minor, not a patch),
  all additive: a Dropdown that passes none of them renders exactly as before.

  `title` and `description` add an identity header block above the menu's rows: the
  title in the popover foreground, the description muted underneath, closed off by
  the card's own hairline before the first row. It coexists with the existing
  `label` section heading, which still sits between the header and the rows. The
  header is plain text, not a menu item, so it takes no tab stop and never enters
  the roving-focus count. The skins carry the gutter per platform (8 x 6 on web,
  16 x 6 on iOS, 16 x 8 on Android, each matching that skin's own section-label
  gutter) over one shared type scale.

  `alignEnd` hangs the menu off the trigger's trailing edge instead of its leading
  edge, for a trigger parked at the end of a bar where a leading-aligned menu would
  run off the surface. It holds on both paths: the inline anchor flips from a
  logical `start` inset to an `end` one, and the portalled path pins the card by an
  inset from the outlet's own edge (so no card measurement and no second layout
  pass). Both are logical, so a right-to-left locale mirrors them.

  `disabled` makes the whole control inert: the trigger dims by each platform's own
  disabled opacity, the press is a no-op, and a controlled `open` cannot force the
  menu out of a disabled Dropdown. The trigger carries `accessibilityState` and its
  `aria-disabled` alias, and both trigger forms now announce `aria-haspopup="menu"`
  alongside `aria-expanded`.

## 2.28.1

### Patch Changes

- 82b8a03: Add a hand-off parity check, so the component layer is guarded the way the token layer already is.
  `validate-tokens` compares every colour and metric against the design hand-off by value, but
  nothing compared the COMPONENT surface, which is how `Field`, `DashboardGrid` and `ChartFrame` sat
  absent from the kit without anything noticing.

  `bun run check-parity` compares the kit's built type surface against a committed snapshot of the
  hand-off's prop contracts and regenerates `HANDOFF-PARITY.md`. It deliberately does not demand
  identical prop names: Canvas's semantic-boolean rule rejects the string-enum props the hand-off
  uses freely, and React Native has no `onClick`. Every difference is adjudicated once in
  `tools/handoff-parity/divergences.json` as either settled (renamed, boolean axis, web-only, not
  offered) or an acknowledged open gap, and the check fails only on a difference recorded in
  neither place, so a hand-off revision surfaces loudly instead of silently.

  Resolving `extends` chains on the kit side is what makes the comparison meaningful:
  `AreaChartProps extends CartesianSeriesProps`, so an own-members-only read reports every inherited
  prop as missing and the result is noise rather than signal.

  Current state: 75 hand-off components, 72 present; 719 props compared, 503 matching by name, 151
  settled divergences, 65 tracked open gaps, 0 unclassified.

  Patch, not minor: this adds no capability to the published package. It is repository tooling plus
  a generated report.

## 2.28.0

### Minor Changes

- 69dac3f: Web hand-off: ship the `surface` and `density` theming axes, which the kit's own
  public API already assumed existed. `setSurface()` and `setDensity()` (exported
  from the package, alongside `getSurface`/`getDensity`) write `data-surface` and
  `data-density` onto the document element, but `styles/canvas.css` shipped no rule
  that responded to either attribute, so on the web both helpers were inert: they
  set an attribute and nothing changed. `styles/tokens/surface.css` and
  `styles/tokens/density.css` are now part of the stylesheet, imported between
  `platforms` and `motion` exactly as the design hand-off orders them.

  With them in place `data-surface="glass"` switches `--popover` to the translucent
  glass fill, sets `--surface-mode`, paints the orb backdrop the frosted panes
  refract, and carries the accessibility fallbacks that turn translucency off under
  `prefers-reduced-transparency`, `prefers-contrast: more`, and `print`.
  `data-density="compact" | "comfy"` remaps the padding steps (`--p-card-pad`,
  `--p-card-gap`, `--p-table-cell-pad-y`) to the values each platform skin already
  declares for that level, so compact under `data-platform="ios"` is iOS's own
  compact metric. Density moves padding only, never type size and never radius.

  Also adds the three z-index reserve tokens the hand-off carries in
  `spacing.css` (`--z-raised: 10`, `--z-dropdown: 40`, `--z-overlay: 50`), so a web
  consumer can layer against the same shallow scale the components use.

  Minor because it adds user-visible capability to a published export path: two
  theming axes a web consumer can now actually switch, and three new tokens. No
  existing token changed value. Verified by diffing all 2214 declarations in the
  hand-off against the shipped CSS (zero mismatches, zero missing) and by reading
  the computed custom properties out of a browser with each attribute applied.

## 2.27.1

### Patch Changes

- 404899a: Colors hand-off: condense the Liquid Glass commentary in `styles/tokens/colors.css`
  so the file fits its 2KB per-file gzip budget again. The file had crossed to 2089B
  against the 2048B cap, failing `check-size` on main. No token changed: all 78
  declarations are byte-identical, and only comment prose was removed.

  The block that shrank was the ~1KB Liquid Glass explanation, which restated at
  length what `src/style/glass-surface` implements and what the glass section of
  `CLAUDE.md` already documents. What a reader of the stylesheet actually needs
  stays: that glass is the functional layer's material and content surfaces remain
  solid, that it is a lens rather than a frost with the bend concentrated at the
  rim, and what `--glass-lens` and `--glass-frost` each are. The file now measures
  1834B gzip, so it carries 214B of headroom rather than the 34B it had before,
  which is what let a single comment edit push it over.

  A per-file exception was considered and rejected: `check-size.ts` justifies the
  `platforms.css` override precisely on the grounds that the 2KB guard must keep
  biting on `colors.css`, so excepting this file would undercut the reason the
  mechanism exists. Since the stylesheet ships to consumers, prose duplicated
  elsewhere is a cost every consumer pays to download.

## 2.27.0

### Minor Changes

- 4cd0950: Treemap: a new chart component for part-of-whole area tiles. Flat one-level `data` lays out through the shared squarified algorithm (Bruls; largest first, near-square aspect ratios, each rect staying attached to its datum's index), rendered as pure Views with no SVG: ramp-colored tiles separated by a card-colored hairline, with the label and formatted value rendered inside only when the tile fits them (measured through the shared text estimator; the card token contrasts with every chart fill). Pressing a tile selects it (the others dim to the shared inspection opacity), flags its value and share, and announces both; pressing between tiles clears; selection is controlled via `selected`/`onSelect` or uncontrolled via `defaultSelected`; `compact` shortens the plot and `formatValue` shapes the values. Nesting and drill-down are deferred scope, named here and in the docs. Minor because it ships a new user-visible chart component, `Treemap`, exported from `@nannier/canvas` (with the `TreemapDatum` type); no existing API changes. Like `Chart` it is a Shared platform treatment (identical on iOS, Android, and the web). Accessibility: the composition lives in the accessible name with values and shares ("Storage: Media 620 (62%), Backups 340 (34%), ..."), independent of which labels fit their tiles. devWarns cover empty `data`, more than 24 tiles, and negative values (zero area).

## 2.26.0

### Minor Changes

- c3a1a2c: FunnelChart: a new chart component for stage-by-stage conversion. Ordered `stages` render as a column of centered trapezoids through the shared funnel layout: each stage's top width proportional to its value, tapering to the next stage's width, the last stage rectangular, ramp-colored with real-text annotations centered on each stage (label, formatted value, and the conversion percent; the annotation text paints the card token, which the palette gates at 3:1 against every chart fill). The percent reads against the previous stage by default; `share` reads every stage against the first. Pressing a stage selects it by its vertical band (the others dim) with deduped announcements; selection is controlled or uncontrolled; `compact` shortens the funnel and `formatValue` shapes the values. Minor because it ships a new user-visible chart component, `FunnelChart`, exported from `@nannier/canvas`; no existing API changes. Like `Chart` it is a Shared platform treatment (identical on iOS, Android, and the web). Accessibility: the funnel is one image whose name walks the stages ("Signup funnel: Visits 1k, Signups 400 (40% of Visits), Paid 120 (30% of Signups)"). devWarns cover empty `stages` and a stage exceeding its predecessor.
- c3a1a2c: RadarChart: a new chart component for polygonal multi-axis comparison. Spoke labels come from `axes` (clockwise from 12 o'clock); each series draws one closed polygon through the shared polar helpers, ramp-stroked over a soft matching wash, on concentric polygon rings at nice tick fractions with a spoke per axis; the outer bound is a nice value above the data max, or the `max` override. Spoke labels render as real RN Text just beyond the outer ring, positioned from the same polar coordinates as the plot (never SVG text, matching the kit's chart typography rule). The tone axis applies to single-series charts (success > destructive, default primary); multi-series charts paint the chart-1..8 ramp with a reachable legend outside the plot image; `compact`, `hideLegend`, `hideGrid`, and `formatValue` behave as elsewhere. Press-to-inspect is deferred scope for this chart, named here and in the docs; the accessible name already carries every value. Minor because it ships a new user-visible chart component, `RadarChart`, exported from `@nannier/canvas`; no existing API changes. Like `Chart` it is a Shared platform treatment (identical on iOS, Android, and the web). Accessibility: the plot's name folds every axis and value per series ("Casey: Coding 8, Design 6, Comms 9; Jordan: ..."). devWarns cover empty `series`, fewer than three axes, an axes/values length mismatch, more than four overlapping polygons, and tone props on multi-series data.
- c3a1a2c: RadialBarChart: a new chart component for concentric attainment rings. One ring per `data` entry, innermost first: a muted full-circle track under a chart-1..8 ramp arc revealed clockwise from 12 o'clock (each ring is a stroked two-half-arc path, since an SVG circle's dash origin sits at 3 o'clock), all sweeping against one `max` (the largest entry by default) so the rings compare attainment rather than shares. A column legend carries the formatted values via `formatValue`; `compact` shrinks the disc; `hideLegend` drops the legend and hoists the image role to the root, StackedBar-style. Pressing a ring selects it by press radius (the others dim to the shared inspection opacity) and announces its share; pressing outside the rings clears; selection is controlled via `selected`/`onSelect` or uncontrolled via `defaultSelected`. Minor because it ships a new user-visible chart component, `RadialBarChart`, exported from `@nannier/canvas`; no existing API changes. Like `Chart` it is a Shared platform treatment (identical on iOS, Android, and the web). Accessibility: the composition lives in the accessible name ("Platform activation: iOS 64%, Android 48%, Web 82%"). devWarns cover empty `data` and more than six rings.

## 2.25.1

### Patch Changes

- 33f5213: The light `ring` token is the hand-off's indigo-500 (`#615fff`, `oklch(0.585 0.233 277.117)`) again, the same value the dark scheme already carried. An earlier pass read the shared value as a dark-mode leak and pointed light `ring` at the light `primary`, which made a focus outline the same colour as the primary fill it often sits on, so the ring vanished on exactly the control it was marking. The ring is deliberately one value in both schemes: it has to read against a light page, a dark page, and the primary fill. Both the CSS custom property and the JavaScript token now carry it, so a React Native call site and the web token layer agree.

## 2.25.0

### Minor Changes

- a688631: BoxPlot: a new chart component for comparing distributions. Each `data` category carries raw `values`; the chart computes the Tukey five-number summary (quartiles by linear interpolation, whiskers at the most extreme data inside the 1.5 IQR fences, outliers beyond) and draws the box, whisker spine with caps, median line, and hollow outlier dots per category on the cartesian frame, with the y domain hugging whisker ends and outliers rather than zero. Scrubbing a category flags Max/Q3/Median/Q1/Min and dims the others, with deduped announcements; selection is controlled or uncontrolled as on every cartesian chart. The tone axis resolves success > destructive (default primary); `compact`, `hideGrid`, `hideAxes`, and `formatValue` behave as elsewhere. Minor because it ships a new user-visible chart component, `BoxPlot`, exported from `@nannier/canvas` (with the `BoxSample` type); no existing API changes. Like `Chart` it is a Shared platform treatment (identical on iOS, Android, and the web). Accessibility: the plot's name gives each category its full summary ("us-east: median 46, quartiles 42 to 49, range 38 to 58, 1 outlier"). devWarns cover empty `data` and categories with fewer than 5 finite samples.
- a688631: Histogram: a new chart component for auto-binned frequency distributions. Pass raw sample `values`; the chart bins them into nice-edged uniform buckets (Sturges' rule by default, `bins` to override) and draws contiguous top-rounded bars on the cartesian frame's numeric x axis, with bars and press/scrub hit-testing both going through the frame's x scale (the frame nices the numeric domain, so bins neither start at pixel 0 nor tile the plot). Press or drag-scrub a bar to flag its range and count, with deduped announcements; selection is controlled via `selected`/`onSelect` or uncontrolled via `defaultSelected`, and the other bars dim while one is inspected. The tone axis resolves success > destructive (default primary); `compact`, `hideGrid`, `hideAxes`, and `formatValue` behave as on the other cartesian charts, with `formatValue` shaping bin edges in ticks, the flag, and the accessible name. Minor because it ships a new user-visible chart component, `Histogram`, exported from `@nannier/canvas`; no existing API changes. Like `Chart` it is a Shared platform treatment (identical on iOS, Android, and the web). Accessibility: the plot's name lists every bin with its bounds and tally ("Latency ms: 30 samples in 6 bins: 30 to 40 4, ..."). devWarns cover empty `values` and input with no finite samples.
- a688631: WaterfallChart: a new chart component for the running-total bridge (a P&L walk, a headcount bridge). Each step floats from the running total by its signed `value`; a `total` step draws an absolute bar from zero, either snapshotting the running total (omit `value` or pass 0) or opening/re-basing it to a non-zero `value` (the "Q2 total, then the walk, then Q3 total" authoring shape). The coloring is fixed semantics rather than a prop, so every bridge reads the same way: rises green, falls red, totals the brand primary; hairline connectors link each bar's end to the next bar's start. Scrubbing a step flags its change and running total (totals flag just the total) and dims the others, with deduped announcements; selection is controlled or uncontrolled as on every cartesian chart; `compact`, `hideGrid`, `hideAxes`, and `formatValue` behave as elsewhere. Minor because it ships a new user-visible chart component, `WaterfallChart`, exported from `@nannier/canvas` (with the `WaterfallStep` type); no existing API changes. Like `Chart` it is a Shared platform treatment (identical on iOS, Android, and the web). Accessibility: the plot's name walks the bridge ("Q2 total 4.2k, New up 980 to 5.2k, Churn down 540 to 4.6k, Q3 total 4.6k"). devWarn on empty `steps`.

### Patch Changes

- a688631: Colors: align every semantic color token in `src/style/tokens.ts` with the web
  hand-off (`styles/tokens/colors.css`), which is the source of truth for what the
  tokens ARE. The hand-off authors its values in `oklch()`; the JS token set carried
  hand-transcribed Tailwind v3 hexes instead, so the two sides had drifted on 13
  values and a component painted one color natively while the CSS published another.
  Six of those were plainly visible: `destructive` was `#dc2626` light / `#ef4444`
  dark against the hand-off's `#e7000b` / `#ff6467`, and `primary` (with `ring`,
  which tracks it) was `#4f46e5` / `#6366f1` against `#4f39f6` / `#615fff`. The rest
  were sub-perceptual: `primary-foreground` and `destructive-foreground` resolve to
  `#fafafa` rather than pure white, and `muted-foreground` and `warning` shift by one
  or two 8-bit steps. Every token now carries the exact sRGB rendering of its
  hand-off `oklch()`, so a native build and a web build paint the same pixel.

  The `chart-1..8` series, the fixed brand constants, and the Tailwind v3 `palette`
  steps were already in agreement and are unchanged.

  `scripts/validate-tokens.ts` now cross-checks the two sides by VALUE, converting
  each `oklch()` declaration back to sRGB and failing the build on any difference.
  It previously checked only that every JS token NAME existed in the CSS, which is
  what let the values drift apart unnoticed.

  Patch, not minor: no new component, API, option, or platform. This corrects
  existing token values to the specification they were always meant to carry.

## 2.24.0

### Minor Changes

- 0bb0f0e: ComposedChart: a new chart component for mixed marks on one categorical axis. Each series extends `ChartSeries` with per-series `line` and `area` booleans (precedence line > area > bars, first match wins): bar-kind series split each band as grouped columns with the 2px spacer, area-kind series paint a gradient wash behind everything, and line-kind series stroke on top, with `dots` marking line/area data points (auto-suppressed when bands drop under 14px, matching LineChart) and `curved` bending the paths. The chart rides the shared cartesian core, so it inherits the whole contract: one zero-based y axis (a dual axis is named deferred scope), the chart-1..8 series colors, scrub-to-inspect with the value flag and deduped announcements, controlled and uncontrolled selection, the reachable legend outside the plot image, `compact`, `hideLegend`/`hideGrid`/`hideAxes`, and `formatValue` flowing into the accessible name that folds every series and value. Minor because it ships a new user-visible chart component, `ComposedChart`, exported from `@nannier/canvas` (with the `ComposedSeries` type); no existing API changes. Like `Chart` it is a Shared platform treatment (identical on iOS, Android, and the web). The docs point bar-only grouped data at the bar `Chart`'s grouped mode; composed earns its keep when the marks mix.
- 0bb0f0e: RangeAreaChart: a new chart component for min/max envelopes (forecast bands, error envelopes, daily ranges). Each label carries a `{ low, high, mid? }` range: the band renders as a translucent tone wash between the low and high edges (`areaBandPath`), the optional mid values draw a solid line through it, and `curved` bends both with the monotone cubic. The y domain hugs the data rather than anchoring at zero, since an envelope is a range idiom; the frame nices it. Scrub-to-inspect selects a column and flags High, Mid, and Low with deduped announcements, controlled via `selected`/`onSelect` or uncontrolled via `defaultSelected`. The tone axis resolves success > destructive (default primary); `compact`, `hideGrid`, `hideAxes`, and `formatValue` behave as on the other cartesian charts. Minor because it ships a new user-visible chart component, `RangeAreaChart`, exported from `@nannier/canvas` (with the `RangePoint` type); no existing API changes. Like `Chart` it is a Shared platform treatment (identical on iOS, Android, and the web). Accessibility: the plot's name folds every range ("p50 to p99: Jan 42 to 118 around 61, ..."), summarizing past 24 points. devWarns cover empty or mismatched `labels`/`data` and an inverted pair (`low` > `high`), which is swapped after warning.

## 2.23.0

### Minor Changes

- 73c67e3: BulletChart: a new chart component for goal-attainment rows. Each `data` row is a leading label, a track holding qualitative background bands (`ranges`, ascending bounds painted in fading muted washes, widest first so the denser washes sit on top), the tone-colored measure bar, an optional vertical `target` tick, and the trailing formatted value; following the classic bullet-graph anatomy each row carries its own scale (its largest value, target, or bound), and `max` forces one shared scale when the rows genuinely share a unit. The measure tone resolves success > destructive (first match wins, default primary); `compact` tightens rows and thins bars; `formatValue` formats values (default compact k/M/B). Minor because it ships a new user-visible chart component, `BulletChart`, exported from `@nannier/canvas` (with the `BulletDatum` type); no existing API changes. Like `Chart` it is a Shared platform treatment (identical on iOS, Android, and the web). Accessibility: each row is one accessible item composing value and target ("Revenue: 275 of target 300"); bands, bar, and tick are decorative. devWarns cover empty `data`, out-of-order `ranges`, and data exceeding an explicit `max`.
- 73c67e3: ProgressRing: a new chart component, the full-circle sibling of the semicircular Gauge. A muted track ring and a tone-colored value arc revealed clockwise from 12 o'clock with rounded caps (the ring is a stroked path built from two half arcs, because an SVG circle's dash origin sits at 3 o'clock), the whole-percent readout centered inside, and an optional `label` below the graphic. The API mirrors Gauge exactly: `value` 0-100 (clamped with a devWarn outside the range), the tone axis `primary` / `success` / `warning` / `destructive` with precedence success > warning > destructive, and the same rounding split (the readout and the accessible name round while the arc keeps the fraction). `compact` shrinks the graphic from 120 to 96; per-instance sizing remains a separate, deferred item, as on Gauge. Minor because it ships a new user-visible chart component, `ProgressRing`, exported from `@nannier/canvas`; no existing API changes. Like `Gauge` it is a Shared platform treatment (identical on iOS, Android, and the web). The accessible name announces "label: N%" with the same number the eye sees.

## 2.22.0

### Minor Changes

- 33ac55e: ServiceHealthList: a new chart component for the status-overview card. One row per service: a status dot (per-item booleans resolving down > degraded, first match wins, operational otherwise), the truncating service name, an optional right-aligned `detail` string ("99.98%"), and, when the item carries `periods`, an embedded mini uptime strip on a second line rendered by the same internal strip module `UptimeBar` uses, so the two components never drift (both export the `UptimePeriod` type). `onPressItem` turns each row into a drill-in button with the platform press affordance; `compact` hides the embedded strips and tightens the rows; `plain` strips the card surface for nesting inside an existing card, mirroring `Stats`. Minor because it ships a new user-visible chart component, `ServiceHealthList`, exported from `@nannier/canvas`; no existing API changes. Like `Chart` it is a Shared platform treatment (identical on iOS, Android, and the web). Accessibility: each row composes label, status, and detail into one accessible name ("Dashboard: degraded, 99.92%"), the embedded strip carries its own tallying summary, and the status dot is decorative. devWarn on empty `items`.
- 33ac55e: UptimeBar: a new chart component for the statuspage strip. A single row of per-period status pills, oldest on the left, each period a plain object whose status booleans resolve down > degraded > unknown (first match wins; an unmarked period is operational), colored through the shared status hues so a degraded pill reads the same amber as a warning badge. An optional `caption` summarizes the strip above it ("99.98% uptime"), and `startLabel` / `endLabel` caption the strip's physical edges below; `compact` shortens the pills. The strip is a time axis, so pills and edge captions keep physical left-to-right ordering even under native RTL, matching the plot convention of the cartesian charts. Minor because it ships a new user-visible chart component, `UptimeBar`, exported from `@nannier/canvas` (with the `UptimePeriod` type); no existing API changes. Like `Chart` it is a Shared platform treatment (identical on iOS, Android, and the web). Accessibility: the strip is one image whose name tallies every status with zero counts omitted ("API uptime, 90 periods: 87 operational, 2 degraded, 1 down"); the caption and edge labels are real text outside it. devWarn on empty `periods`.

## 2.21.0

### Minor Changes

- 8ba9073: BarList: a new chart component for the ranked label/value list (top pages, referrers, sign-up sources). Each row carries a color swatch, a truncating label, a right-aligned formatted value, an optional Stats-style `delta` string toned by `down` (with `steady` for qualifiers, muted, taking precedence), and a proportional track bar. Bars size against the largest row by default; `share` sizes them against the sum of the rows and appends muted percent readouts. Row colors follow the chart-1..8 ramp by index, with per-row `chart1`..`chart8` boolean slot overrides; a list-level `success` or `destructive` tone paints single-hue lists (precedence success > destructive; a row's slot beats the tone, with a devWarn when both are passed). `onPressItem` turns every row into a drill-in button with the platform press affordance; `compact` tightens the rows; `plain` strips the card surface for nesting inside an existing card, mirroring `Stats`. `formatValue` formats values (default compact k/M/B). Minor because it ships a new user-visible chart component, `BarList`, exported from `@nannier/canvas`; no existing API changes. It complements rather than replaces the bar `Chart`: `Chart horizontal` compares magnitudes on a shared axis, while BarList is the ranked list idiom with deltas, shares, slots, and drill-in. Like `Chart` it is a Shared platform treatment (identical on iOS, Android, and the web). Accessibility: every row is one accessible item whose name folds the label, formatted value, share percent (in `share` mode), and the delta with its direction spelled out; the swatch and track are decorative. devWarns cover empty `items` and negative row values (treated as 0 by the bars).
- 8ba9073: MetricBreakdown: a new chart component for the decomposed-metric dashboard card. It stacks a preformatted headline `value` and `label`, an optional secondary `rate` readout with `rateLabel` (toned by the slot-scoped `rateSuccess` / `rateWarning` / `rateDestructive` booleans, first match wins, default muted; slot-scoped because the tone colors the rate readout, not the card), an optional trend strip (`spark`) rendered through the kit `Sparkline` line variant with a floating latest-value tag suffixed by `sparkUnit` (the tag inherits the rate tone when one is set), per-category `breakdown` rows with proportional share bars and Stats-style `delta` strings toned by `down`/`steady`, and a `chips` footer rendered with the kit `Chip` atom behind an optional `chipsLabel` (no default: a kit component does not assume the domain). Row colors follow the chart-1..8 ramp by index with per-row `chart1`..`chart8` boolean overrides; the v2-era chart-1 spark color is deliberately dropped in favor of the Sparkline's default primary tone. `compact` tightens density; `plain` strips the card surface for nesting inside another card, mirroring `Stats`; `formatValue` formats row values and the spark tag. Every section is independently optional. Minor because it ships a new user-visible chart component, `MetricBreakdown`, exported from `@nannier/canvas`; no existing API changes. Like `Chart` it is a Shared platform treatment (identical on iOS, Android, and the web). Accessibility: the card is text-first; the headline, captions, and rate are real text, each breakdown row is one accessible item announcing label, formatted value, and share of the total, the trend carries a data-derived accessible name, and the floating tag, swatches, and tracks are decorative. devWarns cover a single-point `spark` (skipped) and negative row values.

## 2.20.1

### Patch Changes

- bf6d623: Alert resolves its width measure axis `block` > `wide` > `narrow`, the order the design hand-off uses; v2.20.0 shipped the axis with the reverse first-match order. The order only decides what happens when a call site passes more than one measure, which the docs already tell you not to do, so nothing that passes a single measure changes. `block` leading is also the better reading: asking a banner to fill its container is the most specific of the three instructions, so it should not lose to a cap.

## 2.20.0

### Minor Changes

- b0863db: Alert gains the width measure axis, a new public capability: boolean props `narrow` (320px cap), `wide` (640px cap), and `block` (fill the container, no cap) around the default 480px cap. Every measure is a maximum, never a floor: the banner rides width 100% under the cap and still shrinks to its container, so a column of alerts is the same measure top to bottom. The caps sit one step up the field width ladder (narrow 320 matches a base-width field, the default 480 matches a wide one), so a banner over a form lines up with its fields. Axis precedence is first-match narrow > wide > block.
- b0863db: DataTable: the hand-off's row-interaction suite. Passing `onRowEdit` and/or `onRowDelete` adds a trailing actions column of icon buttons at each platform's touch-target size (28px web, 44pt iOS, 48dp Android with a borderless ripple). The pencil opens the row's edit mode: every string cell becomes a skin-styled field (the first takes focus) with a trailing Save/Cancel pair, and Save fires `onRowCommit(rowIndex, cells)` with the edited cells in place while custom ReactNode cells pass through unchanged. Delete is second-press confirm: the first press arms the bin (it turns destructive and its accessible name changes to "Confirm delete ..."), a second press within the confirm window fires `onRowDelete(rowIndex)`, and any other table press or the window lapsing disarms. The new `inlineEdit` boolean lets a string cell be pressed straight into a field: Enter or blur commits via `onCellCommit(rowIndex, colIndex, next)` when the value changed, Escape restores the cell. All indices report ORIGINAL `rows` positions, stable under sorting and paging, and the action buttons fold the row's first cell text into their accessible names. Minor because it adds new public API to a shipped component: the `inlineEdit` boolean plus the `onRowEdit`, `onRowDelete`, `onRowCommit`, and `onCellCommit` callback props. Everything existing is unchanged; tables without the new props render exactly as before.
- b0863db: Gauge: the chart is now the hand-off's semicircle anatomy. The full-circle ring with the value and label centered inside is replaced by a 180 degree top semicircular arc (muted track plus a tone-colored value arc with rounded caps), the percent readout sitting in the open center of the semicircle, and the label below the graphic. The readout (and the accessible name with it) now rounds to a whole percent, matching the hand-off; the arc still fills by the exact fractional value. Minor because it changes the user-visible rendered look of a shipped chart to the design hand-off's geometry. The API is unchanged: `value`, `label`, `testID`, `style`, and the tone axis (default primary; precedence success > warning > destructive) all work as before. The graphic keeps its fixed 120 width; per-instance sizing remains a separate, deferred item.

## 2.19.1

### Patch Changes

- 0629dd5: Fix the web glass lens blanking the page under a Trusted Types CSP

  `glass-lens.ts` injected its SVG filter defs by assigning markup strings to
  `innerHTML`. Under a `require-trusted-types-for 'script'` Content-Security-Policy,
  Chromium throws a `TypeError` on that assignment, and because the shared
  `#cds-glass-lens` def is injected at module-import time the throw escaped the
  module factory before React could mount: every consumer serving that CSP rendered
  a blank page instead of an app. The published docs site did exactly that.

  Both defs are now built as DOM nodes with `createElementNS` and `setAttribute`,
  which touches no Trusted Types sink and needs no policy, so the lens renders
  identically under every CSP. The filter geometry moved from markup-string builders
  to pure spec builders (`sizedLensFilterSpec`, `sharedLensFilterSpec`) that describe
  the tree; rendered output, rim geometry, and the displacement-map data URI are
  unchanged. Kit source is now linted against every Trusted Types sink so this class
  of failure cannot reach a release again.

## 2.19.0

### Minor Changes

- 9823224: ButtonGroup gains a `block` boolean, an orthogonal layout modifier matching the hand-off's ButtonGroup axis: the group stretches to the container width and the segments share the space equally (`flex: 1` gives each a zero flex-basis, so labels of different lengths still split evenly). It applies to the segmented and spaced kinds; the split and stepper kinds are fixed-width chrome (a chevron trigger, prev/next arrow cells), so they ignore it with a dev-only warning.

  Minor justification: new public API (the `block` prop on ButtonGroup), not a fix to existing behavior.

- 9823224: Emblem gains a `warning` tone boolean on the existing tone axis (primary / destructive / success / warning / muted): the amber caution emblem from the hand-off. It follows the same recipe as the other tones, a 12 percent wash of the `warning` token behind the square and the solid token on the glyph or monogram, and slots into the fixed tone precedence after `success`.

  Minor justification: new public API (the `warning` prop on Emblem), not a fix to existing behavior.

- 9823224: Gauge: new `warning` tone boolean on the tone axis. `<Gauge warning />` fills the arc with the kit's shared warning amber (the same statusHues hue a warning badge or alert reads), for dials like a budget-used gauge. Minor because it adds a new public prop, a user-visible tone capability; the existing tones and the default primary fill are unchanged. Tone precedence within the axis is success > warning > destructive (first match wins).
- 9823224: Tabs items accept a per-item `disabled` flag: an item may now be `{ label, badge?, disabled? }`, and a disabled trigger renders through the skin's dimmed disabled treatment, is not pressable, sits out of the tab order, is skipped by the roving arrow-key navigation (Home/End redirect to the nearest enabled tab), and announces itself via `accessibilityState.disabled` plus the `aria-disabled` alias.

  Minor justification: new user-visible capability on the public Tabs API (individually disabled tab triggers on the existing `tabs` items array).

## 2.18.1

### Patch Changes

- a314646: Fix the web Liquid Glass lens's displacement geometry. The shared percentage-sized filter silently mis-rendered in Chromium's reference-filter path: the ramp images did not cover the element, most of the surface sampled a transparent-black map, and the two displacement passes compounded that into a large position-dependent smear (content under a glass bar appeared shifted tens of pixels, and content entering at an edge read unblurred before snapping to frost). The lens is now generated per surface size with pixel-unit geometry (`filterUnits="userSpaceOnUse"`, a map image whose intrinsic size is the filter region), acquired on layout and refcounted so equal sizes share one def and resizes clean up after themselves. Rims are now a constant 12px like the real material instead of proportional to the element, a single dual-channel displacement pass replaces the two-pass chain, and the shared `#cds-glass-lens` def the CSS token points at carries the blur+saturate grade only, so raw-CSS consumers get a correct frost rather than a broken lens.

## 2.18.0

### Minor Changes

- 9af2828: ThemeProvider speaks the color scheme in boolean grammar: `<ThemeProvider dark>` forces the dark scheme, `<ThemeProvider light>` the light one, and passing neither follows the OS appearance, matching the glass/solid surface axis and every component axis (the prop name is the value). Axis first-match: `dark` wins over `light`, and both win over the legacy `scheme` value prop, which stays supported for config-driven code that already holds a scheme value (a stored preference, an `<html>` hook). `ssrScheme` and the resolved `useTheme().scheme` are untouched.

  Minor justification: new public API on ThemeProvider (the dark/light boolean axis), not a fix to existing behavior.

## 2.17.0

### Minor Changes

- 3ed5114: Glass surface mode now renders as a real Liquid Glass LENS on Chromium web: an SVG displacement filter (refraction concentrated at the rim, the centre optically flat, with the blur + saturation built in) injected once per document and applied as the glass material's backdrop-filter through GlassSurface. It needs no optional module, sits above the expo-blur frost in the material ladder (the frost stays the tier for non-Chromium web, Android, and iOS < 26, and the translucent popover fill stays the last resort), matches the shipped CSS token `--glass-lens: url(#cds-glass-lens)`, and keeps the Reduce Transparency / Increase Contrast opaque rungs intact.

  Minor justification: new user-visible capability of the published web glass material (the lens tier), not a fix to existing behavior.

## 2.16.0

### Minor Changes

- c37c7df: ThemeProvider speaks the surface mode in boolean grammar: `<ThemeProvider glass>`

  The one string-valued switch left on the provider joins the kit's semantic
  boolean axis convention. `glass` forces the translucent functional layer on,
  `solid` forces the flat look, and passing neither keeps the platform default
  (glass on iOS 26+, solid everywhere else). Axis first-match: `glass` wins over
  `solid`, and both win over the legacy prop.

  This is the minor's user-visible capability: a new public prop pair on
  `ThemeProvider`, making the provider's call-site grammar match every component
  axis (`<ThemeProvider scheme="dark" glass>` beside `<Button primary large>`).

  `surface="solid" | "glass"` remains supported unchanged, for back-compat and for
  config-driven code that already holds a `Surface` value; the resolved value the
  theme context carries (`useTheme().surface`) and the web DOM helper
  (`setSurface("glass")`) are untouched.

## 2.15.0

### Minor Changes

- 37cbbe9: Add `brandColors` and `statusHues` to the token layer, and fix the light-mode `--ring`

  **New public API, which is what makes this a minor.** Two exports join the style
  foundation, both reachable from the package root:

  - `brandColors` (with its `BrandColors` type): the three fixed brand constants
    that do NOT flip with the scheme, `orb-indigo` / `orb-violet` / `orb-cyan`.
    The CSS layer has shipped these as `--orb-*` since the token handoff, but there
    was no JavaScript equivalent, so a React Native surface (and any docs page)
    had to hard-code the hexes. Keys are the CSS custom-property names verbatim, so
    `brandColors["orb-indigo"]` and `var(--orb-indigo)` name the same token, and
    `bun run validate-tokens` now fails when a key here has no matching `--name` in
    the shipped CSS (the guard `lightColors` already had). That guard checks the
    name EXISTS, not that the two layers carry the same value; cross-checking values
    is a separate change, since the CSS states colors in oklch and the JavaScript in
    hex.
  - `statusHues` (with its `StatusTone` type): the one status-tone to palette-hue
    map, `success` to green, `warning` to amber, `error` to red, `info` to blue.
    Alert, Badge and Chip each carried a private, identical copy; all three now read
    this one, so a toned Alert, a status Badge and a status Chip cannot drift apart. It names the hue only,
    never a step, so each component keeps its own step ladder. No rendered color
    changes.

  **Fix:** the light-mode `--ring` in `styles/tokens/colors.css` shipped the DARK
  primary (`oklch(0.585 0.233 277.117)`) in both `:root` and `.dark`. Ring tracks
  primary per scheme, which the JS tokens have always done (`#4f46e5` light,
  `#6366f1` dark), so `:root --ring` is now the light primary
  `oklch(0.511 0.262 276.966)`. Focus rings on a light web surface pick up the
  slightly deeper indigo they were always meant to have; dark is unchanged.

- 37cbbe9: Add the `Swatch` atom: a color sample built from a filled rounded block plus the
  label column it owns (the token name as `children`, a primary mono `value` line, an
  optional secondary mono `detail` line), the anatomy a design-system color sheet
  repeats down a page.

  New user-visible capability (why this is a minor, not a patch): a new exported
  component with its own boolean axes, `small` / `large` for the block edge, `circle`
  for the shape, `inline` to move the label column beside the block, and `block` for a
  full-width ramp bar whose size reads as its height. The block always carries a
  `border`-token hairline, so a sample of `background` or `foreground` stays visible
  against the surface behind it in both schemes, and the root ships a data-carrying
  accessible name assembled from the name and value (falling back to the color string),
  since its image role hides the rendered lines from assistive tech.

## 2.14.0

### Minor Changes

- 9094fed: Ship the design tokens as plain CSS, and drop Tailwind from the stylesheet

  `styles/canvas.css` is now a manifest of nine token files under `styles/tokens/`
  (colors, palette, typography, spacing, radius, shadows, platforms, motion, base).
  It is plain CSS custom properties end to end: no `@import "tailwindcss"`, no
  `@theme`, no `@custom-variant`, no build step. A bare `<link>` resolves it.

  New capability, which is what makes this a minor: the published stylesheet now
  carries the **platform skin layer**, 732 `--p-*` custom properties that switch
  the whole look from one attribute, so a web surface can render the iOS 26 / HIG
  and Material 3 skins alongside the Canvas web look:

  ```html
  <div data-platform="ios">…</div>
  <div data-platform="android">…</div>
  ```

  That takes the shipped token surface from 71 properties to 932, adds the glass,
  delta and panel token families, and completes the radius scale (`--radius-none`
  through `--radius-3xl`, plus the per-platform shape aliases). Every existing
  token keeps its name and value; the one change is `--radius-sm`, now 2px inside
  the full scale, where it used to be 4px as part of a four-step set.

  Scheme still keys off `.dark` on the root element. The browser floor drops,
  because the layer now needs only `oklch()` and `color-mix()`: Firefox 113
  instead of 128, since `@property` and cascade layers are gone.

  `hsl(name)` (the web token helper) returned `hsl(oklch(…))`, invalid in every
  browser, because token values stopped being HSL triplets. It now returns the
  token value as-is, and applies alpha with `color-mix()`.

  **Migration, required for apps that use Tailwind utility classes.** Canvas's
  stylesheet used to be the Tailwind entry point by side effect, so importing it
  generated every utility in the consuming app. It no longer does. An app that
  writes `className="flex p-6 text-muted-foreground"` must own its own Tailwind
  setup: add these above the Canvas import in your global stylesheet.

  ```css
  @import "tailwindcss";
  @import "@nannier/canvas/styles/canvas.css";

  @custom-variant dark (&:where(.dark, .dark *));

  /* Canvas tokens as Tailwind colours, so bg-primary and friends resolve. */
  @theme inline {
    --color-background: var(--background);
    --color-foreground: var(--foreground);
    --color-card: var(--card);
    --color-card-foreground: var(--card-foreground);
    --color-popover: var(--popover);
    --color-popover-foreground: var(--popover-foreground);
    --color-primary: var(--primary);
    --color-primary-foreground: var(--primary-foreground);
    --color-secondary: var(--secondary);
    --color-secondary-foreground: var(--secondary-foreground);
    --color-muted: var(--muted);
    --color-muted-foreground: var(--muted-foreground);
    --color-accent: var(--accent);
    --color-accent-foreground: var(--accent-foreground);
    --color-destructive: var(--destructive);
    --color-destructive-foreground: var(--destructive-foreground);
    --color-success: var(--success);
    --color-success-foreground: var(--success-foreground);
    --color-warning: var(--warning);
    --color-warning-foreground: var(--warning-foreground);
    --color-border: var(--border);
    --color-input: var(--input);
    --color-ring: var(--ring);
    --color-chart-1: var(--chart-1);
    --color-chart-2: var(--chart-2);
    --color-chart-3: var(--chart-3);
    --color-chart-4: var(--chart-4);
    --color-chart-5: var(--chart-5);
    --color-chart-6: var(--chart-6);
    --color-chart-7: var(--chart-7);
    --color-chart-8: var(--chart-8);
  }
  ```

  Apps that only use Canvas components and `var(--token)` need no change.

## 2.13.0

### Minor Changes

- 880b4c2: ThemeProvider: new `ssrScheme` prop, the SSR/SSG hydration contract for scheme-aware colors.

  Minor justification: new public API capability. Server-rendered apps (Next.js
  static export and the like) can now tell the provider which scheme the server
  resolved. The provider renders that scheme on the server and for the hydration
  render, so the client's first render matches the server HTML exactly, then
  applies the real `scheme` right after mount; the switch re-renders every
  consumer, which writes the correct colors to the DOM. Without this, a client
  whose scheme differs from the server default (stored preference, OS dark mode)
  hits a React hydration attribute mismatch, and React keeps the server's inline
  colors on any component that never re-renders again: kit components appear
  stuck in the server's scheme after a refresh. Omitting the prop keeps the
  existing single-pass behavior; client-only apps and native are unaffected.

## 2.12.0

### Minor Changes

- 9e4cbc2: Card owns its rhythm: a padded surface now also spaces its flat children (padding implies gap), so a stack of Typography lines inside a Card needs no layout wrapper. The gap follows the platform density table: 16 on web and iOS, 12 on Android at the default density (the compact and comfortable steps already carried their own). `CardContent` picks up the same 16px flat-child rhythm. `flush` opts out of both the inset and the gap, sectioned cards are untouched (their sections pad themselves), and a single-child card renders pixel-identical since gap is inert with one child.

  Minor justification: new user-visible layout capability on the public Card API; padded cards and CardContent now space flat children without a Row or Column wrapper.

- 9e4cbc2: Collapsible and Accordion gain `card` and `description`, both backward compatible. `description` renders a muted secondary line under the title in the default trigger anatomy (on Accordion it lives per item, on `AccordionItem`); title truncation is unchanged. `card` wraps the disclosure (or the whole group) in an outlined card surface: an 8px-radius hairline card with 20px insets on web, the Material 3 outlined-card equivalent with 16dp insets on Android, and a documented no-op on iOS, where the default skin already renders the inset-grouped card.

  Minor justification: two new public props on Collapsible and Accordion (card surface variant and per-title description line), a user-visible API capability addition.

## 2.11.3

### Patch Changes

- 158d39e: dev-sync now mirrors straight into consumers' `node_modules/@nannier/canvas` overlays (stamped with `.origin`) instead of the former repo-root `.canvas` directories. Re-testing under clean conditions showed Turbopack live-watches real directories inside node_modules, so the `.canvas` indirection and consumer-side aliases were unnecessary.

## 2.11.2

### Patch Changes

- 2cafc58: `bun run dev` now pairs the tsc watch with a consumer sync watcher: it mirrors `dist/` and `styles/` into every sibling repo whose git-ignored `.canvas` marker points at this checkout, keeping locally linked consumers live-reloading while the kit is edited. Consumers overlay a real directory in node_modules because Next 16 Turbopack refuses out-of-repo symlinks there.

## 2.11.1

### Patch Changes

- 3be32c9: Add a root `dev` script (tsc watch on tsconfig.build.json) so locally linked consumers get live rebuilds of `dist/` while editing the kit.

## 2.11.0

### Minor Changes

- 3eb4265: Add the `Reveal` atom and `RevealGroup`: a scroll-triggered content entrance.

  New user-visible capability, which is what makes this a minor: the kit had no
  in-view primitive at all. Its only animation component, `Entrance`, fires on mount,
  is spring-only opacity plus scale, and has no delay, duration, direction, or trigger,
  so every app that wanted content to arrive as it scrolled into view had to reach
  outside the kit for it. `Reveal` is that capability, and it is additive: no existing
  export changes shape, and `Entrance` and the four overlays built on it are untouched.

  `<Reveal>` wraps content, holds it slightly offset and transparent, then travels it
  into place and fades it in when the element reaches the viewport, once. The API is
  semantic booleans on four axes and carries no numbers: direction (`fromBelow`, the
  default, plus `fromAbove`, `fromLeft`, `fromRight`), distance (`pronounced`), speed
  (`brisk`), and threshold (`deepInView`).

  `<RevealGroup>` makes stagger structural instead of numeric: it hands each child the
  next ordinal in document order and the child turns that into its own delay, so a
  mapped list cascades without any call site computing a per-item delay. It renders no
  host element, so it can sit between a grid and the items the grid lays out without
  disturbing the layout.

  Detection is a shared throttled ticker that measures only elements still waiting,
  and stops dead when the last one arrives, so a fully revealed page holds no timer.
  Every path that cannot produce a trustworthy measurement reveals the content: an
  entrance primitive must never be able to leave content invisible. Under Reduce
  Motion the whole mechanism is skipped, not merely shortened (no registration, no
  measurement, no timer), the final frame renders immediately, and the stagger is
  dropped with the motion, since delaying a static frame would only withhold content.

## 2.10.2

### Patch Changes

- 7ba848f: Count only the changesets that changesets itself will read.

  The release workflow decides whether to version, commit and tag by counting pending
  changesets with a shell `find`, and that `find` did not match changesets' own filter,
  which is `!file.startsWith(".") && file.endsWith(".md") && !/^README\.md$/i.test(file)`.
  It was missing the dotfile exclusion and its README check was case-sensitive.

  The consequence is small but confusing: a repo whose only pending entry is a scratch
  `.changeset/.draft.md` counted as one pending release, so the workflow took the
  version-and-tag path with nothing behind it and produced a no-op re-tag of the
  current version, which the existing `|| echo "Release already exists"` then hid.

  Over-counting is the safe direction, since the worst case is a wasted no-op, whereas
  under-counting would skip a real release. This change only ever removes entries
  changesets refuses to read, so it cannot cause a missed release. Verified against
  every filename case: a dotfile draft, a lowercase readme, a legacy directory
  changeset, and two real changesets.

  Found by adversarially reviewing a changeset guard in daedalus, then confirmed
  identical in all seven repos.

## 2.10.1

### Patch Changes

- 30c4385: DragDrop: never arm a pointer drag whose grip was already released. Arming is
  async (the zone/card measure spans a few macrotasks), so a grip tapped, or
  dragged and released, before the measure landed would arm a drag no pointer
  owned and leave the drop ring and source dim stuck until the next interaction.
  A per-grab session guard now invalidates any measure that finishes after
  release or after a newer grab.

## 2.10.0

### Minor Changes

- ffab6fc: Minor justification: two new user-visible capabilities ship on the public API.

  New `Board` organism: a data-driven kanban board composed from the kit's own
  DragDropProvider/DropZone/Draggable/DragHandle plus Card, Badge, and RowMenu.
  Columns scroll horizontally and are drop zones; cards carry a drag grip, an
  optional trailing badge, a 2-line muted description, a free-form `chips` slot,
  and an optional kebab menu. Works controlled (`items` + `onMove`, with
  `applyBoardMove` exported as the standard reducer and `BoardMove` reporting the
  insertion index plus `afterId`/`beforeId` neighbors) or uncontrolled
  (`defaultItems` + `onItemsChange`). Keyboard and screen-reader drag come from
  the DnD family (Space grabs, arrows move, Space drops, Escape cancels).

  `StackedList` gains `reorderable` + `onReorder` (rows get a leading drag grip
  and the list becomes a drop zone; order stays controlled by the consumer's
  items array) and a per-item `trailing` ReactNode slot rendered before the
  badge/meta cluster for inline controls. A bare StackedList renders exactly as
  before.

  Also: `DragHandle` now refuses pan-responder termination mid-drag, so a
  surrounding ScrollView (a board's lanes, a scrollable page) can no longer
  steal an in-flight drag on native.

## 2.9.1

### Patch Changes

- 9f00da3: Stop BackHandler console.error noise on web: Drawer, ActionSheet, and the Sidebar drill-down now wire Android hardware-back through a shared useHardwareBack hook that subscribes only while the overlay is open and never on web, where react-native-web's BackHandler shim logs "BackHandler is not supported on web" on every addEventListener call. Native behavior is unchanged.

## 2.9.0

### Minor Changes

- d86b6de: `StatItem` gains `steady`, which renders the delta muted rather than as a rise
  or a decline.

  A metric's second line is not always a change. It is often a qualifier: "last 30
  days", "8 M2M / 4 user". Colouring those green reads as good news the caller is
  not claiming. `steady` takes precedence over `down`, and omitting it leaves
  today's rise/decline behaviour exactly as it was.

## 2.8.0

### Minor Changes

- 79b8182: `Stats` gains a per-metric icon, header control and accent; `StackedList` gains a
  per-row badge tone.

  Minor rather than patch because both are new user-visible capabilities.

  `StatItem` takes `icon` (a glyph naming what the metric counts), `actions` (a
  control in the metric's header, a period selector or a filter) and one of
  `chart1` through `chart8`, which accents the headline value from the same
  categorical ramp the charts use, so a dashboard's tiles are tellable apart and a
  metric can carry the identity of the series it summarises. The accent recolors
  only the value: the delta keeps its own rise and decline semantics.

  `StackedListItem` takes `success`, `error`, `warning`, `info` or `neutral`,
  which tone its trailing `badge` as the kit's status pill. A service-health list
  can now say healthy, degraded and down in colour instead of three identical grey
  badges.

  Backward compatible throughout. A metric with no icon, control or accent renders
  exactly as before, and an untoned badge keeps its original `secondary` look.
  Both new axes are mutually exclusive with a documented first-match precedence.

## 2.7.0

### Minor Changes

- d3ce53e: `Card` now renders its header and footer sections beside raw children.

  Minor rather than patch because this is a new user-visible capability: a card
  can express a titled header, an icon, a header action and a footer ABOVE
  arbitrary children (a data table, a form, a list). Previously those props were
  silently dropped the moment children were passed, and the header only rendered
  on the data-driven path, where the body has to be a `string`.

  Backward compatible. A plain card, one with children and no section props, is
  untouched down to its computed surface style; the data-driven string path is
  unchanged; and children still win over a string `body` when both are passed.

  A sectioned card pads through its sections rather than its surface, so `padded`
  and the density booleans do not apply there and now emit a dev warning instead
  of being silently ignored.

## 2.6.2

### Patch Changes

- 55d79fd: Fix optional peers being treated as required by Metro.

  The kit loads its optional peers through a guarded `require()` wrapped in
  `if (typeof require === "function")`. That `if` defeats the mechanism it was
  meant to support: Metro's `isOptionalDependency` walks up from the require and,
  at the first enclosing block statement, returns whether that block belongs to a
  `TryStatement`, without climbing further. The `if`'s own block answers no, so
  Metro registered a required edge and any consumer who skipped the peer failed to
  bundle with "Unable to resolve module".

  Every one of the nine sites now places the require directly inside the try. The
  runtime behaviour is unchanged: where `require` is undefined the ReferenceError
  lands in the same catch that already absorbed a missing module.

  Consumers who install every optional peer see no difference. Consumers who skip
  one, which is the documented and supported case, can now bundle under Metro.

## 2.6.1

### Patch Changes

- 2f3461d: Backdrop: add the optional GPU-backend capability layer, inert for now.

  Declares `@shopify/react-native-skia` as an OPTIONAL peer dependency and adds a
  guarded capability probe behind it, so a later release can upgrade `Backdrop` to
  a GPU renderer for the effects `react-native-svg` structurally cannot express
  (procedural noise, real blur, thousands of bodies in one draw call).

  Deliberately a patch rather than a minor: this adds no user-visible capability.
  The GPU renderer does not exist yet, so every backdrop renders exactly the same
  SVG baseline as before, on every platform, with or without the peer installed.
  What ships is the plumbing and its guarantees.

  The probe asks a capability question ("can this runtime allocate a Skia object
  right now?"), never a platform question, so the eventual upgrade is progressive
  enhancement rather than a platform fork. It resolves the peer through a guarded
  `require`, which `verify-package` now enforces for this specifier alongside the
  existing optional peers: a consumer without the package installed must never hit
  an unresolved module.

  Two new exports for apps that load a backend themselves, notably on web where
  CanvasKit is fetched at runtime: `refreshBackdropRenderer()` re-runs the probe
  and notifies mounted backdrops, and `useGpuBackdrop()` reports whether one is
  live. Loading the backend stays the application's job, exactly as installing
  `expo-blur` is, which keeps the WebAssembly glue out of the kit's module graph.

## 2.6.0

### Minor Changes

- fed7369: Add `Backdrop`, the engine for a full-screen animated background.

  New user-visible capability: a consuming application can now ship an animated
  background through the kit instead of hand-rolling one. Canvas owns the surface,
  the shared clock, the per-platform frame budget and the accessibility ladder; the
  application owns the scene, composed from `Backdrop.Particles`,
  `Backdrop.Gradient`, `Backdrop.Shader` and `Backdrop.Custom` layers. The kit ships
  no artwork of its own, so the animation belongs to the app: point the same engine
  at different children and it renders something else entirely.

  Also exports `BackdropHost`, which lets one surface serve every `Backdrop` in an
  app (so a stack of screens shares a single drawing surface rather than one each),
  and `backdropClock`, so bespoke app-supplied art can bind to the same timeline as
  the declared layers.

  Semantic boolean axes: `energetic`/`calm` (rate), `dense`/`sparse` (field detail,
  which is also the frame-budget lever), `vivid`/`subtle` (weight), plus `still`.
  Reduce Motion renders a poster frame from the first paint, Reduce Transparency
  drops the translucent washes, and Increase Contrast paints the background token
  alone.

## 2.5.0

### Minor Changes

- 748111d: **`AlertDialog` gained the `overlay` presentation**, matching `Dialog`.

  It had the same in-flow default: a scrim sized for the docs preview, no
  positioning, no portal, and `aria-modal` asserted anyway. That is the wrong
  default for the component's main job, which is guarding a destructive action:
  the confirm appeared wherever it happened to be mounted while the page behind it
  stayed scrollable and clickable, so a user could reach the thing they were being
  asked to confirm destroying.

  With `overlay` it teleports into the nearest `OverlayProvider` and fills it. The
  contained behaviour is unchanged and remains the default, and with no provider in
  the tree it still renders in place rather than vanishing.

## 2.4.0

### Minor Changes

- e6c4638: **`Dialog` gained `accessibilityLabel`.** New capability: a dialog whose body is
  supplied as `children` can now carry an accessible name.

  Children REPLACE the built-in title and description, so there is no element left
  for `aria-labelledby` to reference and the dialog was announced with no name at
  all. That is poor for a `dialog` and invalid for the `alertdialog` that a
  `destructive` confirm renders, and there was no way to fix it from the call site
  because the prop did not exist. Dialogs using the data-driven `title` path are
  unaffected and keep naming themselves.

## 2.3.0

### Minor Changes

- 46088bd: Three capabilities the kit was missing, each found by building a real app against
  it rather than by reading the catalogue.

  **`Select` accepts `{ value, label }` options.** It previously took `options:
string[]`, so the stored value was always the visible text. Any list keyed by an
  id (a project id, a region slug, a workspace name) could not be expressed, and
  two separate apps grew the same wrapper independently. Bare strings still work
  and still mean the value is the label, so this is backward compatible.

  **`Card` gained `icon` and `actions` header slots.** `Card` already carried
  `title` and `description`; what consumers kept rebuilding around it was a leading
  glyph and a trailing action in the same header row.

  **`Dialog` gained an `overlay` presentation.** The existing dialog renders inline
  in normal flow with a scrim sized for the docs preview, which is right for the
  catalogue and wrong for an application: it appends a backdrop wherever the
  component happens to be mounted, leaves the page behind scrollable and clickable,
  and still asserts `aria-modal`. In a consumer app that meant a delete
  confirmation appearing at the bottom of the page while the page behind it stayed
  interactive. With `overlay`, the dialog teleports into the nearest
  `OverlayProvider` and fills it, so `aria-modal` is true rather than aspirational.
  The contained behaviour is unchanged and remains the default; with no provider in
  the tree an overlaid dialog still renders in place rather than vanishing.

## 2.2.1

### Patch Changes

- 0bfb91f: Fix the stale package-entry comment that still listed Image among the raw
  primitives; Image graduated to a Canvas atom. No runtime change.

## 2.2.0

### Minor Changes

- f77bb67: The published package is now MIT licensed.

  Canvas was previously published as `UNLICENSED` with no licence file, which in npm's
  vocabulary means proprietary, all rights reserved. Anyone who installed it therefore had
  no grant of rights to use it at all, despite the project being described publicly as open
  source. That is now fixed for consumers: `license` is `MIT`, and the MIT text ships inside
  the tarball.

  The grant is deliberately scoped to the distributed package. The source repository stays
  all rights reserved, so the licence file is generated at pack time by `tools/licensegen`
  rather than committed, because a `LICENSE` at the repository root is exactly how GitHub
  decides a repository's licence and committing it would extend MIT to the source too.

  Nothing about the API, the build output or the runtime changes; this only adds the
  permission to use what was already being published.

## 2.1.0

### Minor Changes

- 0f90a3c: Sidebar now declares itself as the navigation landmark, and GlassSurface accepts a `role`.

  A sidebar of nav rows is the page's navigation, but the shell rendered as an
  unlabelled stack of views, so every row inside it sat outside any landmark. Screen
  reader users had no way to jump to the navigation or skip past it, and axe's `region`
  rule flagged the contents. The shell now carries `role="navigation"` in both its plain
  and header/footer forms.

  That was only possible because `GlassSurface` took a closed set of props, so it has
  gained an optional `role` that forwards to the root element, threaded through the web,
  Android and iOS materials as well as the plain and degraded fallbacks. It is spelled
  with React Native's universal `role` prop, which React Native Web renders as the
  matching HTML element and native maps onto its own traits, so no per-platform branch is
  involved. Reach for it on any glass surface that is a structural shell, for example
  `role="banner"` on a top bar; leave it off decorative surfaces like popovers and menus,
  which already carry a role of their own.

  Both changes are additive and backward compatible: existing markup gains a landmark it
  did not have, and no visual output or layout changes.

## 2.0.1

### Patch Changes

- 0988c64: Sidebar now marks its active row with `aria-current="page"` instead of `aria-selected`.

  ARIA permits `aria-selected` only on roles that carry a selected state, such as
  `option`, `tab` and `row`. A sidebar row is a `button`, so browsers discarded the
  attribute as invalid and assistive technology announced every row as unselected,
  including the current page. The row is navigation, so it now uses the same spelling
  Navbars, Breadcrumb and Pagination already use.

  The native `accessibilityState={{ selected }}` is unchanged, since a selected state is
  valid on iOS and Android and is what VoiceOver and TalkBack read.

  If you query the DOM for the active row, match on `[aria-current="page"]` rather than
  `[aria-selected="true"]`.

## 2.0.0

### Major Changes

- 94be12a: Remove the `Overlay` component (and its `OverlayProps` type). It was a redundant
  umbrella that re-implemented three surfaces the kit already ships as real,
  `Modal`-backed components: use `Drawer` for an edge or bottom panel, `Dialog` for
  a centered modal, and `ActionSheet` for a bottom action sheet. Unlike those,
  `Overlay` only painted a contained inline mock, so it could not present as a true
  floating overlay. Migration: replace `<Overlay drawer />` / `<Overlay sheet />`
  with `Drawer`, and `<Overlay modal />` with `Dialog`.

### Minor Changes

- d7968fa: Give the iOS Slider a real Liquid Glass handle. On iOS 26+ the slider handle
  "transforms into liquid glass during interaction" (WWDC25); the kit now renders
  the iOS thumb through the shared `GlassSurface` primitive, so when glass is the
  active surface (the platform default on iOS 26) the knob is a genuine Apple
  Liquid Glass control. It stays a bright puck (matching the system handle) whose
  Liquid Glass edge-lensing and specular show on physical iOS 26 hardware, and it
  springs up on press (Apple's scale/bounce). The press grow is an animated
  width/height resize, NOT a `transform: scale`, because a scale transform on the
  GlassView's ancestor degrades the Liquid Glass material. Under a solid surface,
  Reduce Transparency, or Increase Contrast it degrades to the previous opaque
  white capsule, and the Android and web handles are unchanged.

  Note: Apple's Liquid Glass only renders its blur/refraction on physical devices;
  on the iOS Simulator the handle shows as a bright capsule with a subtle rim.

  `GlassSurface` also gains an optional `tint` prop that overrides the translucent
  under-fill painted behind the material (default: the `popover` token), so a
  small glass control such as the slider knob can read as a bright puck rather
  than a popover-tinted panel.

### Patch Changes

- 5568134: Dialog, AlertDialog, and Popover no longer scroll the page when they open. Their
  focus management now moves focus into the panel with `focus({ preventScroll: true })`,
  so a modal that opens (or a `<Dialog open>` rendered inline mid-page) keeps focus for
  accessibility without yanking the surrounding scroll container to the panel.
- ae363cc: Dialog and AlertDialog panels now shrink to fit a narrow screen instead of
  overflowing. Their outer wrapper fills the available width (and the backdrop can
  shrink below the card's content width), so the panel's `maxWidth: "100%"` caps it
  to the container: on a phone the panel fits the viewport rather than running off
  the right edge, while on desktop it still renders at its per-size width, centered.
  The optional trigger button keeps its natural width.
- b810a87: Fix the Drawer bottom sheet's dimmed backdrop sliding up with the sheet. The
  bottom sheet was the only edge still using React Native Modal's native
  `animationType="slide"`, which transforms the whole modal (the scrim dim
  included), so the backdrop rose from the bottom instead of taking over the
  surface. Every edge now shares one manual slide path: a stationary full-screen
  dim that fades in while only the panel travels (the bottom sheet rises on
  `translateY` from `+panelHeight` to `0`, mirroring the top sheet). This makes the
  bottom sheet consistent with the left, right, and top variants.
- 21440d8: Fix the Drawer sliding in from the wrong side on the web (a left drawer opened
  "backward", sliding leftward into the edge instead of entering from off-screen
  left). React Native Web's `I18nManager` has no direct `isRTL` property, so
  `I18nManager.isRTL` read `undefined` on the web and the direction comparison
  `(edge === "right") !== I18nManager.isRTL` was always true, forcing every side
  drawer to the right-hand slide origin. All RTL checks now route through a new
  shared `isRTL()` helper backed by `I18nManager.getConstants().isRTL`, which is
  implemented on both native and web. This also corrects the same latent web bug in
  Dropdown, Listbox, Slider, Tabs, and Breadcrumb, whose right-to-left handling was
  silently skipped on the web.
