diff --git a/docs/superpowers/plans/2026-08-07-navigation-phase-1.md b/docs/superpowers/plans/2026-08-07-navigation-phase-1.md
new file mode 100644
index 00000000..cb7cf144
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-07-navigation-phase-1.md
@@ -0,0 +1,1665 @@
+# Navigation Group — Phase 1 Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Ship runtime-owned roving arrow-key focus, then build Nav, Pagination and Stepper as real components on that foundation.
+
+**Architecture:** One declarative attribute pair (`data-wrn-roving` on a container, `data-wrn-roving-item` on children) implements ARIA roving focus once in the reactive runtime, the same way modal dialog focus already lives there. The three components then declare intent with a single attribute instead of carrying five near-identical `@keydown` handlers. Each component uses a local `style {}` block with `wire-*` classes, declares its `outputs`, and takes props in / reports outputs out with no global state.
+
+**Tech Stack:** Bun, TypeScript, `.wrn` components, happy-dom for runtime tests, `@wrnexus/test` (`renderComponent`, `mountHtml`) for component tests.
+
+Source spec: `docs/superpowers/specs/2026-08-07-navigation-components-design.md`
+
+## Global Constraints
+
+These apply to every task. Each has already cost this codebase real debugging time.
+
+- **No apostrophes in `.wrn` comments.** The brace scanner breaks on them and reports a misleading unbalanced-brace error pointing at a different block.
+- **Inside `style {}` blocks use `/* */` only.** `//` is not a CSS comment and silently swallows the rule that follows it.
+- **`packages/csr/src/reactive-runtime.ts` is a `String.raw` template.** A raw backtick terminates it. Escape as `` \` ``. Avoid template literals and regex literals with escapes inside added code.
+- **No deferred state writes in client functions.** State written after the function returns — inside a `setTimeout`, a promise callback, or an observer — is dropped. Anything asynchronous belongs in the runtime.
+- **Never call a peer function after an application callback.** The peer wrapper flushes the entry-time state snapshot.
+- **No boolean attributes bound to loop variables** (`checked`, `selected`, `disabled` referencing an `{#each}` variable). They compile to server expressions where the loop variable does not exist.
+- **Package components need explicit imports** (`import X from "./X.wrn"`), enforced by `bun run check:component-imports`.
+- **happy-dom returns zeros from `getBoundingClientRect()`.** Never gate behaviour on measured size in code that needs a test. Use the `hidden` attribute or a `[data-show="false"]` ancestor instead.
+- **Runtime size budget is 175000 bytes** for `reactive-runtime.ts`, currently ~167k. Check with `node scripts/security-performance-audit.mjs` before committing.
+- Every task ends green on `bun run test`. The phase ends green on `bun run check:production`.
+
+---
+
+## File Structure
+
+| File | Responsibility |
+| ----------------------------------------------------------- | --------------------------------------------------------------------------------- |
+| `packages/csr/src/reactive-runtime.ts` | Add `setupRovingFocus()` and fix `isDialogVisible()` to be testable. Modify only. |
+| `packages/csr/test/reactive.test.ts` | Roving focus tests and the missing dialog-behaviour tests. |
+| `packages/ui/components/Pagination.wrn` | Replace the scaffold. Page controls, compact + numbered. |
+| `packages/ui/components/Stepper.wrn` | Replace the scaffold. Ordered steps, indexed named slots. |
+| `packages/ui/components/Nav.wrn` | Replace the scaffold. Multi-level links, roving, responsive. |
+| `packages/ui/test/ui.test.ts` | Per-component assertions. Modify only. |
+| `examples/component-showcase/scripts/showcase-profiles.mjs` | Demo profiles for the three components. Modify only. |
+
+Task order matters: Task 1 produces the roving attribute contract that Tasks 3 and 4 consume.
+
+---
+
+### Task 1: Roving focus in the runtime
+
+**Files:**
+
+- Modify: `packages/csr/src/reactive-runtime.ts` (add near the existing `setupModalDialogs`)
+- Test: `packages/csr/test/reactive.test.ts`
+
+**Interfaces:**
+
+- Consumes: nothing.
+- Produces: the DOM contract used by Tasks 3 and 4 —
+ - `data-wrn-roving="horizontal" | "vertical" | "both"` on a container element
+ - `data-wrn-roving-item` on each focusable descendant
+ - The runtime sets `tabindex="0"` on the active item and `tabindex="-1"` on the rest, and moves focus on ArrowLeft/Right (horizontal), ArrowUp/Down (vertical), both axes for `"both"`, plus Home and End, wrapping at the ends and skipping disabled items.
+ - Active item is the one with `aria-selected="true"`, `aria-current="page"`, or `aria-current="step"`; otherwise the first item.
+
+- [ ] **Step 1: Write the failing tests**
+
+Append to `packages/csr/test/reactive.test.ts`:
+
+```ts
+test("roving focus moves with arrow keys and wraps", () => {
+ const win = mount(
+ `
`,
+ );
+ const a = win.document.querySelector("#a") as HTMLElement;
+ a.focus();
+ a.dispatchEvent(new win.KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
+ expect(win.document.activeElement!.id).toBe("a");
+});
+
+test("roving tabindex starts on the selected item, not the first", () => {
+ const win = mount(
+ `
+
+
+
`,
+ );
+ expect(win.document.querySelector("#b")!.getAttribute("tabindex")).toBe("0");
+ expect(win.document.querySelector("#a")!.getAttribute("tabindex")).toBe("-1");
+});
+
+test("nested roving groups do not capture the outer group items", () => {
+ const win = mount(
+ `
+
+
+
+
+
+
+
`,
+ );
+ const doc = win.document;
+ const a = doc.querySelector("#a") as HTMLElement;
+ a.focus();
+ a.dispatchEvent(new win.KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }));
+ expect(doc.activeElement!.id).toBe("b");
+});
+```
+
+- [ ] **Step 2: Run the tests to verify they fail**
+
+```bash
+bun test packages/csr/test/reactive.test.ts
+```
+
+Expected: FAIL. Arrow keys do nothing, so `activeElement` stays `a` and no `tabindex` is set.
+
+- [ ] **Step 3: Implement the roving manager**
+
+In `packages/csr/src/reactive-runtime.ts`, insert immediately before `function hydrateScopes(root) {`:
+
+```js
+/*
+ * Roving arrow-key focus, the ARIA pattern shared by tabs, menus, sidebars
+ * and steppers. A container marked data-wrn-roving owns its
+ * [data-wrn-roving-item] descendants: exactly one carries tabindex="0" so
+ * Tab reaches the group once, and the arrow keys move focus within it.
+ *
+ * This lives here rather than in each component because it is the same code
+ * five times over, and because visibility and focus bookkeeping cannot be
+ * held in component state -- a client function writing after it returns has
+ * that write dropped.
+ */
+var ROVING_SELECTOR = "[data-wrn-roving]";
+var ROVING_ITEM_SELECTOR = "[data-wrn-roving-item]";
+
+function rovingItems(container) {
+ var found = [];
+ var candidates = container.querySelectorAll(ROVING_ITEM_SELECTOR);
+ for (var index = 0; index < candidates.length; index += 1) {
+ var candidate = candidates[index];
+ // A nested group owns its own items; do not steal them.
+ if (candidate.closest(ROVING_SELECTOR) !== container) continue;
+ if (candidate.hasAttribute("disabled")) continue;
+ if (candidate.getAttribute("aria-disabled") === "true") continue;
+ // Deliberately not a size check: the test DOM reports every element as
+ // zero-sized, so measuring here would make the whole feature untestable.
+ if (candidate.hasAttribute("hidden")) continue;
+ if (candidate.closest('[data-show="false"]')) continue;
+ found.push(candidate);
+ }
+ return found;
+}
+
+function rovingActiveIndex(items) {
+ for (var index = 0; index < items.length; index += 1) {
+ var item = items[index];
+ if (item.getAttribute("aria-selected") === "true") return index;
+ var current = item.getAttribute("aria-current");
+ if (current === "page" || current === "step" || current === "true") return index;
+ }
+ return 0;
+}
+
+function applyRovingTabindex(items, activeIndex) {
+ for (var index = 0; index < items.length; index += 1) {
+ items[index].setAttribute("tabindex", index === activeIndex ? "0" : "-1");
+ }
+}
+
+function syncRovingGroup(container) {
+ var items = rovingItems(container);
+ if (!items.length) return;
+ applyRovingTabindex(items, rovingActiveIndex(items));
+}
+
+function syncRovingGroups() {
+ var groups = document.querySelectorAll(ROVING_SELECTOR);
+ for (var index = 0; index < groups.length; index += 1) syncRovingGroup(groups[index]);
+}
+
+function handleRovingKeydown(event) {
+ var target = event.target;
+ if (!target || !target.closest) return;
+ var item = target.closest(ROVING_ITEM_SELECTOR);
+ if (!item) return;
+ var container = item.closest(ROVING_SELECTOR);
+ if (!container) return;
+
+ var items = rovingItems(container);
+ var index = items.indexOf(item);
+ if (index === -1) return;
+
+ var orientation = container.getAttribute("data-wrn-roving") || "horizontal";
+ var horizontal = orientation === "horizontal" || orientation === "both";
+ var vertical = orientation === "vertical" || orientation === "both";
+ var key = event.key;
+ var next = -1;
+
+ if ((horizontal && key === "ArrowRight") || (vertical && key === "ArrowDown")) {
+ next = (index + 1) % items.length;
+ } else if ((horizontal && key === "ArrowLeft") || (vertical && key === "ArrowUp")) {
+ next = (index - 1 + items.length) % items.length;
+ } else if (key === "Home") {
+ next = 0;
+ } else if (key === "End") {
+ next = items.length - 1;
+ } else {
+ return;
+ }
+
+ event.preventDefault();
+ applyRovingTabindex(items, next);
+ if (items[next].focus) items[next].focus();
+}
+
+function setupRovingFocus() {
+ if (window.__wrnexusRovingBound) return;
+ window.__wrnexusRovingBound = true;
+
+ document.addEventListener("keydown", handleRovingKeydown, true);
+
+ if (typeof MutationObserver === "function") {
+ new MutationObserver(function () {
+ window.setTimeout(syncRovingGroups, 0);
+ }).observe(document.documentElement, {
+ subtree: true,
+ childList: true,
+ attributes: true,
+ attributeFilter: ["aria-selected", "aria-current", "disabled", "aria-disabled", "hidden"],
+ });
+ }
+
+ syncRovingGroups();
+}
+```
+
+Then register it next to the existing call:
+
+```js
+setupAnchoredOverlays();
+setupModalDialogs();
+setupRovingFocus();
+```
+
+- [ ] **Step 4: Run the tests to verify they pass**
+
+```bash
+bun test packages/csr/test/reactive.test.ts
+```
+
+Expected: PASS, all five new tests.
+
+- [ ] **Step 5: Check the runtime size budget**
+
+```bash
+node scripts/security-performance-audit.mjs
+```
+
+Expected: `ok PERF-RUNTIME-SIZE-REACTIVE`. If it fails, the roving code must shrink — do not raise the ceiling again in this task.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add packages/csr/src/reactive-runtime.ts packages/csr/test/reactive.test.ts
+git commit -m "feat(csr): runtime-owned roving arrow-key focus"
+```
+
+---
+
+### Task 2: Make dialog visibility testable and cover it
+
+Closes a gap shipped in 0.8.5: `isDialogVisible` gates on `getBoundingClientRect`, which is always zero in happy-dom, so the modal focus trap and scroll lock have no test coverage at all.
+
+**Files:**
+
+- Modify: `packages/csr/src/reactive-runtime.ts` (the `isDialogVisible` function)
+- Test: `packages/csr/test/reactive.test.ts`
+
+**Interfaces:**
+
+- Consumes: `openDialogs`, `isDialogVisible`, `syncDialogs` from the existing dialog block.
+- Produces: no new contract. Behaviour is unchanged in a real browser; only the visibility test becomes DOM-implementation independent.
+
+- [ ] **Step 1: Write the failing tests**
+
+Append to `packages/csr/test/reactive.test.ts`:
+
+```ts
+test("opening a modal dialog traps Tab inside it", () => {
+ const win = mount(
+ `