From 5b6080b80ef5f395d5a76f57d1284bc32f97738e Mon Sep 17 00:00:00 2001 From: grim <75869731+ripgrim@users.noreply.github.com> Date: Mon, 3 Aug 2026 22:56:27 -0700 Subject: [PATCH 1/7] CMP-1 chore: add agent interface skills --- .agents/skills/ai-sdk/SKILL.md | 78 ++++ .agents/skills/better-accessibility/SKILL.md | 148 +++++++ .../better-accessibility/agents/openai.yaml | 3 + .../focus-and-keyboard.md | 131 ++++++ .agents/skills/better-accessibility/forms.md | 86 ++++ .../skills/better-accessibility/hit-areas.md | 76 ++++ .../better-accessibility/motion-and-zoom.md | 79 ++++ .../better-accessibility/screen-readers.md | 101 +++++ .../semantics-and-aria.md | 83 ++++ .agents/skills/better-colors/SKILL.md | 113 +++++ .../better-colors/accessibility-contrast.md | 87 ++++ .../skills/better-colors/agents/openai.yaml | 3 + .../skills/better-colors/color-conversion.md | 53 +++ .agents/skills/better-colors/color-usage.md | 94 ++++ .../better-colors/gamut-and-tailwind.md | 103 +++++ .../better-colors/palette-generation.md | 93 ++++ .agents/skills/better-interface/SKILL.md | 138 ++++++ .../better-interface/agents/openai.yaml | 5 + .agents/skills/better-layout/SKILL.md | 114 +++++ .../skills/better-layout/agents/openai.yaml | 3 + .../better-layout/grouping-and-alignment.md | 123 ++++++ .../better-layout/spacing-and-adaptivity.md | 159 +++++++ .agents/skills/better-typography/SKILL.md | 171 ++++++++ .../better-typography/agents/openai.yaml | 3 + .../better-typography/choosing-fonts.md | 64 +++ .../better-typography/css-cheat-sheet.md | 65 +++ .../details-and-accessibility.md | 109 +++++ .../better-typography/spacing-and-sizing.md | 126 ++++++ .../variable-fonts-and-opentype.md | 105 +++++ .../wrapping-and-punctuation.md | 58 +++ .agents/skills/better-ui/SKILL.md | 141 ++++++ .agents/skills/better-ui/agents/openai.yaml | 3 + .agents/skills/better-ui/animations.md | 404 ++++++++++++++++++ .agents/skills/better-ui/icons.md | 110 +++++ .agents/skills/better-ui/performance.md | 88 ++++ .agents/skills/better-ui/surfaces.md | 219 ++++++++++ .agents/skills/better-writing/SKILL.md | 145 +++++++ .../skills/better-writing/agents/openai.yaml | 3 + .claude/skills/ai-sdk/SKILL.md | 78 ++++ .claude/skills/better-accessibility/SKILL.md | 148 +++++++ .../better-accessibility/agents/openai.yaml | 3 + .../focus-and-keyboard.md | 131 ++++++ .claude/skills/better-accessibility/forms.md | 86 ++++ .../skills/better-accessibility/hit-areas.md | 76 ++++ .../better-accessibility/motion-and-zoom.md | 79 ++++ .../better-accessibility/screen-readers.md | 101 +++++ .../semantics-and-aria.md | 83 ++++ .claude/skills/better-colors/SKILL.md | 113 +++++ .../better-colors/accessibility-contrast.md | 87 ++++ .../skills/better-colors/agents/openai.yaml | 3 + .../skills/better-colors/color-conversion.md | 53 +++ .claude/skills/better-colors/color-usage.md | 94 ++++ .../better-colors/gamut-and-tailwind.md | 103 +++++ .../better-colors/palette-generation.md | 93 ++++ .claude/skills/better-interface/SKILL.md | 138 ++++++ .../better-interface/agents/openai.yaml | 5 + .claude/skills/better-layout/SKILL.md | 114 +++++ .../skills/better-layout/agents/openai.yaml | 3 + .../better-layout/grouping-and-alignment.md | 123 ++++++ .../better-layout/spacing-and-adaptivity.md | 159 +++++++ .claude/skills/better-typography/SKILL.md | 171 ++++++++ .../better-typography/agents/openai.yaml | 3 + .../better-typography/choosing-fonts.md | 64 +++ .../better-typography/css-cheat-sheet.md | 65 +++ .../details-and-accessibility.md | 109 +++++ .../better-typography/spacing-and-sizing.md | 126 ++++++ .../variable-fonts-and-opentype.md | 105 +++++ .../wrapping-and-punctuation.md | 58 +++ .claude/skills/better-ui/SKILL.md | 141 ++++++ .claude/skills/better-ui/agents/openai.yaml | 3 + .claude/skills/better-ui/animations.md | 404 ++++++++++++++++++ .claude/skills/better-ui/icons.md | 110 +++++ .claude/skills/better-ui/performance.md | 88 ++++ .claude/skills/better-ui/surfaces.md | 219 ++++++++++ .claude/skills/better-writing/SKILL.md | 145 +++++++ .../skills/better-writing/agents/openai.yaml | 3 + AGENTS.md | 52 +++ CLAUDE.md | 52 +++ skills-lock.json | 48 +++ 79 files changed, 7526 insertions(+) create mode 100644 .agents/skills/ai-sdk/SKILL.md create mode 100644 .agents/skills/better-accessibility/SKILL.md create mode 100644 .agents/skills/better-accessibility/agents/openai.yaml create mode 100644 .agents/skills/better-accessibility/focus-and-keyboard.md create mode 100644 .agents/skills/better-accessibility/forms.md create mode 100644 .agents/skills/better-accessibility/hit-areas.md create mode 100644 .agents/skills/better-accessibility/motion-and-zoom.md create mode 100644 .agents/skills/better-accessibility/screen-readers.md create mode 100644 .agents/skills/better-accessibility/semantics-and-aria.md create mode 100644 .agents/skills/better-colors/SKILL.md create mode 100644 .agents/skills/better-colors/accessibility-contrast.md create mode 100644 .agents/skills/better-colors/agents/openai.yaml create mode 100644 .agents/skills/better-colors/color-conversion.md create mode 100644 .agents/skills/better-colors/color-usage.md create mode 100644 .agents/skills/better-colors/gamut-and-tailwind.md create mode 100644 .agents/skills/better-colors/palette-generation.md create mode 100644 .agents/skills/better-interface/SKILL.md create mode 100644 .agents/skills/better-interface/agents/openai.yaml create mode 100644 .agents/skills/better-layout/SKILL.md create mode 100644 .agents/skills/better-layout/agents/openai.yaml create mode 100644 .agents/skills/better-layout/grouping-and-alignment.md create mode 100644 .agents/skills/better-layout/spacing-and-adaptivity.md create mode 100644 .agents/skills/better-typography/SKILL.md create mode 100644 .agents/skills/better-typography/agents/openai.yaml create mode 100644 .agents/skills/better-typography/choosing-fonts.md create mode 100644 .agents/skills/better-typography/css-cheat-sheet.md create mode 100644 .agents/skills/better-typography/details-and-accessibility.md create mode 100644 .agents/skills/better-typography/spacing-and-sizing.md create mode 100644 .agents/skills/better-typography/variable-fonts-and-opentype.md create mode 100644 .agents/skills/better-typography/wrapping-and-punctuation.md create mode 100644 .agents/skills/better-ui/SKILL.md create mode 100644 .agents/skills/better-ui/agents/openai.yaml create mode 100644 .agents/skills/better-ui/animations.md create mode 100644 .agents/skills/better-ui/icons.md create mode 100644 .agents/skills/better-ui/performance.md create mode 100644 .agents/skills/better-ui/surfaces.md create mode 100644 .agents/skills/better-writing/SKILL.md create mode 100644 .agents/skills/better-writing/agents/openai.yaml create mode 100644 .claude/skills/ai-sdk/SKILL.md create mode 100644 .claude/skills/better-accessibility/SKILL.md create mode 100644 .claude/skills/better-accessibility/agents/openai.yaml create mode 100644 .claude/skills/better-accessibility/focus-and-keyboard.md create mode 100644 .claude/skills/better-accessibility/forms.md create mode 100644 .claude/skills/better-accessibility/hit-areas.md create mode 100644 .claude/skills/better-accessibility/motion-and-zoom.md create mode 100644 .claude/skills/better-accessibility/screen-readers.md create mode 100644 .claude/skills/better-accessibility/semantics-and-aria.md create mode 100644 .claude/skills/better-colors/SKILL.md create mode 100644 .claude/skills/better-colors/accessibility-contrast.md create mode 100644 .claude/skills/better-colors/agents/openai.yaml create mode 100644 .claude/skills/better-colors/color-conversion.md create mode 100644 .claude/skills/better-colors/color-usage.md create mode 100644 .claude/skills/better-colors/gamut-and-tailwind.md create mode 100644 .claude/skills/better-colors/palette-generation.md create mode 100644 .claude/skills/better-interface/SKILL.md create mode 100644 .claude/skills/better-interface/agents/openai.yaml create mode 100644 .claude/skills/better-layout/SKILL.md create mode 100644 .claude/skills/better-layout/agents/openai.yaml create mode 100644 .claude/skills/better-layout/grouping-and-alignment.md create mode 100644 .claude/skills/better-layout/spacing-and-adaptivity.md create mode 100644 .claude/skills/better-typography/SKILL.md create mode 100644 .claude/skills/better-typography/agents/openai.yaml create mode 100644 .claude/skills/better-typography/choosing-fonts.md create mode 100644 .claude/skills/better-typography/css-cheat-sheet.md create mode 100644 .claude/skills/better-typography/details-and-accessibility.md create mode 100644 .claude/skills/better-typography/spacing-and-sizing.md create mode 100644 .claude/skills/better-typography/variable-fonts-and-opentype.md create mode 100644 .claude/skills/better-typography/wrapping-and-punctuation.md create mode 100644 .claude/skills/better-ui/SKILL.md create mode 100644 .claude/skills/better-ui/agents/openai.yaml create mode 100644 .claude/skills/better-ui/animations.md create mode 100644 .claude/skills/better-ui/icons.md create mode 100644 .claude/skills/better-ui/performance.md create mode 100644 .claude/skills/better-ui/surfaces.md create mode 100644 .claude/skills/better-writing/SKILL.md create mode 100644 .claude/skills/better-writing/agents/openai.yaml diff --git a/.agents/skills/ai-sdk/SKILL.md b/.agents/skills/ai-sdk/SKILL.md new file mode 100644 index 00000000..19eb3ee3 --- /dev/null +++ b/.agents/skills/ai-sdk/SKILL.md @@ -0,0 +1,78 @@ +--- +name: ai-sdk +description: 'Answer questions about the AI SDK and help build AI-powered features. Use when developers: (1) Ask about AI SDK functions like generateText, streamText, ToolLoopAgent, embed, or tools, (2) Want to build AI agents, chatbots, RAG systems, or text generation features, (3) Have questions about AI providers (OpenAI, Anthropic, Google, etc.), streaming, tool calling, structured output, or embeddings, (4) Use React hooks like useChat or useCompletion. Triggers on: "AI SDK", "Vercel AI SDK", "generateText", "streamText", "add AI to my app", "build an agent", "tool calling", "structured output", "useChat".' +--- + +## What the AI SDK Is + +The AI SDK by Vercel (the `ai` package on npm) is a TypeScript toolkit for building AI applications. It provides a unified API across model providers for text generation, structured output, tool calling, agents, embeddings, and framework UI integrations. + +- Repository: https://github.com/vercel/ai +- Documentation: https://ai-sdk.dev/docs + +## Critical: Do Not Trust Your Own Memory + +Whatever you remember about the AI SDK is likely outdated. The SDK changes frequently across versions - APIs are renamed, removed, and added. Your training data almost certainly contains obsolete APIs, deprecated patterns, and model IDs that no longer exist. UI hooks like `useChat` are among the most frequently changed APIs, so be especially careful with client code. + +**Never write AI SDK code from memory.** Always verify every API, option, and pattern against the documentation and source code for the version that is actually installed in the project. + +## Use the Bundled, Version-Matched Docs + +The `ai` package ships its full documentation and source code inside `node_modules`. These always match the installed version, so trust them over anything you remember. + +1. Ensure `ai` is installed. If `node_modules/ai/` does not exist, install **only** the `ai` package using the project's package manager (e.g. `pnpm add ai`). Install provider packages (e.g. `@ai-sdk/openai`) and framework packages (e.g. `@ai-sdk/react`) later, when the task requires them. +2. Read and grep the bundled docs at `node_modules/ai/docs/` and the source at `node_modules/ai/src/`. +3. Provider and framework packages bundle their own docs at `node_modules/@ai-sdk//docs/`. +4. If something isn't in the bundled docs, search https://ai-sdk.dev/docs. You can append `.md` to any docs page URL to get its markdown, and search via `https://ai-sdk.dev/api/search-docs?q=your_query`. +5. If you cannot find support for an answer in the docs or source, say so explicitly — do not guess. + +## AI Gateway: The Fastest Way to Start + +The Vercel AI Gateway is the fastest way to get started with the AI SDK. It provides access to models from OpenAI, Anthropic, Google, and other providers through a single API, without installing provider packages or managing multiple API keys. + +To set it up: + +1. Authenticate with OIDC (for Vercel deployments) or get an AI Gateway API key. +2. Provide it to your app via the `AI_GATEWAY_API_KEY` environment variable. +3. Reference models with `provider/model` strings. + +For exact setup, authentication, and usage, read the bundled guide and the AI Gateway docs. + +### Choosing a Model + +Never use model IDs from memory — models are released and retired frequently. Fetch the current list before writing code that references a model. Do not truncate the list (e.g. with `head`) so you can find the newest models: + +```bash +# All available models +curl -s https://ai-gateway.vercel.sh/v1/models | jq -r '.data[].id' + +# Filter by provider (e.g. anthropic, openai, google) +curl -s https://ai-gateway.vercel.sh/v1/models | jq -r '[.data[] | select(.id | startswith("anthropic/")) | .id] | reverse | .[]' +``` + +When multiple versions of a model exist, prefer the one with the highest version number. + +## Building and Consuming Agents + +Use the SDK's built-in agent abstraction (such as `ToolLoopAgent`) rather than hand-rolling tool-calling loops. For end-to-end type safety, infer the UI message type from your agent definition when consuming it on the client (e.g. with `useChat`). Consuming an agent is framework-specific: check `package.json` to detect the stack, then follow the matching quickstart. + +Look up the current agent, tool, and type-safety APIs in the bundled docs (`node_modules/ai/docs/`, especially the agents section) or at https://ai-sdk.dev/docs. + +## DevTools + +AI SDK DevTools captures your AI SDK calls - requests, responses, tool calls, token usage, and multi-step runs - so you can inspect exactly what your agents do. Use it while developing to debug generations. It is a separate package and is intended for local development only. + +For setup instructions, read the bundled DevTools documentation. + +## Keep the SDK Current + +Outdated installs are the most common source of errors. Compare the installed version against the latest: + +- **Installed:** the `version` field in `node_modules/ai/package.json`. +- **Latest:** run `npm view ai version`. + +If the installed version is a major version (or more) behind the latest, tell the user they are on an old release, and recommend upgrading before continuing. Migration guides are at https://ai-sdk.dev/docs/migration-guides. + +## After Making Changes + +Run the project's type checker. Be minimal — only set options that differ from the defaults, checking docs or source for the defaults rather than over-specifying. Most type errors come from remembered, now-changed APIs; re-check the current docs and source when they occur. diff --git a/.agents/skills/better-accessibility/SKILL.md b/.agents/skills/better-accessibility/SKILL.md new file mode 100644 index 00000000..d2790fdf --- /dev/null +++ b/.agents/skills/better-accessibility/SKILL.md @@ -0,0 +1,148 @@ +--- +name: better-accessibility +description: Accessibility engineering for product interfaces, from focus states and keyboard support to ARIA, forms, and screen readers. Use when building or reviewing UI components, modals, menus, forms, custom widgets, or when the user says "make this accessible" or reports keyboard or screen-reader issues. Triggers on accessibility, a11y, WCAG, aria, focus ring, focus-visible, focus trap, keyboard navigation, tab order, tabindex, screen reader, sr-only, aria-live, alt text, hit area, touch target, prefers-reduced-motion, autoplay, toast duration, skip link, semantic HTML, aria-label, form errors, disabled buttons, "not keyboard accessible". +--- + +# Accessibility that comes with the craft + +Accessibility is not a compliance checkbox bolted on at the end; it is the floor for interface craft. Most of it is free if you use the platform: native elements ship with keyboard support, real labels announce themselves, and a visible focus ring is one CSS rule. Apply these principles when building or reviewing UI code, and match the project's existing styling system (Tailwind vs. plain CSS vs. CSS-in-JS) when applying fixes. + +When reviewing, walk the interface as a keyboard-only user first (every flow must complete without a mouse), then as a screen-reader user: does each control announce a name, a role, and its state? When unsure, prefer the platform default over a custom rebuild, and remove ARIA rather than add it. + +Rendered-pair contrast measurement and color remediation are covered by the `better-colors` skill; visual text sizing and iOS input zoom by `better-typography`; spatial RTL layout by `better-layout`. + +## Quick Reference + +| Category | When to Use | +| --- | --- | +| [Focus & Keyboard](focus-and-keyboard.md) | Focus rings, skip links, tabindex, focus trapping, APG keyboard patterns | +| [Semantics & ARIA](semantics-and-aria.md) | Native elements first, button vs link, landmarks, accessible names, disabled states | +| [Forms](forms.md) | Labels, autocomplete, error messaging, input types | +| [Screen Readers](screen-readers.md) | Visually hidden content, live regions, toasts, alt text, SVG | +| [Hit Areas](hit-areas.md) | Target sizes, expanding hit areas, collision rules | +| [Motion & Zoom](motion-and-zoom.md) | `prefers-reduced-motion`, autoplay and timed UI, 200% zoom, reflow, rem vs px | + +## Core Principles + +### 1. Native Elements First + +The first rule of ARIA: don't use ARIA when a native element exists. `` | Add `aria-label="Close"`; mark the icon `aria-hidden="true"` | The icon-only control has no accessible name | +| HIGH | `src/Nav.tsx:18` | `` | Add `aria-label="Settings"` | The link destination is unavailable to screen readers | + +#### Visible focus rings +| Severity | Location | Before | After | Why | +| --- | --- | --- | --- | --- | +| HIGH | `src/button.css:12` | `button:focus { outline: none; }` | `button:focus-visible { outline: 2px solid; outline-offset: 2px; }` | Keyboard users cannot see focus | +| HIGH | `src/Menu.tsx:31` | `focus:outline-none` | `focus-visible:outline-2 focus-visible:outline-offset-2` | Menu navigation has no visible focus indicator | + +#### Errors that announce +| Severity | Location | Before | After | Why | +| --- | --- | --- | --- | --- | +| HIGH | `src/EmailField.tsx:27` | Error shown only as `border-red-500` | Add `aria-invalid="true"` + `aria-describedby="email-error"` with inline error text | Color alone neither explains nor announces the error | +| MEDIUM | `src/SignupForm.tsx:64` | Submit disabled until the form is valid | Keep submit enabled; on failure, focus the first invalid field | A disabled action hides what must be fixed | + +#### Minimum hit area +| Severity | Location | Before | After | Why | +| --- | --- | --- | --- | --- | +| MEDIUM | `src/Toolbar.tsx:22` | `size-4` icon-only button | Extend the hit area to 44×44px with `after:absolute after:size-11` | The target is too small for reliable touch input | + +### Verification and Verdict + +After the findings: + +1. **Verification**: list the exact checks run and their observed results, including keyboard traversal, accessible-name inspection, and screen-reader or automated checks when applicable. If a check was not run, state what still needs verification. +2. **Verdict**: `Block` if any `HIGH` finding remains, `Needs changes` if only `MEDIUM` or `LOW` findings remain, and `Approve` only when no actionable findings remain. + +When there are no findings, omit the tables, state "No actionable accessibility findings", report verification, and end with `Approve`. diff --git a/.agents/skills/better-accessibility/agents/openai.yaml b/.agents/skills/better-accessibility/agents/openai.yaml new file mode 100644 index 00000000..4f20e8a5 --- /dev/null +++ b/.agents/skills/better-accessibility/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Better Accessibility" + short_description: "Focus, keyboard, ARIA, forms and screen readers" diff --git a/.agents/skills/better-accessibility/focus-and-keyboard.md b/.agents/skills/better-accessibility/focus-and-keyboard.md new file mode 100644 index 00000000..6a2cc6da --- /dev/null +++ b/.agents/skills/better-accessibility/focus-and-keyboard.md @@ -0,0 +1,131 @@ +# Focus and Keyboard + +Focus rings, skip links, tabindex, focus trapping, and the APG keyboard patterns. + +## Focus rings + +Style `:focus-visible`, not bare `:focus`. The browser shows `:focus-visible` for keyboard and assistive-tech focus but suppresses it for mouse clicks, where focus is already obvious. Never write `outline: none` or `focus:outline-none` without a visible replacement; that removes keyboard navigation for sighted keyboard users. + +Prefer the browser's unmodified focus indicator: it adapts to platform and forced-color settings without the author predicting every background. Adding only `outline-offset` generally preserves that indicator. A custom `outline: 2px solid` with no color renders `currentColor`; that is not automatically accessible because the outline may cross colors different from the text's own background. So the preference order is: + +```css +/* Best: keep the browser ring, just give it breathing room */ +:focus-visible { + outline-offset: 2px; +} + +/* Custom ring when the design requires one: use the project's verified token */ +:focus-visible { + outline: 2px solid var(--focus-ring); + outline-offset: 2px; +} +``` + +```tsx +// Tailwind: use the project's focus token or established focus-ring utility + +``` + +A custom focus indicator must meet the applicable project/WCAG target for visible area and change of contrast. Inspect the whole perimeter against every adjacent color it crosses, including component fills, page surfaces, images, gradients, and hover/selected states. A token, brand color, or `currentColor` is acceptable only when that rendered check passes. + +In `forced-colors: active` (Windows High Contrast), keep the default color adjustment or explicitly use a system color such as `Highlight`; never freeze the authored color with `forced-color-adjust: none` unless the control remains perceivable. + +Group focus styles with `:focus-within` when a wrapper should light up while an inner input has focus (e.g. a search box with an icon inside the border). + +## Skip link + +When repeated navigation or other repeated chrome precedes the primary content, the first focusable element is a "Skip to content" link targeting `
`. Visually hide it until focused: + +```css +.skip-link { + position: absolute; + inset-inline-start: -999px; +} +.skip-link:focus { + inset-inline-start: 16px; + top: 16px; +} +``` + +```html + + +
+
+ +``` + +Give in-page anchor targets `scroll-margin-top` (e.g. `scroll-margin-top: 80px` under a sticky header) so the target isn't hidden when jumped to. + +## tabindex rules + +- `tabindex="0"`: adds an element to the natural tab order. Only for custom interactive elements that aren't natively focusable. +- `tabindex="-1"`: focusable via JavaScript only (`el.focus()`). Use for headings you move focus to, modal containers, and roving-tabindex members. +- Positive `tabindex`: never. It hijacks the tab order for the whole page; fix the DOM order instead. + +### Roving tabindex + +Composite widgets (tabs, menus, toolbars, radio groups) occupy a single Tab stop. The active item has `tabindex="0"`, all others `tabindex="-1"`, and arrow keys move both focus and the `0`: + +```tsx +
+ {tabs.map((tab, i) => ( + + ))} +
+``` + +## Focus trapping and restoration + +Modals must trap focus. The modern technique is the `inert` attribute on everything behind the dialog; it removes background content from the tab order and from assistive tech in one move: + +```tsx +// On open +document.getElementById("app-content").inert = true; +const dialog = dialogRef.current; +(dialog.querySelector("[autofocus]") ?? + dialog.querySelector("button, [href], input, select, textarea"))?.focus(); + +// On close +document.getElementById("app-content").inert = false; +triggerRef.current?.focus(); // always return focus to the element that opened it +``` + +Native `` with `showModal()` gives you the trap, `inert` background, and Escape handling for free; prefer it. A custom overlay that can't use `` needs `role="dialog"`, `aria-modal="true"`, and an accessible name (`aria-labelledby` pointing at its heading). Either way: + +- On open, focus the first focusable element; for destructive confirmations, focus the least destructive action instead. +- On close, return focus to the trigger. If the trigger no longer exists, move focus to the nearest logical container. +- Add `overscroll-behavior: contain` on the dialog so scrolling inside never scrolls the page behind it. + +## Keyboard patterns (ARIA APG) + +Native elements come with these behaviors; custom widgets must implement them. A role is a promise: if you give something `role="tab"`, users expect the full tab keyboard model. + +| Widget | Keys | +| --- | --- | +| Dialog | Tab/Shift+Tab cycle inside (wrap at ends); Escape closes | +| Tabs | Arrow keys move between tabs (wrapping); Tab exits to the panel; Home/End jump to first/last | +| Menu button | Enter/Space/ArrowDown opens and focuses first item; ArrowUp opens and focuses last; arrows navigate; Escape closes and refocuses the button | +| Disclosure / accordion | Header is a `