# AdiaUI Catalog — LLM Rules (v0.9)

# Cross-component and contextual constraints that the JSON Schema validator
# cannot express. Concatenated into the agent system prompt.
# Generated from each component YAML's `a2ui.rules:` entries.

## AccordionItem
- Child of <accordion-ui> — places one collapsible section with header + body.
- Toggle behavior: click anywhere on the row toggles, unless inside [slot='actions'] or [data-no-toggle].
- For single-button disclosures (one expanding panel without a list) use <details>/<summary> or compose <button-ui> + <section-ui>.

## Accordion
- Hosts <accordion-item-ui> children. One or more sections may be open simultaneously (set single= to enforce only-one-open).
- For tab-switched panels (always exactly one open) use <tabs-ui>; accordion supports zero-open and multi-open.
- Item ordering is DOM-order; no auto-sort.

## ActionItem
- Child of <action-list-ui> — one inline-action row with icon + label + optional shortcut + optional sublabel.
- Different from <menu-item-ui>: action-items live inline in content surfaces; menu-items live inside <menu-ui> popovers.
- For navigation entries (route-changing) use <nav-item-ui> instead.

## ActionList
- Hosts <action-item-ui> children as a vertical command list inline in content surfaces.
- For popover-style menus use <menu-ui> + <menu-item-ui> instead.
- Typical use: command palettes, settings panels, agent suggestion lists.

## AgentArtifact
- Inline container for structured agent payloads inside <chat-thread-ui> message bodies or <inspector-ui> panes.
- kind attribute (a2ui|json|ticket|code|...) sets content-type-aware rendering and icon/badge styling.
- For free-text agent output use <text-ui> or <richtext-ui>; artifact-ui is for structured payloads only.
- primary + secondary slots for header chrome; default slot for the artifact body.

## AgentFeedbackBar
- Thumbs-up / thumbs-down rating row + optional Save action, rendered beneath an LLM-generated message.
- Different from <rating-ui> (star-based ordinal) — agent-feedback is binary good/bad.
- Emits feedback events with value=up|down and optional save action; consumer wires the actual feedback API call.
- Place at the end of an assistant message in <chat-thread-ui>, not on user messages.

## AgentQuestions
- Multi-choice clarifying-question card emitted by an agent when it needs disambiguation before proceeding.
- multi attribute controls single-select (default) vs multi-select answer behavior.
- Slot accepts <button-ui> answer chips; for radio-card style use <segmented-ui> + <segment-ui> children instead.
- Different from <agent-suggestions-ui> (follow-up suggestions, optional) — agent-questions is gating (agent waits for answer).

## AgentReasoning
- Agent inner-monologue + pipeline viewer with steps, thoughts, plans, and iterations.
- Composes <timeline-ui> internally — pass reasoning steps as children rendered as <timeline-item-ui>.
- noAutocollapse attribute prevents the auto-collapse-on-complete behavior; useful for debug surfaces.
- Different from <agent-trace-ui> (metrics + tool-call summary) — reasoning is narrative, trace is structured-data.

## AgentSuggestions
- Row of follow-up suggestion chips presented under an agent response. User taps to send the chip text as the next prompt.
- Hosts <button-ui> children with chip-style variant; auto-applies variant if children don't set one.
- Different from <agent-questions-ui> (required disambiguation) — suggestions are optional and the agent doesn't wait.
- Place at the end of an assistant message in <chat-thread-ui>; not for first-message empty-state suggestions (use <empty-state-ui>).

## AgentTrace
- Collapsible metrics + training-feedback panel showing reasoning steps, tool calls, latency, and token counts.
- Place inside <chat-thread-ui> message bodies for expert/debug views; hide by default in user-facing chat.
- Different from <agent-reasoning-ui> (narrative inner monologue) — agent-trace is structured metrics.
- Default collapsed=true; expand-on-demand keeps chat-thread visual density low.

## Alert
- Inline alert/banner for status messages within a content region. Severity via variant (info, success, warn, error).
- For ephemeral toast notifications use <toast-ui> (or post to <feed-ui>); alert-ui is persistent inline.
- For modal-style critical alerts use <modal-ui> with alert content.
- Billing dunning / payment-failed notices use pattern="dunning" + amount + currency + dueAt props, NOT inlined into title/description strings.
- pattern="dunning" SHOULD use variant="danger" (default) or variant="warning" (grace period); never variant="info" or "success".
- When pattern="dunning", slot at least one button-ui in slot="actions" with data-dunning-action ("update" or "retry").

## Aside
- Use <aside-ui> as a slot stub inside an IN-PAGE primitive container parent (<card-ui>, <drawer-ui>, <modal-ui>, <page-ui>) for two-column layouts with a semantic side region. It ships no behavior; the parent reads [collapsible] and [width] via @scope. Typical contents: <list-ui> / <tree-ui> / <nav-ui variant="section">.
- Do NOT use <aside-ui> for app-shell sidebars. <admin-shell> no longer reads <aside-ui slot="leading|trailing"> (retired in v0.4.0 per ADR-0024). Use the bespoke <admin-sidebar slot="leading|trailing" collapsible resizable> with reflected [collapsed] / [resizing] state. Same rule for <chat-sidebar> under <chat-shell> and <editor-sidebar> under <editor-shell>.
- Do NOT confuse <aside-ui> with <pane-ui>: aside-ui declares the semantic side role (a11y + parent layout hint); <pane-ui> owns resize / collapse interaction. When you need interactive resize inside an aside, nest <pane-ui resizable> inside.
- Width hints map to tokens: `width="rail"` ≈ icon-only nav, `width="panel"` ≈ nav with labels, `width="wide"` ≈ workspace pane. Unset defers to the parent default. Only pair `collapsible` with parents that wire a toggle (the prop is a hint, not a behavior).
- For the settings-page description rail (label + help text on the left, controls on the right) use plain HTML <aside> inside a [data-section] flex layout — see apps/saas/billing and apps/saas/members. That pattern is NOT aside-ui; the primitive is for slotted container parents only.

## AvatarGroup
- Cluster of overlapping <avatar-ui> children with negative-margin stacking + +N overflow indicator.
- max attribute controls visible-avatar count before +N overflow kicks in.
- For a single user use <avatar-ui> standalone; avatar-group only for 2+ users.

## Avatar
- Use for representing a single person, account, or entity. Image, initials, or icon fallback in priority order.
- For clusters of multiple users, wrap multiple <avatar-ui> in <avatar-group-ui> instead of placing them inline.
- Do not embed inside <button-ui> for clickable avatars — make the <avatar-ui> itself the click target with role='button'.

## Badge
- Use for small status/count labels attached to another element (notification counts, status pills, version tags).
- Place adjacent to or absolutely-positioned over the badged element, not inside it.
- For dismissable taxonomy labels (filter chips, selected items in multi-select), use <tag-ui> instead.

## Block
- Generic semantic block — use for arbitrary content groupings without strong layout semantics.
- For row/column layouts use <row-ui> / <col-ui>; for grid use <stack-ui>; block is for content-flow groupings.
- Block-ui does not impose padding/gap by default — wrap in <section-ui> if you want chrome.

## Blockquote
- Use for pull-quotes, testimonials, or inline citations. Renders italic body text with a left rule indicator + optional em-dash attribution line.
- Set [cite] for plain-string attribution; use [slot="cite"] for rich attribution (linked source, role pairing). Slot content overrides the [cite] prop when both are set.
- Do NOT wrap blockquote-ui in a native <blockquote> element — that produces nested-blockquote semantics. The tag IS the blockquote.

## Breadcrumb
- Canonical placement: render <breadcrumb-ui> inside <admin-topbar> for hierarchical page-context display. It is the topbar's heading region — typically preceded by a sidebar-toggle <button-ui icon="sidebar" variant="ghost" size="sm"> and followed by topbar actions in [slot="action"]. The host stamps role="navigation" + aria-label="Breadcrumb" automatically.
- Child shape: each crumb is either an <a href> (ancestor link) or a plain <span> (terminal / non-link current page). The LAST child is the current page and MUST be a plain <span> — the component auto-applies aria-current="page" and disables pointer events on it. Optional first child may be an <icon-ui> (or <a> wrapping one with aria-label) for an app / home glyph.
- Separator + overflow: do NOT insert your own separator elements ([data-sep] spans are stamped automatically between children). For deep trails (4+ items) prefer the `collapse` attribute over manual truncation; tune visible edges with [collapse-keep-leading] / [collapse-keep-trailing]. Collapsed middle crumbs are presented as a `…` <menu-ui data-overflow> popover.
- Decision rule: <breadcrumb-ui> is read-only PATH-CONTEXT display ("where am I"). For primary navigation (sidebar) use <nav-ui> + <nav-item-ui> inside <admin-sidebar>. For switching sub-views within a page use <tabs-ui>. Never use <breadcrumb-ui> as the primary navigation control or wrap navigation controls (selects, tabs, form controls) inside it.

## Button
- Canonical clickable affordance — text + optional icon. Variant attribute sets primary/secondary/ghost/destructive intent.
- Do not repeat the icon's glyph in text=. Icon provides the symbol; text= carries only the words.
- For navigation (route-change) use <nav-item-ui> or anchor; button-ui is for actions only.
- For toggleable on/off state use <switch-ui>; for multi-select clusters use <toggle-group-ui> + <toggle-option-ui>.

## CalendarGrid
- Use <CalendarGrid> only as a substrate primitive composed inside a higher-level component (date-range picker, datetime picker, custom date affordance). For a full single-date input, use <CalendarPicker> — it adds a trigger button, popover surface, and form-association.
- <CalendarGrid> is NOT form-associated. Its emitted `change` event is the sole signal to the parent — the parent owns the canonical value + form participation.

## CalendarPicker
- Form-associated date input. Trigger button + popover calendar grid; emits ISO date string via change events.
- Use for single-date input. For date ranges compose two pickers or use a dedicated range component.
- min/max attributes constrain selectable range; disabled-dates accepts a function or date list.

## Canvas
- A2UI rendering surface — consumes a DocStore / A2UI document and renders the component tree from it.
- Typically wrapped by <a2ui-root> internally; for direct-mount use <canvas-ui> as the root.
- Do not place static children inside — the runtime owns the rendered DOM.
- theme attribute scopes the AdiaUI token theme to the canvas subtree.

## Card
- The card's <header> grid activates only for DIRECT-child slotted elements. If you need an icon column, place the icon element (avatar-ui, icon-ui) directly in the header with slot="icon" — not inside a wrapper.
- Heading slot accepts inline badges/metadata: <span slot="heading"><text-ui strong>Title</text-ui><badge-ui text="New" variant="accent"></badge-ui></span> renders title + badge on one row.
- Description slot also accepts bare <p> or <small> elements as siblings of the heading — they participate in the grid's row 2 without needing slot="description".
- Multiple <section> siblings are allowed and stack vertically. [bleed] on a section removes its margin for edge-to-edge content (tables, charts); [padding] adds a canvas-scrim background for hero regions.
- Footer with a [slot="description"] + [slot="action"] pair triggers justify-content: space-between — useful for a "Last saved …" note on the left and a Save/Cancel button group on the right.
- Do NOT substitute <card-ui> for shell/page containers. <admin-page>, <admin-content>, and <page-ui> own routed page layout; <card-ui> is a localized bordered surface that lives INSIDE them. For overlays use <modal-ui> (centered) or <drawer-ui> (edge-anchored) — both share the same 4-stub <header>/<section>/<footer> vocabulary.
- Use [raw] when a parent already owns the surface chrome (e.g. an auth screen centred on the viewport — see apps/user-flow/auth). Use [elevation] 0–3 for shadow depth; [variant="ghost"|"flat"] to remove shadow without removing structure. [draggable] emits a drag-end event; only enable on cards meant to move.

## ChartLegend
- Standalone legend primitive — a row of <badge-ui>+<swatch-ui> chips that are keyboard-focusable and click-toggleable.
- Pairs with <chart-ui> via [for] id-ref (auto-bidirectional series toggling) or via items= for standalone use.
- position attribute (top|bottom|left|right) places legend relative to its chart; static= disables interactivity.

## Chart
- Declarative SVG chart supporting 18 types via the type attribute (bar, line, pie, donut, radar, area, ...).
- For density-grid visualizations (calendar heatmap, etc.) use <heatmap-ui> instead.
- Pair with <chart-legend-ui [for]=...> for keyboard-toggleable series legends; otherwise use the built-in inline legend.
- title + empty slots for chart chrome; canvas slot for chart-internal overlays.

## ChatInput
- ChatInput is a self-contained composer — it stamps its own textarea + model picker + send button. DO NOT add a separate Button sibling for "send" inside the same parent. The user gets two send buttons (one built-in, one redundant).
- For chat interfaces emit ChatInput as the sole input component inside the chat shell. The submit event fires on Enter or send-button click; `detail` is `{ text, model }`.
- To customize models, set the `models` prop with an array of {value, label} option objects. Do not stamp a separate Select next to ChatInput for model selection — the built-in model picker handles it.

## ChatThread
- Primitive chat-message scroll container — ad-hoc message rendering surface.
- Different from the bespoke <chat-thread> (module-tier, lives inside <chat-shell-ui>, owns scroll + load-more affordances).
- Hosts arbitrary children — typical content: agent + user message blocks, <agent-feedback-bar-ui>, <agent-suggestions-ui>.
- streaming attribute hints the consumer to show a streaming indicator (composes well with <stream-ui> children).

## Check
- Self-labeling widget — use the [label] attribute directly; do NOT wrap in <field-ui>. The widget renders its own label inline via CSS attr() pattern. Wrapping breaks the consent-row layout (see field-ui anti_patterns).
- Three states supported: unchecked (default), checked (via [checked]), and indeterminate (via [indeterminate]); form-associated.
- For binary on/off toggles where visual switch metaphor matters, use <switch-ui> instead; check-ui is for multi-select sets and form-acceptance gates.

## Code
- Inline (default) for short tokens in prose; block variant (display='block') for multi-line snippets.
- Do not use for editable code — use <richtext-ui> with code variant or a dedicated editor primitive.
- Inside <agent-artifact-ui> blocks for agent-emitted code snippets; lang= sets syntax-highlighting hint.

## Col
- Vertical-stack flex container. Children flow top-to-bottom with token-driven gap.
- Pair with <row-ui> for horizontal layouts; both share the same gap-token contract.
- Set align/justify attributes for cross-axis / main-axis alignment; default is start/start.

## ColorInput
- Use <color-input-ui> for any form-row color field — it canonicalizes the popover + button + color-picker recipe and is the only form-bearing color primitive. Do not hand-roll the composition; reach for color-input-ui directly.
- Set [format="oklch"] when the persisted value feeds CSS tokens or perceptual math; [format="hex"] for legacy / design-tool interop. Event detail (`change` / `input`) carries BOTH `hex` and `oklch` views regardless of [format], plus parsed `{l, c, h}` channel scalars.
- For brand-palette constraints, set [maxChroma] / [minL] / [maxL] / [hueDriftMax] (with [baseHue]) on the host — they forward to the inner <color-picker-ui> and clamp generation. Useful for Tokens-Studio-style guarded color generation.
- Use <color-picker-ui> DIRECTLY (no color-input-ui wrapper) only for full-surface editors where the picker IS the page (e.g. Tokens Studio main canvas). For inline form-row use, always reach for <color-input-ui>.
- Per ADR-0027 (cross-primitive composition imports), consumer pages MUST explicitly import <button-ui>, <popover-ui>, and <color-picker-ui> before <color-input-ui>. The primitive composes them but does NOT auto-register them.

## ColorPicker
- OKLCH-native color picker with 2D color area + H/C/L sliders. Form-associated; emits OKLCH color strings.
- For simple color swatches (read-only display) use <swatch-ui>; for hex/rgb text input use <color-input-ui>.
- Output format defaults to oklch(); set format= to override (hex, rgb, hsl).

## Combobox
- Use <combobox-ui> for typeahead-filterable single-select with a constrained-choice value model. `value` MUST be one of `options[].value` unless `[free-text]` is set. For ≤ 4 options, use <segmented-ui> or <radio-ui> instead.
- For free-form text entry with suggestions, use <autocomplete-input-ui> (SPEC-035) — combobox is constrained-choice. For button-first dropdowns where the trigger should be closed by default, use <select-ui searchable>.
- Compose options via native <option> / <optgroup> children, OR set `.options` programmatically as an array of `{value, label, disabled?}` (grouped form: `{label, options:[…]}`). Setting `value` to a string not in `options` is invalid mid-state (free-text=false) and the validator should reject it.
- `[creatable]` implies `[free-text]` and adds the "Create '{value}'" footer affordance + `create` event. Consumer wires the create event to a backend flow.
- Multi-select goes through <multi-select-ui> (SPEC-040), not combobox. Combobox is single-select.

## Command
- <command-ui> is the searchable PALETTE primitive (input + option list). For Cmd+K palettes, wrap it in <admin-command> at the shell tier — admin-command owns the native <dialog>, focus management, and the Cmd+K / Ctrl+K shortcut listener. command-ui is content-only and does NOT own the dialog or shortcut. Do NOT hand-roll a <dialog> + Cmd+K key listener.
- Author items as native <option value data-icon data-shortcut> elements inside <optgroup label="…"> for grouped sections. The `select` event's detail.category mirrors the parent optgroup's label. Detail = { value, label, category }.
- Decision rule vs adjacent surfaces. Use <menu-ui> for small NON-searchable popover menus (≤10 actions, triggered by a button). Use <modal-ui> for generic centered dialogs. Reach for <command-ui> only when you need a searchable, keyboard-navigable list of commands or destinations.
- command-ui MAY render inline (no dialog) for embedded search panels, but the canonical AdiaUI admin pattern is exactly one <command-ui placeholder="…"> as the sole child of <admin-command> inside <admin-shell>. See site/index.html and playgrounds/admin-shell/ for production references.

## ContextMenu
- Use <context-menu-ui> for right-click menus on a target (table row, file item, canvas object). For button-triggered menus use <menu-ui>; for popover content that is not a menu use <popover-ui> directly.
- Items are <menu-item-ui> children inside the default slot — same shape as <menu-ui> items.
- Bind target via wrap (default-slot first non-menu-item-ui child) OR [for] selector. The selector form is useful for whole-table or whole-canvas menus where wrapping isn't practical.

## DateRangePicker
- DateRangePicker.value MUST be `{from, to}` with both ISO 8601 dates, OR null. Either side null is invalid mid-state and the validator should reject it (use `input` event for partial state).
- DateRangePicker.value.to MUST be `>=` value.from lexicographically. Reversed ranges trigger `invalid` and do NOT commit.
- When `comparison: true`, both `value` AND `compareValue` MUST be set on commit. If only one is set, the form participation emits the set one and omits the other.
- presets array entries each require both `label` (string) and `range` (`{from, to}`). Empty preset arrays are valid (rail renders empty).
- Use DateRangePicker for date ranges. Do NOT compose two adjacent `<calendar-picker-ui>` instances + JS synchronization — that is the pattern this primitive replaces.

## DatetimePicker
- `DatetimePicker.value` MUST be ISO 8601 datetime (`YYYY-MM-DDTHH:mm` or `YYYY-MM-DDTHH:mm:ss`) OR empty string. Date-only or time-only strings fire `invalid`.
- `precision: "second"` requires the value to include seconds when set; missing seconds are coerced to `:00` on commit.
- `min` and `max` MUST be parseable ISO 8601 datetimes if non-empty. If `value` falls outside, `invalid` fires and the value does not commit.
- `hour-cycle` overrides the locale-derived cycle in the time pane. Set explicitly when the surface needs a specific cycle (cron editors, log queries, system surfaces).
- Use DatetimePicker for combined date+time. Do NOT compose `<calendar-picker-ui>` + a free-form `<input-ui>` manually as an alternative — that is the pattern this primitive replaces.
- Per ADR-0025 NEVER wrap a native `<input type="datetime-local">` — the calendar pane + time pane composition + ElementInternals together provide form participation.

## DemoToggle
- Demo-page-only — toggles between live and code views in component documentation surfaces.
- Do not use in apps/ — restrict to packages/web-components/components/*/<name>.html demo pages and docs surfaces.
- Behavior: shows/hides the [data-code] block when toggled.

## DescriptionList
- Use for key:value pairs in dense detail views (user profile fields, metadata panels, audit logs).
- Do not use for editable forms — use <fields-ui> instead.
- Pairs render as label + value on the same row by default; use vertical variant for narrow panes.

## Divider
- Use to separate visually distinct sections within a container; horizontal default, vertical via orientation='vertical'.
- Inside menus / popovers use <menu-divider-ui> instead; it has menu-specific token spacing.
- Do not stack multiple consecutive dividers — use a single divider or restructure the section.

## Drawer
- Direct children of <drawer-ui> MUST be <header>, <section> (1..n), <footer>, or an element with slot="header|body|footer". Wrap stray <col-ui> / <row-ui> / <div> / <text-ui> in a <section>. Enforced by scripts/audit/audit-drawer-structure.mjs. Bypassing the body slot loses the section inset and teaches the gen-UI corpus the wrong pattern.
- Reflect open state via the [open] boolean attribute on the host (open=true / open=false). Do NOT toggle visibility with CSS, [hidden], or a [data-open] proxy — drawer-ui owns the native <dialog> lifecycle, focus trap, and ::backdrop. Listen for the `close` event (detail.reason ∈ escape | backdrop | close-button | programmatic) to react to dismiss.
- Use drawer-ui (not modal-ui) for edge-anchored detail / edit panels and mobile bottom-sheet patterns. Pick side="right" for inspector flows opened FROM a row/list item, side="bottom" for sheets, side="left" for navigation drawers on narrow viewports. Centered confirmations belong in modal-ui.
- Compose the drawer header with [slot="icon|heading|description| action"] direct-child elements to activate the card-ui-aligned 3-column grid (icon | heading+description | action+close). The close button is auto-stamped; do not author your own X button unless using [permanent] (which suppresses the close button).
- Never set drawer.innerHTML wholesale — it wipes the stamped <dialog> part and the author skeleton. Mutate a stable inner element inside a persistent <section> instead. Same rule as <modal-ui>.

## Embed
- Responsive sandboxed <iframe> wrapper at constrained aspect ratio. Safe sandbox attributes are applied by default.
- For raster/vector images use <image-ui>; for icons use <icon-ui>; embed-ui is for live external content only.
- aspect attribute locks ratio (e.g. 16/9, 4/3) to prevent layout shift; width/height override when needed.
- Do not embed first-party AdiaUI app routes — use direct component composition instead.

## EmptyState
- Use <empty-state-ui> for zero-data states: empty lists, no-results search, fresh accounts. Not for loading (use <skeleton-ui>). For inline notices within populated content use <alert-ui>. For full-section error states where the data cannot be shown at all (API failure, permission error), use <empty-state-ui variant="danger"> — the centered layout is more appropriate than an inline banner when the entire content area is replaced.
- Canonical placement: slotted into cluster-specific wrappers — <chat-empty> inside <chat-thread>, <editor-canvas-empty> inside <editor-canvas>, OR inside <card-ui><section> for in-card empty states. Compose with an <icon-ui> for the leading glyph (pick a Phosphor name that mirrors the missing entity — folder, inbox, magnifying-glass).
- Put AT MOST ONE CTA in slot="action". It should be the single next step (Create, Retry, Clear filters). Prefer [variant="primary"] for create-flows; [variant="outline"] for retry / secondary actions.
- Use [minimal] for inline empty rows inside <table-ui> / <list-ui> where full-canvas chrome is too prominent. The `minimal` flag (§223 v0.5.9) drops chrome for a compact placeholder cell.
- Per ADR-0027, empty-state-ui composes <icon-ui> but does NOT auto-register it. Consumer pages must explicitly import <icon-ui> before <empty-state-ui> renders correctly.

## FeedItem
- One notification entry inside <feed-ui>. Title + description + optional icon + auto-dismiss timer.
- Typically created programmatically via UIFeed.post(...); do not place declaratively.
- Different from <alert-ui> (inline persistent) and <toast-ui> (standalone ephemeral); feed-item is feed-scoped.
- For "notification deep-link" pattern: post with action + onAction. Click navigates and auto-dismisses (router.push() / location.href / api.markRead()).

## Feed
- Top-layer notification feed channel — singleton per position (top-right, bottom-center, etc.) mounted lazily into document.body.
- Hosts <feed-item-ui> children programmatically via static API (UIFeed.post(...)). Do not place feed-items declaratively.
- For inline persistent alerts inside content regions use <alert-ui> instead; feed is ephemeral overlay.

## Field
- field-ui is for WIDE controls (input-ui, select-ui, textarea-ui, slider-ui, etc.) that need a separate label row. Small self-labeling widgets (check-ui, switch-ui, radio-ui, toggle-ui) carry their own [label] attribute and MUST NOT be wrapped in field-ui.
- field-ui[inline] is for inline WIDE-control rows (e.g. search field with trailing kbd hint), NOT for compact-widget rows. For settings rows or consent rows, use the widget's own [label] attribute directly without a field-ui wrapper.

## Fields
- Container for form rows. Hosts <field-ui> children (or <row-ui>/<col-ui> with labeled inputs) with consistent label + control + help layout.
- Different from <description-list-ui> (read-only key:value display) — fields-ui is for editable inputs.
- For dynamic field arrays (repeat-able rows) compose fields-ui with manual add/remove controls.

## Footer
- Use <footer-ui> as the bottom chrome row inside a PRIMITIVE container parent (<card-ui>, <drawer-ui>, <modal-ui>, <page-ui>). Typically holds 1–3 <button-ui> children — the most common pattern is a Cancel / Confirm pair inside <modal-ui> or <drawer-ui>.
- Use `justify="end"` (default) for Cancel+Confirm and trailing action clusters; `justify="between"` when one action lives left and one right (e.g. Back / Submit); `justify="center"` for single-button confirm flows; `justify="start"` for solo leading actions like Back.
- Do NOT substitute <footer-ui> for bespoke shell-tier bottom chrome. Inside <admin-sidebar slot="footer"> use <admin-statusbar>; inside <editor-shell> use <editor-statusbar>. <footer-ui> is exclusively the in-container action bar at the primitive tier.
- Do NOT wrap children in <row-ui> just to align them — the `justify` prop already lays children out horizontally. A nested row-ui double-applies layout and breaks the chrome's gap tokens.
- Prefer flat <button-ui> children. If you need non-button content (text summary, pagination indicator), keep it as a single flat child so `justify` resolves cleanly.

## Grid
- For asymmetric column ratios, use [columns="N"] plus [span="M"] on the child that should be wider. Never set gridTemplateColumns via inline style in compositions — the component's responsive resolver sets it internally for @bp values; direct inline style bypasses that and reads as a local hack.
- Canonical ratios: 1:1 → columns="2"; 2:1 → columns="3" + span="2"; 3:1 → columns="4" + span="3"; 3:2 → columns="5" + span="3"+"2"; 4:1 → columns="5" + span="4". Round unusual ratios (e.g. 7:5) to 3:2.
- The default (no columns attribute) gives equal auto-columns flowing in a single row. Prefer the explicit numeric attribute for dashboard rows so the layout is predictable when items wrap.
- For viewport-responsive layouts use `@bp` notation on [columns] and [gap]: columns="1 2@sm 4@lg" gives 1 column on xs, 2 from sm, 4 from lg/xl. Breakpoints (mobile-first, min-width): xs=0, sm=480, md=768, lg=1024, xl=1280. Unannotated value = base (smallest). Each @bp overrides upward until the next larger annotation takes over.

## Header
- Use <header-ui> as the top chrome row inside a PRIMITIVE container parent: <card-ui>, <drawer-ui>, <modal-ui>, <page-ui>. It is a CSS-only slot stub — no JS, no events. The parent's @scope rules drive layout and styling.
- Compose with the default slot for the heading (typically <text-ui variant="title">) and slot="action" for trailing controls (button-ui, badge-ui). The slot vocabulary (icon / heading / description / action) is shared with <aside-ui> / <section-ui> / <footer-ui> per ADR-0009.
- Do NOT substitute <header-ui> for bespoke shell-tier chrome. Inside <admin-content> / <admin-sidebar> use <admin-topbar>; inside <chat-shell> use <chat-header>; inside <editor-shell> use <editor-toolbar>. Those modules carry shell-specific slot vocabulary and CSS that <header-ui> does not. (admin-topbar.yaml codifies this explicitly.)
- Do NOT wrap header-ui's children in <col-ui> or <row-ui>. The container parent's @scope already lays out default+icon+heading+description+action via slot vocabulary; extra layout primitives fight the chrome styling.
- The `padding` attribute is a bare boolean — it enables default header padding, but the *scale* is set by the container parent's own `padding` prop. Do not pass numeric values.

## Heatmap
- Grid-cell heatmap visualization (calendar heatmap, density grid). Cells colored by value via OKLCH scale.
- For continuous time-series use <chart-ui> with appropriate variant; heatmap is for grid-cell density.
- Tooltip on hover shows cell value; click events emit cell coordinates.

## Icon
- [name] must be a registered Phosphor key (e.g. "house", "gear", "trash", "warning-circle"). Free-form names render empty. When unsure, default to a semantically obvious glyph (check, x, warning-circle, info).
- Set [label] when the icon is the ONLY content of an interactive control (icon-only button); omit [label] when icon is decorative beside a text label. The host stamps role="img" + aria-label when [label] is set.
- Use named [size] tokens (sm | md | lg | xl) — px / rem values only when matching a strict design spec. [size="xl"] is conventional for empty-state hero icons; [size="md"] is the default for inline use.
- Use slot="icon" on parent primitives (<button-ui>, <badge-ui>, <alert-ui>, <list-item-ui>, <input-ui>, <nav-item-ui>, every chrome bar) — do NOT bare-render <icon-ui> beside text in a <row-ui> when the parent supports the [slot="icon"] convention.
- Use [weight="fill"] for selected / active toggles, [weight="regular"] (default) for inactive. Do not mix weights in the same set; the visual inconsistency reads as a bug.

## Image
- Use for content images (illustrations, screenshots, user-uploaded photos). For icons use <icon-ui>; for embedded artifacts use <embed-ui>.
- Set aspect-ratio attribute to lock dimensions and prevent layout shift during load.
- For decorative-only images, set alt='' and aria-hidden='true' so screen readers skip them.

## InlineEdit
- Use inline-edit-ui for click-to-edit titles, draft names, table-cell text, breadcrumb labels — anywhere the user expects to edit text without opening a dialog.
- inline-edit-ui IS form-participating (extends UIFormElement). Pair with a hidden <form> + name="..." to submit edits as a field.
- Distinct from <input-ui> (always shows input chrome) and from <field-ui> (stacked label + input composition). inline-edit reads as text in the static state.
- Listen to `change` event (detail.value, detail.oldValue) for commit; `cancel` for Escape. Default commit=blur saves on focusout + Enter.

## InlineMessage
- In-flow annotation under a form input (validation feedback, hint copy, inline confirmation). Severity via [variant] (info, success, warning, danger).
- For overlay / banner-style notices use <alert-ui> instead; for transient toasts use <toast-ui>.
- Place inside <field-ui>, <col-ui>, or <row-ui>; never as a page-level banner.
- Do not nest a focusable child (button, link with action) — InlineMessage is non-interactive annotation.

## Input
- <input-ui> is the canonical single-line text input. The host IS the contenteditable surface — NEVER wrap a native <input>. The sole exception is type="password", which internally uses a real <input type="password"> for masking (per ADR-0025).
- Wrap <input-ui> in <field-ui label="…" hint="…" error="…"> for the canonical stacked label / hint / error chrome. The inline [label] / [hint] / [error] props are also supported on the primitive for compact use.
- Form participation is implicit via UIFormElement. Set [name] for FormData submission; [required] / [disabled] / [readonly] reflect; listen for `change` (blur or Enter commit) and `input` (per keystroke). `submit` event fires when Enter commits the value (used by <chat-composer>'s `composer-submit` forwarding).
- For numeric input use [type="number"] with [min] [max] [step] [precision] [prefix] / [suffix] — this stamps a contenteditable surface + <button-ui> / <icon-ui> stepper column with ARIA spinbutton semantics. Read `el.valueAsNumber` for the parsed Number. Never substitute a native <input type="number">.
- Inside <chat-composer>, the canonical inner input is <chat-input-ui> (chat variant subclass — adds the send button + model picker + paste-to-attach plumbing). The plain <input-ui> primitive ALSO fires a bubbling `submit` event on Enter (unconditional, no opt-in attribute); <chat-input-ui> simply builds on that semantic.

## Inspector
- Developer-tools pane for A2UI runtime state — composes <tabs-ui> + <code-ui> internally.
- Binds to a <canvas-ui> / <a2ui-root> via value= or implicit selector; shows live doc JSON + rendered HTML + event log.
- Use only in dev/debug surfaces; not for product UI.
- Place in a right-pane inside <editor-shell-ui>'s editor-sidebar slot for editor-style inspector layouts.

## IntegrationCard
- Use <integration-card-ui> for one tile in an integrations grid. Set `provider` and `name` — both are required for analytics keys and the accessible name. Set `description` so users don't have to look the integration up externally.
- `status` MUST be one of `available | connected | error | pending | coming-soon`. The button label and variant are DERIVED from `status` — never slot a <button-ui> directly in the card body. Use the `actions` slot for an overflow <menu-ui> with secondary actions like Reauthenticate or Disconnect.
- When `status="error"`, set `error-message` — otherwise the user sees a Retry button with no context. The message renders below the description in danger-text color.
- `logo` accepts a URL (containing `/`, renders as <img>) or a registered icon name (renders as <icon-ui>). Don't mix — one provider gets one logo source.
- Group integration cards in <grid-ui columns="auto-fit"> or use <integrations-page-ui> (SPEC-063) for the canonical Settings-panel grid. Never nest <card-ui> around an <integration-card-ui> — it IS a card variant.

## Kbd
- Inline-only — content from innerHTML, typically one or two key labels.
- In menu items, use the kbd= attribute on <menu-item-ui> instead of nesting <kbd-ui> directly.
- Chord notation uses + between keys (Ctrl+Shift+P); platform-symbol variants ⌘⌥⇧ are fine.

## Link
- Use `<link-ui>` for navigation; use `<button-ui>` for actions. They are NOT interchangeable.
- When wrapping action affordances that visually mimic links (e.g. 'Forgot password?' that triggers a reset flow), prefer `<button-ui variant="ghost">` over a fake `<link-ui>` — the affordance is semantically a button, just visually understated.
- For inline-sentence affordances ('I agree to the [Terms] and [Privacy]'), nest `<link-ui>` directly inside `<text-ui>` so it inherits the paragraph's font / size / line-height.

## ListWindow
- ListWindow.items MUST be an array of plain objects OR scalars. Functions / DOM nodes / Promises are invalid.
- ListWindow.render cannot be expressed in A2UI JSON — declarative authoring MUST use a <template> child (or default <list-item-ui> for objects with a text field).
- When items.length > 200, the validator SHOULD recommend ListWindow over List to keep the DOM tractable.
- ListWindow MUST have a defined height (via parent layout or style="height:..."). An unbounded-height windowed list defeats the windowing math.
- ListWindow.item-size SHOULD be set when item heights are known and constant — the fast-path is significantly cheaper.
- Do NOT nest ListWindow inside another scroll container; double-scroll containers break the IntersectionObserver math. Use one scroll boundary.
- Do NOT use ListWindow for short lists (< 50 items). The windowing overhead exceeds the cost of rendering all rows. Use List for short lists.
- Do NOT use for tabular data — that is Table with virtualized rows.

## ListItem
- Child of <list-ui> — one row of generic-list content.
- For navigation lists use <nav-item-ui> inside <nav-ui>; for menu items use <menu-item-ui>; for tree rows use <tree-item-ui>.
- Interactive list-items should have role='button' or be wrapped in <button-ui>; default is non-interactive.

## List
- Generic vertical list container — hosts <list-item-ui> or arbitrary children.
- For interactive selection lists use <nav-ui> (single-select navigation) or <menu-ui> (action menu); list-ui is content display.
- For data-grid / sortable / sticky-header needs use <table-ui> instead.

## LoadingOverlay
- <LoadingOverlay> MUST be placed inside a sized container with content (Card, Section, Table body, Chart). It absolutely positions against the nearest positioned ancestor. The parent's CSS must include `position: relative` (or any non-static positioning) — the component does not mutate parent layout styles.
- Toggle <LoadingOverlay active> from consumer code while async work is in flight. The overlay applies aria-busy="true" to its parent while active; on dismiss, both inert and aria-busy are released.
- The [delay] grace window (default 200ms) suppresses paint on fast-resolving loads. For server-rendered "definitely-slow" states (>1s known wait) consider [delay="0"] for immediate feedback.
- Do NOT use as a page-level / viewport loader — use a dedicated route-loader pattern. <LoadingOverlay> is container-scoped.
- Do NOT nest <LoadingOverlay> inside <Modal> or <Drawer> — those primitives own their own busy state. When they support it, pass [loading] to those directly.
- Do NOT use <LoadingOverlay> to disable a form during submit — use <Button type="submit" loading> for the submit affordance.
- The default slot accepts any busy indicator. When empty, a centered <Spinner size="lg"> is auto-stamped. Slot a <Skeleton> stack for placeholder-shaped loading; slot <Progress value=…> when the wait is determinate.

## Mark
- Use mark-ui for search-result match highlighting — wrap the matched substring in <mark-ui> within the surrounding text. Inline; preserves text flow.
- variant=warning (default) is the conventional yellow-marker tone; info for brand-color highlights; success for "new since" / "added" emphasis; danger for "removed" / "deleted-line" in diff prose.
- Distinct from <text-ui strong> (weight emphasis), <tag-ui> (block pill), <code-ui> (monospace). mark-ui is specifically the "visual rail behind matched text" use case.

## MenuDivider
- <menu-divider-ui> MUST be a direct child of <menu-ui>; a raw <hr> will render OUTSIDE the popover because <menu-ui> only hoists <menu-item-ui> and <menu-divider-ui> children via its direct-child selector.
- Use to group items by semantic tier — primary actions → secondary → destructive (danger). Avoid leading / trailing dividers and consecutive dividers; they produce visual noise without grouping value.
- Do not place a divider as the first or last child of <menu-ui> — the top / bottom popover padding already provides separation from the chrome.
- Has no props, slots, or events. Treat as an inert separator with role="separator". If you need a labeled group, split into multiple <menu-ui> instances stacked, not custom content inside a divider.

## MenuItem
- <menu-item-ui> MUST be a direct child of <menu-ui>; do not place inside arbitrary containers or other components — <menu-ui> hoists items into its top-layer popover via a `:scope > menu-item-ui` selector that requires direct descendancy.
- Always set a stable [value] — it is the only identifier surfaced on the parent's `action` event detail. [text] is the visible label; optional [icon] is a Phosphor icon name shown leading the label.
- Use [variant="danger"] exclusively for destructive / irreversible actions (Delete, Remove). "Sign out" is NOT danger. Pair danger items with an explicit confirm flow (<modal-ui> destructive-confirm pattern) when the action cannot be undone.
- Set [disabled] (not [hidden]) when an action is contextually unavailable — disabled items remain visible for affordance discoverability but skip roving focus + don't fire `action`.
- Prefer [icon] + [text] props over slotted markup for consistency. Use slot="icon" / slot="text" only when you need custom markup (e.g. <avatar-ui slot="icon">, <kbd slot="trailing"> shortcut hint).

## Menu
- <menu-ui> MUST have exactly one child with slot="trigger" (typically <button-ui>, but any focusable element works). Without it the menu cannot open. The trigger lives in light DOM; the items are hoisted to a top-layer popover on open.
- Default slot accepts only <menu-item-ui> and <menu-divider-ui> children — no submenus, headers, or arbitrary content. Roving tabindex + Arrow / Home / End / Enter / Escape keyboard nav is built in.
- Listen for the `action` event on <menu-ui> (or an ancestor — bubbles). Detail = { value, text }. Do NOT treat menu-ui as a value-holder; for single-select form input use <select-ui> instead. Menu fires actions, doesn't store state.
- Use <menu-ui> for transient action surfaces (kebab / ⋯ row actions, workspace / user switchers, view-as toggles). For persistent side navigation use <nav-ui>; for inline (non-popover) command lists use <action-list-ui>; for searchable command palettes use <command-ui> inside <admin-command>.
- Set [placement="top-start"] or [placement="top-end"] when the trigger sits near the bottom of the viewport (statusbar / footer menus); default [placement="bottom-start"] otherwise. Adjust [gap] (default 4px) only when chrome demands it.

## Modal
- Reflect modal visibility via the [open] boolean attribute on the host (open=true / open=false). Do NOT toggle [hidden], CSS display, or wrap in a sibling visibility container — modal-ui owns the native <dialog> lifecycle, focus trap, ::backdrop, and Escape-dismiss. Listen for the `close` event to react to dismiss.
- Compose modal-ui with the same triplet as drawer-ui / card-ui: <header> (or [slot="header"]) + <section> (or default body) + <footer> (or [slot="footer"]). Set the title via [text] (which stamps the heading + aria-label) or a <span slot="heading">. Do not author your own close button unless using [permanent].
- Use modal-ui (centered, interruptive) for short confirmations, destructive prompts, and transient previews — typically ≤2 form fields or a single decision. For edge-anchored multi-field detail editors and mobile sheets, use <drawer-ui> instead. The destructive-confirm-modal pattern lives at catalog/ui-patterns/app/destructive-confirm-modal/ as the canonical reference.
- modal-ui is NOT the Cmd+K command palette. The palette is a bespoke shell-tier component (<admin-command> under <admin-shell>); do not model command palettes as modal-ui surfaces even though the visual is overlay-like. The palette owns its own keyboard / filter loop.
- Use [size] presets (sm = 24rem | md = 32rem | lg = 48rem | xl = 95vw) to scale width. Do not override width with inline style or wrap modal-ui in a sizing container — the [size] attribute is the only supported width contract.

## NavGroup
- Composition: <nav-group-ui> MUST be a direct child of <nav-ui> (or standalone with explicit variant="section" outside a rail). Default slot accepts <nav-item-ui> children; do NOT wrap them in layout primitives — selection, keyboard, and the section-variant cascade all rely on direct descendancy.
- Variant cascade: when parent <nav-ui variant="section">, this group inherits section styling (static kicker header, children always visible) via CSS — without touching its JS state. Set [variant="section"] explicitly only when there is no <nav-ui> parent, OR to override a primary-rail context.
- Collapsibility: primary variant — [collapsible] (default true) toggles [open] via header click / Enter / Space; fires `group-toggle` event with detail {text, open}. When the parent <nav-ui> is collapsed to icon-only, the header opens a popover with this group's children instead. Section variant ignores [open] / [collapsible] (children always visible).
- Decision rule: use <nav-group-ui> to group ≥2 related navigation links under one label inside <nav-ui>. For a single link, use <nav-item-ui> directly. For switching sub-views inside one page, use <tabs-ui> / <tab-ui>, never <nav-group-ui>.
- Anti-patterns: do NOT nest <nav-group-ui> inside another <nav-group-ui> (flatten the hierarchy or split into two groups). Do NOT place non-<nav-item-ui> content in the default slot. Do NOT use it as a generic disclosure — for that use <accordion-ui> or <details>.

## NavItem
- Composition: <nav-item-ui> MUST be a direct child of <nav-ui> or <nav-group-ui>. Never wrap in <col-ui> / <row-ui> / <li> / <a> — the item is already a focusable, accessible link element; wrapping breaks selection, ARIA, and the section-variant CSS cascade.
- Variant cascade: when an ancestor <nav-ui variant="section"> (or sibling-group with section variant) is present, this item inherits section styling (flat row, no reserved icon space when icon absent, left-edge selected accent) via CSS. Set [variant="section"] explicitly only when used standalone outside a section rail, or [variant="primary"] to override the cascade for a single highlighted item.
- Selection: [selected] is set by the parent <nav-ui>'s select() method; do NOT set it on multiple items simultaneously. Provide [value] as a route or anchor; consumers listen for `nav-select` on <nav-ui> rather than per-item.
- Decision rule: use <nav-item-ui> for a single navigable link (route or anchor). For an in-page view toggle, use <tab-ui>. For a popover menu action, use <menu-item-ui>. For an action button styled like a nav row, use <button-ui variant="ghost"> — not <nav-item-ui>.
- Authoring options: default stamping renders icon+text+badge from attributes. Named slots `icon`, `text`, `trailing` are also supported for custom content (e.g. a `<kbd slot="trailing">⌘K</kbd>` shortcut hint on a command-trigger item — see the admin-shell playground). Do NOT nest <nav-item-ui> inside another <nav-item-ui>.

## Nav
- Composition: place <nav-ui> inside <admin-sidebar slot="leading"> wrapped in <section-ui> for app sidebars; inside an <aside data-subnav> with variant="section" for section / subnav rails; standalone on docs / auth pages. Children: <nav-group-ui>, <nav-item-ui>, optional <hr data-nav-divider>.
- Variants: variant="primary" (default) — app sidebar; ResizeObserver collapses to icon-only ≤96px; collapsible groups open as a popover when collapsed. variant="section" — subnav rail; quieter chrome; optional [heading] kicker rendered via ::before.
- Section-variant cascade (ADR-0015 § Nav consolidation): variant="section" on <nav-ui> cascades visually to direct <nav-group-ui> / <nav-item-ui> descendants via CSS `:not([variant])`. Children's JS state is NOT mutated; the cascade is purely visual. Explicit [variant] on a child always wins — use it to escape the cascade or style a standalone group/item.
- Decision rule: if the user navigates AWAY (different page, route, or anchor) → <nav-ui>. If the user switches VIEWS within the same logical page → <tabs-ui>. Never use <nav-ui> as an in-page section switcher.
- Anti-patterns: do NOT wrap <nav-ui> children in <col-ui> / <row-ui> — wrapping breaks selection bubbling + the variant cascade. Do NOT nest <nav-ui> inside <nav-ui>. The legacy 6-element family (<app-nav-ui> / <section-nav-ui> / <app-nav-item-ui> / <section-nav-item-ui> / <app-nav-group-ui> / <section-nav-group-ui>) was retired in ADR-0015 — only <nav-ui> / <nav-group-ui> / <nav-item-ui> remain.

## Noodles
- SVG connection lines between children with declared ports — bezier (default), step, or straight curves.
- Place inside <canvas-ui> with port-declaring children (nodes); noodles draws connections between matching port refs.
- editable attribute enables drag-to-connect interactions; default is render-only.
- For data-flow visualizations only — for general-purpose lines/shapes use raw SVG.

## NumberFormat
- Use for read-only numeric display with locale-aware formatting. For numeric INPUT use <input-ui type=number>; for KPI big-number display use <stat-ui>.
- [number-style=currency] REQUIRES [currency] to be set to a valid ISO 4217 code. Without it the element renders nothing rather than producing malformed output.
- [number-style=percent] treats the [value] as a fraction (0.5 → 50%). To display 50 as '50%' pass value=0.5 OR keep [number-style=decimal] and append '%' manually.
- Compact notation auto-defaults maximumFractionDigits to 1. Override only when extra precision is needed (rare for compact display — defeats the purpose).

## OptionCard
- Use <option-card-ui> for one-of-N choices where each option needs a heading + description, a leading icon, or both (onboarding personas, plan tiers, source pickers). Use <radio-ui> for dense forms with short labels and no descriptions. Use <segmented-ui> for compact horizontal single-select with short labels.
- Group siblings by sharing the same [name] attribute — they auto-form a radiogroup. Form participation is implicit via UIFormElement: submits `name=value` with parent form. Wrap the group in <col-ui gap="2"> for default layout or <grid-ui> for [layout="tile"] hero pickers.
- Set [checked] on the recommended / default option. Never leave the group with zero checked at first paint unless [required] is set and the form intentionally demands a deliberate choice.
- Use [layout="tile"] only for hero pickers (3–4 options max) where the icon is a primary brand cue (plan tiers, role pickers). Use [layout="default"] (left-indicator) for everything else.
- Put conditional follow-up inputs (a <textarea-ui> on "Other", a <select-ui> on "Custom") in the default slot — the spillover content auto-hides when the card is not checked. Do NOT duplicate this visibility logic in JS.

## OtpInput
- Use <otp-input-ui> for any fixed-length verification code (TOTP, email / SMS OTP, MFA enrollment) — NOT <input-ui type="number">. otp-input-ui provides per-digit focus management, paste-splitting of full codes, and the `complete` event that fires exactly once when all digits are filled.
- Always set [autocomplete="one-time-code"] so iOS / Android keyboards surface inbound SMS codes via the platform autofill UI.
- Listen for `complete` to auto-submit (fires exactly once per fill cycle), and `change` / `input` for live validation. Form participation is implicit via UIFormElement: [name] for FormData; [disabled] reflects.
- Default [length=6]. Use [length=4] only for legacy 4-digit SMS codes and [length=8] for backup codes. Never compose two <otp-input-ui> side-by-side — use a single one with the right length.
- Canonical placement: inside a <col-ui align="center"> beneath a description text. Do NOT wrap in <field-ui> — otp-input-ui has no label slot and visual centering is the canonical chrome.

## Page
- Top-level page container — wraps an entire route's content surface.
- Inside <admin-shell-ui>, <chat-shell-ui>, or <editor-shell-ui>, use the shell's own body slot instead; page-ui is for standalone routes without shell chrome.
- Hosts arbitrary children — no enforced child contract.

## Pagination
- Renders below tables, card grids, or list views for paginated data. Emits page-change events.
- For cursor-based / infinite-scroll patterns use a custom load-more <button-ui> instead; pagination-ui is offset-based.
- Page-number range auto-truncates with ellipsis for high counts; set siblings= to control visible window size.

## Pane
- pane-ui is a resizable / collapsible content panel. As a standalone primitive (no [side] attribute), it carries its own four-sided chrome and a right-edge resize grabber. As a horizontal-sibling child of a layout container (set [side="leading"] or [side="trailing"]), the chrome and grabber move to the inner edge so adjacent panes share a single seam.
- Wrapped by <editor-sidebar slot="leading|trailing"> inside <editor-shell>, and by <admin-sidebar slot="leading|trailing"> inside <admin-shell>. The bespoke sidebar owns [collapsed] / [resizing] reflected state + localStorage persistence; the inner pane-ui owns the physical drag. Don't reimplement drag in the bespoke sidebar — delegate to <pane-ui resizable>.
- Inner shape inside a pane-ui is the conventional <header> + <section> + optional <footer> slot pattern. Headers carry [slot="action"] button clusters; sections hold the navigator tree, inspector form rows, or other primary content.
- For a standalone resizable two-pane layout (no shell), nest panes directly inside a flex row — both with [side]-typed chrome — and the resize handle will live on the seam between them. For a non-resizable summary pane (a fixed-width detail rail), drop [resizable] and pane-ui collapses to a static container.

## PasswordStrength
- Pair with <input-ui type=password> via a JS listener (input.addEventListener('input', e => meter.value = e.target.value)). The meter does NOT participate in form data — it is a display indicator.
- The score is 0 (Weak) / 1 (Fair) / 2 (Good) / 3 (Strong). [min-score] sets the threshold for the `satisfied` boolean in score-change events. Use the boolean to gate a submit button.
- Do NOT set value via setAttribute — value is JS-property only and never appears in rendered HTML (security: avoids leaking the password into the DOM).

## PipelineStatus
- Single updating pipeline status indicator — status pill with optional progress bar.
- stage + complete + message attributes drive the current display state.
- For multi-step wizards use <stepper-ui>; for chronological history use <timeline-ui>; pipeline-status is a single current-state pill.
- Renders inline — place inside toolbars, status bars, or agent message bodies.

## Popover
- <popover-ui> wraps a focusable trigger (slot="trigger", typically <button-ui>) + arbitrary interactive content (slot="content"). Never put bare text in slot="trigger" — it must be focusable so keyboard users can open the popover.
- Decision rule vs adjacent surfaces. (a) For a list of action items use <menu-ui> instead — menu-ui is the specialized popover with role=menu + roving tabindex. (b) For read-only hover hints use <tooltip-ui>. (c) For centered focus-trapping dialogs use <modal-ui>. (d) For edge-anchored multi-field forms use <drawer-ui>. popover-ui is the GENERAL anchored surface for everything else (inline forms, color pickers, theme panels, export menus with non-action content).
- Placement convention (ADR-0034): default `bottom` centers under the trigger — correct for wide pickers (calendar, color, date-range, filter forms). Use `bottom-start` for trigger-width menus (action lists, listboxes, breadcrumb overflow — popover width ≈ trigger width). Use `bottom-end` only when the trigger sits at the right edge of a container by construction (toolbar spillover). Use `top-*` when the trigger sits low in the viewport (statusbar). [gap] (default 4px) sets offset from anchor.
- [trigger="hover"] is for non-essential disclosure only — never use it for popovers containing inputs, destructive actions, or anything the user must interact with via keyboard. Default [trigger="click"] for everything interactive.
- Do NOT nest <modal-ui> or <drawer-ui> inside slot="content"; popovers are non-modal anchored surfaces, not dialog hosts. Stacking dialog surfaces inside a popover breaks focus management.

## ProgressRow
- Labeled progress row — composes a label + <progress-ui> + optional value display in one horizontal row.
- For standalone progress bars without a row context use <progress-ui> directly.
- Inside lists (multiple tasks with progress) stack multiple progress-rows in a <col-ui>.

## Progress
- Use for in-progress task feedback with known or indeterminate state. Value < 0 = indeterminate animation.
- For labeled task lists (multiple progress bars with row labels), use <progress-row-ui> instead.
- Spinner variant (variant='spinner') for circular loading indicators; bar variant default for linear.

## QRCode
- Set [value] for URLs / share links / plain text. The built-in encoder covers byte-mode UTF-8 up to QR version 10 (~150 chars at ECC-M, more at lower ECC levels).
- For data that exceeds version 10, switch to a higher ECC level downgrade ([error-correction=L]) OR use a BYO encoder and pass the precomputed [matrix].
- When QR sits over a non-white surface, set [background] to a solid color. White-on-image QR backgrounds are unreliable for scanners — quiet-zone contrast against the dark cells is the load-bearing visual.
- The [margin] (quiet zone) defaults to 4 cells per QR spec. Smaller margins may scan on some devices but not others; do not go below 2.

## Radio
- Self-labeling widget — use the [label] attribute directly; do NOT wrap in <field-ui>. The widget renders its own label inline via CSS attr() pattern. For radio groups, the canonical pattern is a column of bare <radio-ui label='…'> elements sharing a [name=] — no field-ui wrapper around each radio.
- Use the [name] attribute to group radios — exactly one is selected per name group; form-associated.
- For button-style single-select clusters (visually richer) use <segmented-ui> + <segment-ui> instead.

## Range
- Two-handle slider for selecting a range (min + max). Form-associated; emits two-value change events.
- Different from <slider-ui> (single value) — use range when both endpoints matter.
- Step attribute controls handle increments; min/max set the bound rails.

## Rating
- Star/icon-based ordinal rating input. Form-associated; emits numeric value 0..max via change events.
- For thumbs-up/down agent feedback use <agent-feedback-bar-ui> instead — different semantics + visual.
- max attribute sets the scale (default 5); allow-half attribute enables half-step granularity.

## RelativeTime
- Use for displaying a single timestamp as a relative phrase. Self-updates on a tick so the rendered text stays current; no parent re-render required.
- Set [datetime] to an ISO 8601 string. Empty datetime renders nothing — do not stamp a relative-time-ui element until you have a timestamp value.
- [update-interval=0] freezes the render (no tick). Use for historical timestamps that will never become 'just now' (audit-log rows, version-history entries from days+ ago).

## RichText
- Rich-text display + editor primitive. Renders paragraphs, lists, headings, and inline formatting; contenteditable when editable= is set.
- Different from <text-ui> (single semantic block) — richtext handles multi-paragraph + inline marks.
- For plain code use <code-ui> or richtext with code variant; for chat input use <chat-composer-ui>.

## Row
- Horizontal-stack flex container. Children flow left-to-right with token-driven gap.
- Pair with <col-ui> for vertical layouts; both share the same gap-token contract.
- Wrap attribute enables flex-wrap; default is nowrap.

## Search
- Search-input variant of <input-ui> with built-in search icon + clear affordance.
- For command-palette interactions use <command-ui> (which composes search + menu).
- Emits input events on each keystroke; debounce in the consumer for live-search.

## Section
- Use <section-ui> as the content body region inside a primitive container parent (<card-ui>, <drawer-ui>, <modal-ui>, <page-ui>) OR inside the body region of a bespoke shell-tier sidebar (<admin-sidebar> nav body — site/index.html and the admin-shell playground use this canonically). It is a CSS-only chrome stub for ADR-0009 slot vocabulary; the parent's @scope handles padding and borders.
- `scroll` is a CONDITIONAL attribute. It makes section-ui the scroll container ONLY inside <card-ui> / <drawer-ui> / <modal-ui>. Inside <page-ui> and shell-tier hosts (<admin-shell>, <admin-page-body>) it is a no-op — those parents own their own scroll surface. Never nest <section-ui scroll> inside another scroll container (nested scroll is a UX anti-pattern).
- Use `bleed` to remove section padding so children reach the card / drawer edges — typical for cover images, full-width charts, or media galleries (apps/saas/members.contents.html uses this pattern to wrap <table-ui raw>). Mix bleed and non-bleed sections in the same card to alternate edge-to-edge media with padded prose.
- Do NOT substitute <section-ui> for bespoke shell-tier body regions. Inside <admin-page> use <admin-page-body>; inside <admin-content> use the bespoke children. <section-ui> is reserved for the primitive container chrome triad + the <admin-sidebar> nav body.
- <section-ui> is the right place for layout primitives (<col-ui>, <row-ui>, <grid-ui>) that organize body content. Do NOT put those layout primitives inside <header-ui> or <footer-ui> — the parent's chrome scope already lays those rows out.

## Segment
- Child of <segmented-ui> — one selectable option button in a single-select group.
- Different from <toggle-option-ui> (which is multi-select inside <toggle-group-ui>).
- Selected state managed by parent <segmented-ui> via active attribute; do not set selected directly on segment.

## Segmented
- Single-select segmented control. Hosts <segment-ui> children; exactly one selected at a time.
- For multi-select use <toggle-group-ui> + <toggle-option-ui> instead.
- Use for view-mode switches (grid/list, light/dark) or short filter sets (3-5 options); for longer sets use <tabs-ui> or <select-ui>.

## Select
- Use <select-ui> for single-select with > 4 options or any list that benefits from a popover. Prefer <segmented-ui> / <radio-ui> when ≤ 4 visible options fit the row.
- Compose options via native <option> / <optgroup> children — other tag names are silently ignored (per §225 v0.5.9) and warned once at runtime. Or set `.options` programmatically as an array of `{value, label, disabled?}` (grouped form: `{label, options:[…]}`).
- For dynamic option lists rendered inside <editor-shell>, set the JSON via the [data-options] attribute — <editor-shell>'s wireSelects() finds select-ui[data-options], JSON.parses the attribute, and assigns `.options` on connect. Useful for static-HTML toolbars where JS hydration would be awkward.
- <select-ui> owns its own label / hint / error chrome (via [label] / [hint] / [error] props). Only wrap in <field-ui> when you need to share the field-chrome stack with sibling inputs in the same form row group.
- Enable [searchable] for > 10 options; add [free-text] only when unmatched values are valid (tag entry, email-with-suggestion). Use [multiple searchable] for multi-select rather than authoring a separate multi-select primitive.
- For multi-select, set [multiple] — the trigger automatically renders <tag-ui> chips per selected option; the popover renders checkbox-style option rows where clicks toggle membership without closing. Form value is comma-separated under [name]. There is NO `<multi-select-ui>` tag — that name does not exist.
- Multi-select bulk controls: [select-all] renders a "Select all" / "Clear" control above the option list; [clearable] adds a clear-all `x` affordance to the trigger. [max-chips] caps the visible chip count and renders "+N more". [min] / [max] gate form validity.

## Skeleton
- Use to placeholder content during loading. Shape via CSS sizing (width/height/border-radius); shimmer is automatic.
- Compose multiple <skeleton-ui> blocks to mock the actual content shape (card-skeleton, row-skeleton, etc.).
- For post-load empty states use <empty-state-ui> instead; skeleton is pre-load only.

## SkipNav
- Place as the FIRST focusable element on the page — inside <body> before any nav / shell chrome. Otherwise keyboard users still tab through the nav before hitting the skip link.
- Target [#main] (or whatever id wraps the routed content). The target element should have tabindex="-1" so focus lands on it after the link is activated (otherwise focus may move to nothing perceptible).

## Slider
- Default mode is single-handle. Form-associated; emits numeric change events with detail.value.
- For two-handle range selection (price filters, date ranges, audio range gates), set [dual] and use [lower-value]/[upper-value] instead of [value]. Form-data submits as "<lower>,<upper>" under [name]; change event detail carries {lower, upper}. Do NOT use <range-ui> — that primitive is a draggable numeric field, not a two-thumb range slider.
- [step] controls increments for both single and dual modes. Constraint: in dual mode, [lower-value] ≤ [upper-value] is enforced on each setter; reversed values clamp to equal.

## Spinner
- Use <Spinner> for INDETERMINATE loading where the duration is unknown. For determinate progress (a known fraction complete), use <Progress> (linear) instead. For known-shape placeholder loading, use <Skeleton>.
- When a Spinner is inside a Button, set tone="current" so it matches the button label color, and disable the button while the operation is in progress.
- When overriding [label], use a present-progressive verb form ("Loading", "Saving", "Uploading"). Never use "Spin" or "Wait" — they describe the visual, not the operation.
- Do not nest <Spinner> inside <Skeleton>; they are siblings (two different loading idioms), not parent/child.
- Do not stack multiple sibling <Spinner>s in the same viewport region. Use one parent-level Spinner instead — multiple spinners add visual noise without extra information.

## Stack
- Overlay/layer stacking container — children occupy the same area, stacked on the z-axis.
- Use for overlapping content (image + overlay, badge-over-avatar, drop-shadow stacks).
- Do not use for vertical content flow — that's <col-ui>. Stack-ui is overlap-only.

## Stat
- Use for prominent metric/KPI displays inside dashboard cards. Value + label + optional delta indicator.
- For inline percent/progress displays use <progress-ui>; stat is for standalone metrics, progress for completion bars.
- Delta indicator uses positive/negative semantic tokens; pass change= attribute with sign.

## StepProgress
- Compact step indicator — N dots/segments showing current step out of total.
- Different from <stepper-ui> (labeled, expanded) — step-progress is dense and label-free.
- For multi-task progress bars (multiple labeled rows) use <progress-row-ui> stack.

## StepperItem
- Child of <stepper-ui> — one numbered step with label + complete/current/upcoming state.
- State driven by parent's active-index; do not set state directly on stepper-item.
- For chronological events (no completion semantics) use <timeline-item-ui> instead.

## Stepper
- Hosts <stepper-item-ui> children with a parent active-index driving complete/current/upcoming states.
- Use for wizards, onboarding flows, multi-step forms.
- For read-only event history use <timeline-ui>; stepper requires forward progress semantics.

## Stream
- Renders an AsyncIterable<string> as streaming text — canonical for LLM token streaming.
- Place inside <chat-thread-ui> message bodies for LLM responses; standalone for log tailing.
- pace attribute controls typewriter-effect speed; hide-cursor disables the blinking caret.
- For static (post-stream) display use <text-ui> or <richtext-ui>; stream-ui assumes live token feed.

## Swatch
- Use to display a single color sample with optional label. For interactive color picking use <color-picker-ui>.
- Inside design-token displays or palette grids; not for general decoration.
- Color value accepts hex, rgb, hsl, or oklch; oklch preferred for AdiaUI token alignment.

## Swiper
- Horizontal slide carousel with touch + arrow nav. Hosts arbitrary children as slides.
- For tab-switched peer views use <tabs-ui>; swiper is for content browsing, not panel switching.
- Set autoplay attribute for timed-rotation; otherwise user-driven only.

## Switch
- Self-labeling widget — use the [label] attribute directly; do NOT wrap in <field-ui>. The widget renders its own label inline via CSS attr() pattern. For settings rows (label-left, switch-right), put the descriptive text in switch-ui's own [label] attribute; do not introduce a field-ui wrapper. For descriptive helper text below the switch, use <text-ui variant='caption'> as a sibling — not field-ui's hint slot.
- Binary on/off only — no third state. For tri-state controls use <check-ui> with [indeterminate].
- Use for settings toggles, feature flags, mode switches; for form gates / consent / multi-select use <check-ui> instead.

## TableToolbar
- Pair <table-toolbar-ui> with <table-ui> via [for="<table-id>"] (or rely on first-sibling fallback when both are inside the same parent). One toolbar per table. Do NOT also use <card-ui>'s <header> on the same card — that produces a doubled chrome row.
- All four affordances (search, filter, sort, columns) default ON. Opt out individually via [no-search] / [no-filter] / [no-sort] / [no-columns]. The previous [searchable] / [filterable] attributes are deprecated — do NOT emit them.
- Place the toolbar ABOVE the <card-ui> containing the table-ui, or use [variant="card"] when standing alone outside a card-ui parent (the variant wraps the toolbar in card-style chrome).
- Use slot="action" (or [slot="actions"]) for trailing primary buttons (Invite, Export, +New). Use [text] / [count] props for the left cluster, or slotted [slot="title"] / [slot="count"] when content is markup (a <span> + <badge-ui>, etc.).
- Listen for toolbar events (`search`, `filter-change`, `sort-change`, `columns-change`) only to mirror state to URL / persistence / analytics. The toolbar already wires its changes into the bound table — you don't need to manually update the table.

## Table
- Canonical composition: wrap <table-ui> in <card-ui><section bleed> for edge-to-edge tables. The [bleed] removes section padding so columns span the full card width (see apps/saas/members, billing, admin-dashboard).
- Pair with <table-toolbar-ui for="<table-id>"> for any table that needs search / filter / sort / columns visibility. Do NOT re-implement those affordances in the card header — the toolbar auto-wires search/filter/sort/columns changes into the bound table.
- Cells truncate single-line by default (v0.6.21 §403 truncate-default). Opt out per-table with [wrap] for whole-table multiline, or per-cell with [data-wrap] on a single column / cell.
- Use [raw] in production app consumers — it disables the demo seed data so the table renders only consumer-provided rows / columns / data props.
- Listen for the `sort` event with detail.key + detail.dir (NOT .column / .direction). `cell-click` detail carries {key, row, value, dataIndex}. Per ADR-0027, table-ui composes check-ui, icon-ui, progress-ui, pagination-ui, skeleton-ui, badge-ui — consumer pages must explicitly import the ones they use.
- For large datasets (100+ rows), use [paginate="N"] to limit rendered rows and add a pagination bar. [virtual] is NOT a recognized prop and is silently ignored — there is no DOM-level virtual scrolling in table-ui. [paginate] is the supported performance mechanism. Wire [paginate] with optional [search] and the `page` event for server-driven pagination.

## Tab
- <tab-ui> only renders inside <tabs-ui>. Never use it standalone. The parent reads each tab's [text] + [icon] + [value] to render the button strip; the tab's default slot is the panel content that the parent auto-hides when inactive.
- [value] is required and must be unique among siblings — the parent <tabs-ui> matches its own [value] against each <tab-ui>[value] to decide which is active. [text] is the visible button label; optional [icon] is a Phosphor icon name shown leading the label.
- Use the default slot for panel content. Inactive <tab-ui> children are auto-hidden by the parent's [hidden] toggling; do NOT set [hidden] yourself unless you want to remove the button from the strip entirely (i.e. a temporarily-disabled tab whose strip button shouldn't render at all).
- Use [disabled] to keep a tab visible in the strip but non-selectable. Do not pair <tab-ui> with <button-ui> wrappers — the strip button is parent-rendered. Do not nest <tab-ui> inside another <tab-ui>.

## Tabs
- Decision rule: use <tabs-ui> when switching VIEWS within the same logical page (no route change, no URL change). For navigating AWAY (different page / route / anchor), use <nav-ui> instead. For a form-control segmented selector that returns a value, use <segmented-ui>.
- Children of <tabs-ui> MUST be <tab-ui> elements. The button strip is rendered from each child's [text] + optional [icon]; the parent auto-toggles [hidden] on inactive <tab-ui> children. Do not place arbitrary markup directly inside <tabs-ui> — wrap it in <tab-ui>.
- Canonical placements: inside <card-ui>'s <header> for in-card section switching; inside <editor-canvas-toolbar> for editor sub-views (see the editor sub-views recipe in patterns-recipes.md); or standalone as an in-page switcher. When standalone, wire sibling <div data-view="…"> panels via the `change` event (detail.value); for tabs whose content lives inside the <tab-ui> child, the auto-hide handles visibility.
- Set [value] to the initially active tab. If omitted, the first non-disabled <tab-ui> becomes active on connect. Set [orientation="vertical"] for left-rail tab strips.
- Variant caveat: only [variant="bordered"] is implemented (adds a subtle divider). [variant="underline"] is widely used in source but is equivalent to default (no-op). [variant="pills"] and [variant="segmented"] are declared in the enum but NOT styled — do NOT emit them; for a form-style selector use <segmented-ui>.

## Tag
- Use <tag-ui> for INTERACTIVE / DISMISSABLE labels — filter chips, autocomplete tokens, user-managed labels. Tag-ui fires a `remove` event when [removable] is set. For READ-ONLY status flags (counts, Beta / New / Deprecated markers, notification dots) use <badge-ui> instead — badge has no remove event and includes the [status] shorthand.
- Set [removable] and listen for the `remove` event (detail: {text, value}) when the tag represents a user-applied filter or selection that can be cleared.
- [variant] maps to semantic state of the underlying record: success = active / approved, warning = pending, danger = blocked / error, info = neutral-emphasis. Default (no variant) for unlabeled categories.
- Use [size="sm"] for inline-with-text contexts (doc page headers, table cells, badges next to titles); [size="md"] (default) for filter-bar chips and standalone tag rows.
- Group multiple tags inside a <row-ui gap="2"> — never stack them vertically; vertical lists of dismissable items are an <action-list-ui> use case, not <tag-ui>.

## TagsInput
- Use <tags-input-ui> for OPEN-SET free-form token entry — labels, keywords, email recipients, comma-separated lists. For CLOSED option sets (pick N from a fixed list), use <select-ui multiple> (SPEC-040) — that primitive gates the value against `options[]`.
- `TagsInput.value` MUST be a string array. Pass `["a","b"]`, not the comma-joined string `"a,b"`. The host parses string-form `value` attributes as JSON; non-array shapes throw `invalid`.
- `delimiter: "enter"` disables in-line character commits; only the Enter key (or programmatic `addToken`) commits. Use this when the token grammar legitimately includes the default `,`.
- `unique: true` (default) silently coalesces accidental duplicate adds. Do NOT emit `invalid` for those — the contract specifies silent dedup for ergonomic typing.
- `validateFn` is a JS property, NOT serializable in A2UI JSON. Wire validators post-mount via DOM scripting; A2UI authors should not expect to declare a validator in the JSON payload.
- Chips are rendered automatically from `value`. Do NOT slot individual <tag-ui> children manually — that decouples the rendered DOM from the form value and breaks Backspace removal.

## Text
- Use for typographic content with semantic role (heading, body, label, caption). Variant attribute sets the role.
- For inline-flow rich content with multiple paragraphs, use <richtext-ui> instead.
- Heading variants (h1, h2, ...) are not auto-tagged — set the role explicitly via variant.

## Textarea
- <textarea-ui> is the canonical multi-line text input. The host IS the contenteditable surface — NEVER use a native <textarea> (banned by ADR-0025).
- Wrap <textarea-ui> in <field-ui label="…" hint="…" error="…"> for the canonical labeled stack. The inline [label] / [hint] / [error] props are also supported on the primitive for compact use.
- Form participation is implicit via UIFormElement. Set [name] for FormData submission; [required] / [disabled] / [readonly] reflect; listen for `change` (on blur after value change) and `input` (per keystroke).
- Use [rows] to set initial height (default 3) and [resize] (vertical | horizontal | both | none; default vertical) to control user resize. Never substitute a native <textarea> just to get rows / resize.
- Enter (without Shift) dispatches a bubbling `submit` event; Shift+Enter inserts a newline. This is unconditional — there is no opt-in/opt-out attribute. For chat composer surfaces wrap inside <chat-composer> + <chat-input-ui> (which adds the send button + model picker + paste-to-attach plumbing on top of the same Enter→submit semantics).

## TimePicker
- `<time-picker-ui>` is the canonical standalone time-of-day picker. Per ADR-0025 NEVER wrap a native `<input type="time">` — segments are contenteditable spans + ElementInternals provides form participation.
- `value` MUST be ISO 8601 time `HH:mm` or `HH:mm:ss` (24-hour), or empty string. Localized formats (e.g. "9:30 AM") are not accepted; display formatting is derived from `hour-cycle` regardless of how the value is stored.
- `step` is in seconds. 60 = minute precision (default); 900 = 15-minute precision (meeting-time common); 1 = second precision (requires `precision="second"`).
- `precision="second"` exposes the seconds segment AND emits `HH:mm:ss`. Default `precision="minute"` emits `HH:mm`.
- `hour-cycle` overrides locale-derived behavior. Set explicitly (`h12` / `h23`) when the surface needs a specific cycle (cron editors, log queries, system surfaces).
- For datetime selection use `<datetime-picker-ui>` (SPEC-038) — it composes this primitive as its time pane.

## TimelineItem
- Child of <timeline-ui> — one chronological event with timestamp + content + optional icon dot.
- Different from <stepper-item-ui> (process steps with completion state) — timeline-item is event history.
- Order is DOM-order — no auto-sort by timestamp.

## Timeline
- Use for chronological event lists (audit log, activity feed, version history).
- Hosts <timeline-item-ui> children only.
- For multi-step processes with progress state use <stepper-ui> instead; timeline is read-only history.

## Toast
- Single ephemeral notification item — auto-dismissing or manually-closable.
- Typically posted into <feed-ui> via UIFeed.post(...); for inline persistent alerts use <alert-ui> instead.
- Variant maps to severity (info, success, warn, error); same tokens as <alert-ui>.

## TableOfContents
- Use for in-page section navigation. Pair with a sticky container in an aside / right rail for the docs-site outline pattern.
- Default scans the toc-ui's parent for h2/h3 headings. Set [target] to a CSS selector for a specific container; set [headings] to a comma-separated tag list (e.g. h1,h2,h3) for different depth coverage.
- Headings missing an [id] receive an auto-generated slug from their text content. Existing ids are preserved.
- Smooth-scroll on click is handled by the global `scroll-behavior: smooth` in resets.css (gated by prefers-reduced-motion). Do NOT add per-toc-ui smooth-scroll JS — the global wins.

## ToggleGroup
- Multi-select button cluster — hosts <toggle-option-ui> children, each independently toggleable.
- Different from <segmented-ui> (single-select) — toggle-group emits a SET of active values.
- Use for filter chips, multi-flag toggles, day-of-week pickers; for binary on/off use <switch-ui>.

## ToggleOption
- Child of <toggle-group-ui> — one independently-toggleable button in a multi-select cluster.
- Different from <segment-ui> (which is single-select inside <segmented-ui>).
- Active state controlled via the toggle-option's own active attribute (independent of siblings).

## ToggleScheme
- Place toggle-scheme-ui in the shell topbar's trailing action cluster — slot="action" inside <admin-topbar slot="header"> of <admin-content>. It is a persistent, app-wide preference control; never put it in a sidebar footer / <admin-statusbar>, which hosts user-account items only.
- Stores user override in localStorage and writes color-scheme inline-style to the target; falls back to prefers-color-scheme if no override is set.
- Singleton per page — placing more than one in DOM creates conflicting writes to the same target.

## ToolbarGroup
- Child of <toolbar-ui> — clusters related action buttons with token-driven internal gap.
- Separate clusters with <divider-ui> siblings inside <toolbar-ui>.
- Do not nest toolbar-groups; flat hierarchy only.

## Toolbar
- Horizontal action bar — hosts <button-ui>, <toolbar-group-ui>, and <divider-ui> children.
- Cluster related buttons inside <toolbar-group-ui> with <divider-ui> separating clusters.
- For navigation use <nav-ui>; for modal/popover action bars use the modal's footer slot. Toolbar is for inline action bars within content regions.

## Tooltip
- Use <tooltip-ui> to label icon-only <button-ui> elements (text="Save" on a save-icon button, etc.) and for short descriptive hover hints. Never use it for content the user must read carefully (the bubble is transient + non-focusable).
- Never place INTERACTIVE children inside <tooltip-ui> — it is a read-only hint, not a surface. If the user must click or type, use <popover-ui> instead. Tooltips never receive keyboard focus.
- [follows="pointer"] mode requires [for] pointing at a <chart-ui> or <heatmap-ui> [id]; the tooltip subscribes to that target's `chart-hover` / `chart-leave` events to render data-viz annotations that track the cursor. Without [for] the tooltip renders nothing in pointer mode.
- Set [delay=0] only for high-frequency exploratory surfaces (sparkline ticks, chart hover). Keep the default 400ms delay elsewhere to avoid hover noise.
- [indicator] (dot | line | dashed) is meaningful only in pointer mode for per-series swatches. Omit it or leave default "none" for text-mode tooltips.

## TourStep
- Single step inside <tour-ui>. Declares target (CSS selector) + title + body content.
- target is a CSS selector (e.g. "#dashboard", ".filters button"), NOT a bare ID. Use the # prefix.
- Renders nothing on its own — tour-ui orchestrates the popover stamping when the step becomes active.

## Tour
- Use tour-ui for spotlight-driven product tours / walkthroughs — sequence of steps each targeting a CSS-selector-resolved element.
- Children are <tour-step target="..." title="..."> with body content as the default slot. tour-ui reads them in DOM order.
- Set [auto-start] for first-run flows (paired with [storage-key] so it only shows once). Otherwise call .start() programmatically from a "Take the tour" button.
- Distinct from <onboarding-checklist-ui> (persistent setup todo-list, user-paced) and <tooltip-ui> (single hover hint, no orchestration).
- Listen to `tour-finish` for "user completed the tour" analytics; `tour-skip` for "user opted out". Both fire once per session.

## TreeItem
- <tree-item-ui> MUST be a direct or nested descendant of <tree-ui>. It is not a standalone primitive — outside a <tree-ui> parent, selection / expansion / keyboard nav don't wire.
- Provide a stable [value] attribute on every <tree-item-ui> that consumers will select. The `tree-select` event's detail.value is the identifier downstream code reads to know which node was picked.
- Use [open] to pre-expand branch nodes on initial render. Do NOT set [selected] declaratively on more than one item — the parent <tree-ui> manages selection. The host listens for click / Enter / Space / ArrowRight (expand) / ArrowLeft (collapse) per WAI-ARIA tree-view APG.
- Use [icon] (Phosphor name) for affordance (folder / file / component icons); use [badge] for counts or short status labels (§184 v0.5.5).
- Nest further <tree-item-ui> in the default slot only — no <list-item-ui>, <nav-item-ui>, or arbitrary content inside a tree row. The chevron is auto-stamped when the row has nested tree-item-ui children.

## Tree
- Use <tree-ui> only when data is hierarchical with arbitrary nesting AND a single selected node is meaningful. For flat lists use <list-ui>; for flat sidebar navigation use <nav-ui>; for one-level collapsible groups use <accordion-ui>.
- Canonical mount: inside <editor-sidebar slot="leading"> → <pane-ui side="leading" resizable> → <section> → <tree-ui id="…"> as the structure / navigator pane of the three-pane editor shell. Pair with a <header> in the same pane (e.g. "Structure", "Layers", "Files").
- Direct children of <tree-ui> MUST be <tree-item-ui>. No other element types in the default slot. <tree-ui> manages single-selection across the whole subtree and implements WAI-ARIA tree-view keyboard navigation (Arrow keys, Enter / Space, Home / End).
- Listen for `tree-select` on the <tree-ui>, NOT on individual rows — selection is managed by the parent and bubbles once. Detail = {item, text, value, ctrlKey, metaKey, shiftKey}.
- Per ADR-0027, <tree-ui> composes <icon-ui> (for chevrons) but does NOT auto-import its children. Consumer pages must explicitly import both <tree-ui> and <tree-item-ui>.

## Upload
- File-upload input with drop zone + browse button. Form-associated; emits file-list change events.
- Multiple attribute enables multi-file selection; accept= constrains file types.
- For agent chat attachments use <chat-composer-ui>'s built-in upload affordance instead.

## VisuallyHidden
- Wrap text that screen readers need but sighted users do not — icon-only-button labels, contextual disambiguation ("Edit profile" inside a row whose visible button just says "Edit").
- Distinct from [hidden] / display:none (hides from EVERYONE) and from aria-label (replaces visible text). Use <visually-hidden-ui> when both visible AND invisible text need to coexist on the same element.

## BillingOverview
- BillingOverview.account is REQUIRED. The composite is meaningless without an account snapshot; emitting BillingOverview with null account and no data-stream-src renders the empty state.
- account.status MUST be one of `active` | `trialing` | `past_due` | `canceled` | `paused`. Unknown values render neutral chrome.
- account.dunning is REQUIRED when account.status is `past_due` (otherwise the dunning banner cannot render). Shape: `{amount, currency, dueAt, cardLast4?, reason?}`. The composite stamps `<alert-ui pattern="dunning">` per SPEC-006.
- account.plans is REQUIRED when [variant] is `full` (otherwise the plan-picker section has nothing to render). Omit `plans` when [variant] is `compact` or `enterprise` to skip the plan-picker section.
- account.paymentMethods is forwarded verbatim to <payment-method-list-ui>; account.invoices is forwarded verbatim to <invoice-history-ui>. Their shapes are owned by SPEC-010 and SPEC-008 respectively.
- BillingOverview MUST NOT be nested inside Modal or Drawer. The dashboard is a route, not a modal — Modal traps focus + clips on long content; this surface is designed for full-page layout.
- Wire one listener to `account-action` instead of subscribing to every child primitive's event. The composite normalises plan-action / payment-action / invoice-action / dunning-action into the single bubbling event.

## InvoiceDetail
- InvoiceDetail MUST set `invoice` OR `data-stream-src`. Neither produces the empty state but cannot be the submission state for a billing surface.
- invoice.lines[] MUST contain at least one row with `description`, `qty`, `unitAmount`, `amount`. Empty lines arrays render the lines table empty-state row, not the host empty state.
- invoice.total MUST equal subtotal + tax − (discount or 0). The composite renders the host-supplied total verbatim; consumers are authoritative on rounding rules per locale (SPEC-007 OD-001).
- invoice.status MUST be one of draft / open / paid / past-due / void. Unknown statuses render the default badge variant + a one-shot console.warn.
- InvoiceDetail MUST NOT be nested inside Modal or Drawer. Invoices are routes, not modals — Modal traps focus + clips on long content; the composite is designed for full-page layout. Use `<a href="/invoices/{number}">` instead.
- Slot ALL header actions explicitly via `slot="header-actions"` — the composite NEVER stamps a default toolbar (SPEC-007 OD-002). Action sets vary across products; default is too opinionated.

## InvoiceHistory
- InvoiceHistory.invoices MUST be a non-empty array on commit. Empty arrays render the empty-state (no rows).
- invoices[].status MUST be one of "draft" | "open" | "paid" | "past-due" | "void". Unknown values fall back to a neutral badge variant.
- InvoiceHistory MUST NOT receive a `columns` prop — columns are owned by the composite. Consumers needing custom columns should compose <table-ui> directly with their own column array.
- `hrefPattern` is a string template with `{number}` interpolation. Consumers needing dynamic resolution (query-strings, function- based routes) should preventDefault on `invoice-row-click` and route themselves.
- Pair with `InvoiceDetail` (SPEC-007) at the route resolved by `hrefPattern` so the row-click destination exists.

## PaymentMethodForm
- PaymentMethodForm SHOULD be wrapped in a <Form> so its form-value (the resolved token) is delivered on submission. Standalone use requires the consumer wire `tokenize()` from a custom submit handler.
- PaymentMethodForm MUST NOT receive a `value` prop carrying raw card digits. The form value is the tokenized output; raw card digits live only inside the form's sub-fields and are never mirrored to `value`.
- PaymentMethodForm SHOULD only be used in test / demo / non-PCI surfaces. Production card-capture flows MUST tokenize via a real payment processor (Stripe Elements, Braintree, Adyen) and consume only the resulting token — this primitive is the form chrome + structured validation, not a tokenization provider.
- Wrapping PaymentMethodForm inside <Field> duplicates the label and corrupts the grid layout. PaymentMethodForm IS a field group; the host carries `aria-label` directly.
- `countries` SHOULD list every ISO 3166-1 alpha-2 code your product accepts. Empty arrays fall back to a default short list (US, CA, GB, DE, FR, AU) suitable for early-stage demos only — production deployments should set this explicitly.

## PaymentMethodList
- PaymentMethodList.methods MUST be an array. Empty array is valid and renders the empty-state.
- methods[].id MUST be unique within the list. Duplicate ids cause row collision at diff time.
- Exactly one record in `methods` MAY have `default: true`. If none does, the primitive marks the first row as default on connect.
- methods[].brand SHOULD be from the enumerated set (visa | mastercard | amex | discover | jcb | unionpay | diners | paypal | apple-pay | google-pay | ach | sepa). Unknown brands fall back to a generic credit-card mark.
- methods[].type MUST be `card`, `bank`, or `wallet`. Drives the brand-icon fallback when `brand` is unknown.
- `value` wins on conflict with methods[].default — if both are set and disagree, value is the source of truth and the primitive emits `change` to reconcile.
- Mutations are events, not props. Wire `change` / `add` / `remove` / `select` to your billing API; the primitive does NOT write to the underlying data source.

## PlanPicker
- PlanPicker.plans MUST be a non-empty array on commit. Empty arrays render the empty-state but cannot be the submission state.
- At most ONE plans[] record may carry `recommended: true`. Two recommended plans are visually ambiguous and defeat the treatment's purpose.
- `current` MUST match a plans[].id or be empty. Unknown ids fail silently — no anchor row renders, no current-state contextualization on siblings.
- plans[].prices.monthly is REQUIRED on every record. annual is optional; if absent and cycle="annual" the card renders the monthly price with the annual cycle suffix and no `note`.
- `cycle` MUST be `monthly` or `annual`. Other values are coerced to `monthly` and a one-shot console.warn fires.
- Use `layout="list"` for in-app settings panels where the picker sits inside a <card-ui> and width is constrained. Use the default `layout="grid"` for marketing pricing pages.
- Wire `select` (not `change`) to the actual commit handler — change fires on selection without commit (keyboard focus traversal, programmatic value-set); select fires when the user clicks the CTA inside a card.

## ChatComposer
- chat-composer is the bespoke replacement for legacy <chat-input-ui data-chat-input> inside <chat-shell>. Place an inner <chat-input-ui> as the primary input — it emits `submit` on Enter without Shift (unconditional; delegated from the inner textarea).
- The host listens for 'composer-submit' on the composer (not on the inner input). The event detail mirrors the inner submit event so existing handlers Just Work.
- Default slot holds a single <chat-input-ui> child; trailing/attach/leading slots host action buttons (send, attach, model picker).
- For non-chat input surfaces (forms, prompts, search) use <chat-input-ui> directly without the composer wrapper.

## ChatEmpty
- chat-empty is the bespoke replacement for legacy <empty-state-ui data-chat-empty>. Place as the first child of <chat-thread>; visibility is automatic via the [empty] reflected attribute.
- CSS-only — no JS module needed in shell HTML imports.
- For non-chat empty states (lists, tables, canvas) use <empty-state-ui> primitive instead; chat-empty is chat-cluster-namespaced.

## ChatHeader
- chat-header replaces the legacy <header> chrome bar inside <chat-shell>. Use named slots for canonical clusters; ad-hoc content goes in the default slot.
- Slots: [slot="name"] for chat title, [slot="status"] for streaming/connection indicator (<chat-status>), [slot="action"] for action buttons.
- For admin-shell-style chrome bars inside chat-shell, use <admin-topbar> instead — chat-header is for in-chat metadata only.

## ChatShell
- chat-shell takes bespoke chat-* children only. The canonical composition is <chat-thread> (with optional first-child <chat-empty>) followed by <chat-composer> wrapping a <chat-input-ui>. Add <chat-header> / <chat-sidebar> / <chat-status> as needed.
- Don't nest col-ui / row-ui or generic layout primitives directly inside chat-shell — the shell's CSS reads child tag selectors to lay them out. Generic layout goes inside the bespoke children.
- The shell listens for 'composer-submit' on <chat-composer> (not on the inner input). Streaming state is reflected on this host and propagates to <chat-thread>[streaming] + <chat-composer>[disabled] automatically — don't toggle child attributes manually.
- Legacy data-attribute shapes were retired in v0.4.0 per ADR-0024. Do not author <section data-chat-messages>, <chat-input-ui data-chat-input>, <empty-state-ui data-chat-empty>, or <header data-chat-name> inside chat-shell.

## ChatSidebar
- chat-sidebar is the bespoke replacement for legacy <aside data-sidebar>. Use slot="leading" or slot="trailing" to position. Add resizable + collapsible attributes to opt in to interactive behaviors.
- For chrome bars inside the sidebar, prefer <admin-topbar slot="header"> and <admin-statusbar slot="footer"> over raw <header-ui> / <footer-ui> when authoring shell-tier markup.
- Use cluster-distinct localStorage key (adia-chat-sidebar-*) to avoid collisions with admin (adia-sidebar-*) and editor (adia-editor-sidebar-*) sidebars.
- For chat-list / conversation-switcher content, host <nav-ui> + <nav-item-ui> children in the default slot.

## ChatStatus
- chat-status replaces legacy <span data-chat-status> for the streaming/connection indicator. Place inside <chat-header slot="status">.
- CSS-only — content is innerHTML (typically a short status word like 'connected' or 'streaming' or an icon).
- Use [data-state="streaming|connected|disconnected|..."] for token-driven coloring; the CSS reads the state attribute for tinting.

## ChatThread
- chat-thread is the bespoke replacement for legacy <section data-chat-messages> inside <chat-shell>. Use it for the message scroll surface; the host appends dynamic message divs as children.
- Place <chat-empty> as an optional first child for the empty state; the [empty] reflected attribute drives its visibility via CSS (no JS toggling).
- Different from primitive <chat-thread-ui>: chat-thread (no -ui suffix) is the module-tier shell-aware version with scroll-on-new-message + load-more + empty-state coordination.
- Hosts message blocks (typically agent/user message rows) as default-slot children; <chat-empty> goes as first child for the empty state.

## DashboardLayout
- DashboardLayout MUST receive children via the four named slots
(toolbar / kpis / charts / table) plus the optional aside.
Children without a [slot] attribute will not render in any band.

- DashboardLayout[kpi-columns] MUST be 2-6. Out-of-range values
fall back to the default 4. Eight cards in a row is too narrow
to be legible at typical container widths; container queries
collapse the band to 2 columns ≤48em and 1 column ≤32em.

- DashboardLayout[chart-split] MUST be one of the documented
enum strings — "" (full-width) / 2 / 2:1 / 3:2 / 3:1.

- A DashboardLayout SHOULD sit inside AdminPageBody for the
canonical chrome stack. Top-level placement is allowed for
surface-only demos but not the production shape.

- A DashboardLayout MUST NOT contain another DashboardLayout as
a descendant. Two parametric-density containers fight over the
--a-density cascade. For nested dashboards use sibling
DashboardLayouts switched by Tabs.


## DateRangeSelector
- DateRangeSelector MUST sit inside a toolbar context — typically a
Row inside DashboardLayout.slot=toolbar, or directly inside
AdminPageHeader.slot=action. Do NOT place it as a freestanding
block primitive at page top-level.

- ONLY ONE DateRangeSelector with broadcast="document" per page.
Secondary instances on the same page MUST use broadcast="self"
or broadcast="none". Two document-broadcasters race for the
data-range-* attributes on <html> and produce flicker.

- `presets` MUST be a comma-separated subset of the documented value
enum (today, 7d, 30d, 90d, qtd, ytd, custom). Unknown keys are
ignored at runtime; the chip-row renders only recognized keys.

- When value="custom", from/to MUST be ISO-8601 dates supplied
either declaratively (from + to attributes) or imperatively via
setRange("custom", from, to). Without a custom from/to, the
composite falls back to the prior resolved range.

- For form participation, set [name]. Without [name], the composite
emits range-change events only and does NOT participate in form
submission. With [name], FormData carries the value "{from}:{to}".


## EditorCanvasEmpty
- editor-canvas-empty is the bespoke empty-state slot for <editor-canvas>. Place as the first child of <editor-canvas>; visibility is automatic via the [empty] reflected attribute.
- CSS-only — no JS module needed in shell HTML imports.
- For non-editor canvas empty states (raw <canvas-ui>) use <empty-state-ui> instead; editor-canvas-empty is editor-cluster-namespaced.

## EditorCanvasToolbar
- editor-canvas-toolbar is the bespoke chrome strip for the <editor-canvas> top edge. Use it to hold view-mode tabs (preview / schema / DOM) or breadcrumb-style navigation scoped to the canvas region.
- Place as the first child of <editor-canvas>; the canvas's flex-column layout positions the toolbar above the content body automatically, and the toolbar is sticky to keep tabs visible during canvas scroll.
- Distinct from <editor-toolbar> (app-scope, top of <editor-shell>) — this is canvas-scope, holding view-mode affordances rather than document-wide actions.

## EditorCanvas
- editor-canvas is the bespoke replacement for legacy <div data-canvas> inside <editor-shell>. Use it as the central content region for editor work surfaces.
- Place <editor-canvas-empty> as an optional first child for the empty state; the [empty] reflected attribute drives its visibility via CSS (no JS toggling).
- Place <editor-canvas-toolbar> as the first child to mount view-mode tabs (preview / schema / DOM, etc.), breadcrumbs, or canvas-scoped actions above the content body. Replaces the ad-hoc <div data-view-strip> pattern. Distinct from <editor-toolbar> (app-scope) — this is canvas-scope.

## EditorShell
- editor-shell takes bespoke editor-* children only. The canonical composition is <editor-toolbar> + <editor-sidebar slot="leading"> + <editor-canvas> + <editor-sidebar slot="trailing"> + <editor-statusbar>. Each child is optional except the canvas.
- Don't nest col-ui / row-ui or generic layout primitives directly inside editor-shell — the shell's CSS reads child tag selectors to lay them out. Generic layout goes inside the bespoke children.
- <editor-sidebar> WRAPS <pane-ui resizable> rather than implementing drag itself (delegation pattern). Inside each sidebar place a single <pane-ui resizable size="sm"> filled with <header> + <section> slots.
- For editor-inside-admin nested-shell pages, place the editor-shell inside <admin-page-body> with `flex: 1; min-height: 0` on every ancestor in the flex chain. Without min-height: 0 the inner shell collapses to zero height.
- Legacy data-attribute shapes were retired in v0.4.0 per ADR-0024. Do not author <div data-editor-body>, <pane-ui data-left|data-right>, <div data-canvas>, <span data-spacer>, or bare <header>/<footer> chrome inside editor-shell.

## EditorSidebar
- editor-sidebar wraps <pane-ui resizable> rather than implementing drag itself. Place a <pane-ui resizable> as the only structural child; fill the pane with header / section / footer slots.
- The cluster-distinct localStorage prefix (adia-editor-sidebar-*) keeps editor sidebars from colliding with admin (adia-sidebar-*) and chat (adia-chat-sidebar-*) sidebars on the same domain.
- Wraps a SINGLE <pane-ui resizable> as its only structural child; fill the pane with sub-views (nav, inspector, layers panel, etc.).
- For multiple panes (e.g. left nav + right inspector), use two <editor-sidebar> instances at slot="leading" and slot="trailing" of <editor-shell>.

## EditorStatusbar
- editor-statusbar replaces legacy <footer> chrome bar inside <editor-shell>. Use named slots for canonical clusters; ad-hoc content goes in the default slot.
- Slots: [slot="status"] for save/sync state, [slot="cursor"] for cursor position, [slot="zoom"] for zoom level, [slot="action"] for actions. All optional.
- Different from <admin-statusbar> (which is for admin-shell); editor-statusbar has editor-specific slot vocabulary (cursor, zoom).

## EditorToolbar
- editor-toolbar replaces legacy <header> chrome bar inside <editor-shell>. Use named slots (title / status / action / action-leading) for canonical clusters; ad-hoc inline content goes in the default slot.
- Buttons that should trigger named actions get [data-toolbar-action="<name>"]. The toolbar bubbles a single 'toolbar-action' event up to the host with the name in detail.
- Different from <admin-topbar> (admin-shell chrome) — editor-toolbar has [full-screen] state + editor-specific [data-toolbar-action] event bubbling.
- Place full-screen toggle buttons inside the toolbar with [data-toolbar-action="toggle-full-screen"]; host reflects [full-screen] up to <editor-shell>.

## ConfirmDialog
- ConfirmDialog is for NON-destructive yes/no questions ("Save changes?", "Switch theme?", "Apply preset?"). For irreversible destructive operations (delete, drop, deploy to prod) use AlertDialog instead — its danger chrome + role="alertdialog" + typed-name speed-bump are the WAI-APG pattern for that case.
- Default focus lands on Cancel by default — this is intentional (WAI guidance: the least-destructive action is the default). Do NOT add autofocus to the confirm button or override the focus model.
- The confirm button is primary-toned by default. Use [confirm-variant="ghost"] only when both options are roughly equivalent in weight (e.g. "Apply preset" vs "Keep current" where neither is strictly preferred).
- Do NOT model the cancel option as a "Discard" action — if cancel means "lose data", the operation is destructive and belongs in AlertDialog. ConfirmDialog's cancel is a no-op return-to-prior- state semantically.
- Reflect dialog visibility via the [open] boolean attribute on the ConfirmDialog host (open=true / open=false). Do NOT toggle [hidden], CSS display, or wrap in a sibling visibility container — the inner <modal-ui> owns the native <dialog> lifecycle.

## OnboardingChecklist
- OnboardingChecklist MUST have a non-empty `items` array. Empty items renders an empty card.
- Each item MUST have a unique `id` within the list — used as the storage key + event detail.
- `storageKey` SHOULD be namespaced per product (e.g. `myapp:onboarding:v1`) to avoid cross-app collisions.
- Items SHOULD number 3 to 8. Fewer than 3 is not worth a checklist; more than 8 should split into stages.
- Use for the "card of optional setup tasks" case. For horizontal multi-screen wizards use <step-progress-ui>; for generic to-do lists compose <list-ui> + <check-ui> directly.
- Do not nest two <onboarding-checklist-ui> with the same `storageKey` — they will clobber each other.

## A2UIRoot
- Mount point for an A2UI-rendered composition. Hosts the runtime-emitted DOM tree.
- Different from <gen-root> (which is for the generative-UI lane with LLM-driven streaming).
- Do not place static children inside; runtime owns the contents.

## GenRoot
- gen-root is an integration shell. Prefer admin-shell for admin UIs; use gen-root only for chat+canvas tooling.
- Hosts <chat-thread-ui>, <canvas-ui>, and <inspector-ui> children via named slots — chat slot for the conversation lane, canvas slot for the artifact lane, inspector slot for the dev-tools lane.
- Mode attribute (chat-only|split|canvas-only) controls layout; transitions are CSS-animated.

## IntegrationsPage
- Use <integrations-page-ui> for the canonical Settings >
Integrations grid. Supply the `integrations` array — the
composite owns the search + grouping + grid breakpoints +
empty state.

- `integrations` MUST be a non-empty array OR the page renders
the empty-state. Each item MUST satisfy the SPEC-062 card
prop contract (provider + name required).

- Do NOT slot <integration-card-ui> children directly — the page
generates cards from the `integrations` prop. Slotted children
are ignored.

- Do NOT nest <integrations-page-ui> inside another
<integrations-page-ui>.

- For one-off integration tile groups (e.g., a single recommended
provider on a marketing page) use <integration-card-ui> directly
inside <grid-ui>; reach for the page composite only when search
+ grouping + empty-state semantics are needed.


## NotificationPreferences
- NotificationPreferences.channels MUST be non-empty. A matrix with zero columns renders nothing meaningful — author at least one channel record.
- NotificationPreferences.preferences[].channels keys MUST match the channels[].key set. Mismatched keys silently render the cell as `false` (off) — they do NOT raise an error, but the user can never toggle them into existence either.
- The composite is fully CONTROLLED — toggling a cell does NOT mutate `preferences`. The host MUST listen for `change` (and `bulk-toggle` for the column master), apply the change to its own state, then re-pass the new `preferences` array. Failing to do this leaves the toggle visually flipping back to its prior state on the next render.
- For binary on/off cell toggles, use <check-ui> semantics — NOT <switch-ui>. The column-header master IS a <switch-ui> (because it's a "setting" — flip the whole column), but each cell is a multi-select-style checkbox.
- Use `group-by="group"` only when `preferences[]` records carry a stable `group` field. Mixing grouped + ungrouped rows in the same matrix produces inconsistent section heights.

## AdminCommand
- admin-command wraps a native <dialog>; the inner <command-ui> is the actual palette. Keyboard shortcut defaults to both Cmd+K (mac) and Ctrl+K (other) — the AdiaUI convention.
- Place admin-command as a direct child of admin-shell, NOT inside a sidebar or main column. The host coordinates triggers ([data-command-trigger]) by reaching across siblings.
- For inline content-region command surfaces use <command-ui> directly; admin-command is the modal wrapper that opens the palette as an overlay.
- Triggers anywhere in the shell via [data-command-trigger] attribute on any element; the host wires the open/close cycle.

## AdminContent
- admin-content is the bespoke replacement for raw <main> inside admin-shell. CSS-only; the shell's css/main.css selectors target both shapes via :is(main, admin-content).
- Place as a direct child of <admin-shell>, not inside a sidebar or topbar; admin-content owns the center column.
- Hosts <admin-page> children OR raw page content via the default slot; for chrome bars use <admin-topbar slot="header">.

## AdminEntityItem
- admin-entity-item is the canonical icon + label + badge identity row for shell surfaces. Slot it whole into [slot="heading"] of an <admin-topbar> / <admin-statusbar> so the icon + label + badge collapse together inside a collapsible <admin-sidebar>. Do NOT re-implement it with a bare <span> or an ad-hoc flex <div> — those have no shared collapse boundary.
- For an INTERACTIVE workspace switcher use <select-ui variant="ghost"> instead — admin-entity-item is read-only identity display.
- Slot whole into [slot="heading"] of <admin-topbar>/<admin-statusbar>; do not nest inside other admin-entity-items.
- Flat hierarchy: icon + label + badge in three slots, no extra wrapping at the admin-entity-item level. For full-width metadata strips compose multiple inside <row-ui> instead.
- For an <icon-ui> glyph, wrap it: <span slot="icon"><icon-ui name="…"></icon-ui></span>. For <img>/<avatar-ui>, slot directly (no wrapper).

## AdminPageBody
- admin-page-body is the centered body band of <admin-page>. Wraps an inner <section-ui> for centered reading-column rhythm; can host full-bleed content directly to opt out.
- Wraps an inner <section-ui> by default for centered reading-column rhythm; for full-bleed content (tables, canvas grids) use [data-full-bleed] on the section.
- Always inside <admin-page slot="body">; do not place admin-page-body outside admin-page.

## AdminPageHeader
- admin-page-header is the sticky top band of <admin-page>. Wraps an inner <header-ui> for centered reading-column rhythm.
- Wraps an inner <header-ui> for centered reading-column rhythm; admin-page-header owns sticky positioning + border, header-ui owns the content layout.
- Always inside <admin-page slot="header">; for shell-tier chrome use <admin-topbar> instead.
- Always wrap an inner <header-ui> — not a raw <header>. Raw <header> has no shadow DOM; slot="action" and other named slots on its children are silently dropped.

## AdminPage
- admin-page is the bespoke replacement for <article data-content-root>. Provides the page-content named container query so descendants can use @container page-content (max-width: 720px) etc.
- Hosts a single page surface via [slot="body"] (typically <admin-page-body>) with optional [slot="header"] (<admin-page-header>) and [slot="footer"].
- Sits inside <admin-content>'s default slot; do not place outside admin-shell.

## AdminScroll
- admin-scroll is the bespoke replacement for the legacy <section> child of <main> inside admin-shell. Single child convention — typically wraps an <admin-page> for sticky-band layout.
- Wraps a single <admin-page> child typically; do not place multiple admin-pages side-by-side as siblings here.
- For horizontal scrolling regions (data grids, canvas surfaces) use overflow on the inner content instead — admin-scroll is vertical-only.

## AppShell
- admin-shell takes bespoke admin-* children only. The canonical composition is <admin-topbar> + <admin-sidebar slot="leading"> + <admin-content> + <admin-sidebar slot="trailing"> + <admin-command> + optional <admin-statusbar>. The shell's CSS grid reads child tag selectors to place them.
- Don't nest col-ui / row-ui or generic layout primitives directly inside admin-shell — app-shell.css handles grid layout based on bespoke child tags. Generic layout goes inside <admin-content> or inside <admin-page-body>.
- Click forwarding patterns — [data-sidebar-toggle="<name>"] on a button forwards to <admin-sidebar[slot="<name>"]>.toggle(); [data-command-trigger] on a button forwards to <admin-command>.show(). The shell doesn't need to know about the buttons; the bespoke children own the behavior.
- Legacy data-attribute shapes were retired in v0.4.0 per ADR-0024. Do not author <aside data-sidebar>, <dialog data-command>, [data-resize], <aside-ui slot=>, <span data-spacer>, or <div data-actions> inside admin-shell.

## AdminSidebar
- admin-sidebar is the bespoke replacement for legacy <aside data-sidebar>. Use slot="leading" or slot="trailing" to position. Add resizable + collapsible attributes to opt in to interactive behaviors.
- For chrome bars inside the sidebar, prefer <admin-topbar slot="header"> and <admin-statusbar slot="footer"> over raw <header-ui> / <footer-ui> when authoring shell-tier markup.
- [resizable] attribute enables drag-resize via internal <pane-ui resizable>; persists collapsed/width to localStorage under adia-sidebar-* keys.
- For non-resizable static rails (fixed-width nav), omit [resizable]; the same primitive serves both modes.

## AdminStatusbar
- admin-statusbar replaces <footer-ui> at shell-tier. Same slot vocabulary as <admin-topbar>; visual treatment differs (the shell css applies a top-border instead of bottom).
- For a user-identity row (avatar + name + role badge), slot a single <admin-entity-item slot="heading"> rather than separate slot="icon" + slot="heading" children — the wrapper collapses the name + badge together inside a collapsible <admin-sidebar>, keeping the avatar.
- All slots optional; common pattern is [slot="heading"] + [slot="action"] only. Empty admin-statusbar renders zero-height (no visual chrome).
- Sits at slot="footer" of <admin-shell>, <admin-content>, or <admin-sidebar>; do not place inside <admin-page slot="footer"> (use raw <footer-ui> there).

## AdminTopbar
- admin-topbar replaces <header-ui> at shell-tier — use it for chrome bars inside admin-shell, admin-content, admin-sidebar. Use <header-ui> for primitive containers (Card / Drawer / Modal).
- For an icon + label (+ optional badge) identity row — workspace or product identity — slot a single <admin-entity-item slot="heading"> rather than separate slot="icon" + slot="heading" children. The wrapper keeps the icon and label as one unit so they truncate and collapse together inside a collapsible <admin-sidebar>.
- All slots optional; common pattern is [slot="heading"] + [slot="action"] only. Empty admin-topbar renders zero-height (no visual chrome).
- Sits at slot="header" of <admin-shell>, <admin-content>, or <admin-sidebar>; do not place inside <admin-page slot="header"> (use <admin-page-header> there).

## SimpleContent
- simple-content is the article surface inside simple-shell. Use for primary page body. Sibling to <simple-hero> (optional).
- CSS-only — do not import its JS module (there isn't one); does not need an `import` in shell HTML.
- For multi-page apps with chrome (nav, topbar, command palette) use <admin-shell-ui> with <admin-content> instead.

## SimpleHero
- simple-hero is the optional top strip inside simple-shell. Use for marketing splashes, error-page reassurance text, or single- flow page intros. Always followed by <simple-content> for body.
- Three named slots: heading (large title), lede (supporting subtitle), actions (button cluster). All optional but order is fixed.
- CSS-only — sibling to <simple-content> inside <simple-shell>. Do not nest hero inside content or vice versa.

## SimpleShell
- simple-shell is the bespoke shell for thin / minimal page surfaces. Use when the page has no nav rail, no chrome bars, no command palette. For full app surfaces use admin-shell; for chat surfaces use chat-shell; for design tools use editor-shell.
- Compose with <simple-hero> (optional top hero strip) and <simple-content> (main article body). Both are CSS-only structural children — no JS, no state.
- [centered] reflected attribute centers content vertically; [full-bleed] drops the max-width constraint on <simple-content> children. Both optional, default unset.
- For multi-page apps with chrome (nav, topbar, command palette) use <admin-shell> instead; simple-shell has zero chrome.

## ThemePanel
- theme-panel is the canonical appearance-preferences popover. Compose inside a <popover-ui slot="content"> in the topbar of a shell (admin, chat, editor, simple) when the page exposes user theming. Avoid hardcoding it as a shell child — placement is the consumer's call.
- Use [persist] for the docs surface or a real product app; omit [persist] for playgrounds and embedded demos so state stays ephemeral. The default is ephemeral.
- Add boolean attributes [parametric], [presets], [scheme-toggle] to enable sections; the minimum panel is the theme button grid.

