diff --git a/apps/cockpit/src/components/cockpit-shell.tsx b/apps/cockpit/src/components/cockpit-shell.tsx
index fcb803147..695b7a842 100644
--- a/apps/cockpit/src/components/cockpit-shell.tsx
+++ b/apps/cockpit/src/components/cockpit-shell.tsx
@@ -10,6 +10,7 @@ import { NarrativeDocs } from './narrative-docs/narrative-docs';
import { ModeSwitcher } from './modes/mode-switcher';
import { RunMode } from './run-mode/run-mode';
import { CockpitSidebar } from './sidebar/cockpit-sidebar';
+import { MobileNavOverlay } from './mobile-nav-overlay';
const PRIMARY_MODES = ['Run', 'Code', 'Docs', 'API'] as const;
type PrimaryMode = (typeof PRIMARY_MODES)[number];
@@ -69,30 +70,14 @@ export function CockpitShell({
/>
- {/* Mobile sidebar overlay */}
- {isSidebarOpen && (
- <>
-
setIsSidebarOpen(false)}
- />
-
-
-
- >
- )}
+ {/* Mobile full-screen nav overlay */}
+
setIsSidebarOpen(false)}
+ />
diff --git a/apps/cockpit/src/components/mobile-nav-overlay.spec.tsx b/apps/cockpit/src/components/mobile-nav-overlay.spec.tsx
new file mode 100644
index 000000000..7808c4252
--- /dev/null
+++ b/apps/cockpit/src/components/mobile-nav-overlay.spec.tsx
@@ -0,0 +1,131 @@
+import React from 'react';
+import { renderToStaticMarkup } from 'react-dom/server';
+import { describe, expect, it } from 'vitest';
+import { cockpitManifest } from '@cacheplane/cockpit-registry';
+import { buildNavigationTree } from '../lib/route-resolution';
+import { MobileNavOverlay } from './mobile-nav-overlay';
+
+const tree = buildNavigationTree(cockpitManifest);
+const entry = cockpitManifest.find(
+ (e) =>
+ e.product === 'render' &&
+ e.section === 'core-capabilities' &&
+ e.topic === 'spec-rendering' &&
+ e.language === 'python'
+)!;
+
+describe('MobileNavOverlay', () => {
+ it('renders nothing when closed', () => {
+ const html = renderToStaticMarkup(
+ {}}
+ />
+ );
+ expect(html).toBe('');
+ });
+
+ it('renders all four product groups when open', () => {
+ const html = renderToStaticMarkup(
+ {}}
+ />
+ );
+ expect(html).toContain('LangGraph');
+ expect(html).toContain('Render');
+ expect(html).toContain('Chat');
+ expect(html).toContain('Deep Agents');
+ });
+
+ it('renders topic chips as links', () => {
+ const html = renderToStaticMarkup(
+ {}}
+ />
+ );
+ expect(html).toContain('Streaming');
+ expect(html).toContain('Persistence');
+ expect(html).toContain('Messages');
+ expect(html).toContain('href="/');
+ });
+
+ it('highlights the active entry chip', () => {
+ const html = renderToStaticMarkup(
+ {}}
+ />
+ );
+ expect(html).toContain('aria-current="page"');
+ });
+
+ it('strips product prefix from topic titles', () => {
+ const html = renderToStaticMarkup(
+ {}}
+ />
+ );
+ expect(html).toContain('Spec Rendering');
+ expect(html).not.toContain('>Render Spec Rendering<');
+ });
+
+ it('filters out overview topics', () => {
+ const html = renderToStaticMarkup(
+ {}}
+ />
+ );
+ // No chip should link to an overview topic (topic segment = "overview")
+ const hrefMatches = html.match(/href="[^"]+"/g) || [];
+ const overviewHrefs = hrefMatches.filter((h) => /\/overview\/overview\//.test(h));
+ expect(overviewHrefs).toHaveLength(0);
+ });
+
+ it('includes the language picker', () => {
+ const html = renderToStaticMarkup(
+ {}}
+ />
+ );
+ expect(html).toContain('Python');
+ });
+
+ it('includes a close button', () => {
+ const html = renderToStaticMarkup(
+ {}}
+ />
+ );
+ expect(html).toContain('aria-label="Close navigation"');
+ });
+});
diff --git a/apps/cockpit/src/components/mobile-nav-overlay.tsx b/apps/cockpit/src/components/mobile-nav-overlay.tsx
new file mode 100644
index 000000000..f99fe6d70
--- /dev/null
+++ b/apps/cockpit/src/components/mobile-nav-overlay.tsx
@@ -0,0 +1,175 @@
+'use client';
+
+import React, { useEffect, useState } from 'react';
+import type { CockpitManifestEntry } from '@cacheplane/cockpit-registry';
+import type { NavigationProduct } from '../lib/route-resolution';
+import { toCockpitPath } from '../lib/route-resolution';
+import { PRODUCT_LABELS, stripProductPrefix } from '../lib/navigation-labels';
+import { LanguagePicker } from './sidebar/language-picker';
+
+function CloseIcon() {
+ return (
+
+
+
+ );
+}
+
+interface MobileNavOverlayProps {
+ navigationTree: NavigationProduct[];
+ manifest: CockpitManifestEntry[];
+ entry: CockpitManifestEntry;
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+export function MobileNavOverlay({
+ navigationTree,
+ manifest,
+ entry,
+ isOpen,
+ onClose,
+}: MobileNavOverlayProps) {
+ const [state, setState] = useState<'closed' | 'open' | 'closing'>(
+ isOpen ? 'open' : 'closed'
+ );
+
+ useEffect(() => {
+ if (isOpen) {
+ setState('open');
+ } else if (state === 'open') {
+ setState('closing');
+ }
+ }, [isOpen]);
+
+ useEffect(() => {
+ if (state !== 'closing') return;
+ const timer = setTimeout(() => setState('closed'), 150);
+ return () => clearTimeout(timer);
+ }, [state]);
+
+ useEffect(() => {
+ if (state !== 'open') return;
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') onClose();
+ };
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, [state, onClose]);
+
+ if (state === 'closed') return null;
+
+ return (
+
+ {/* Header */}
+
+
+ Cockpit
+
+
+
+
+
+
+
+
+
+ {/* Scrollable product cards */}
+
+ {navigationTree.map((product) => {
+ const label = PRODUCT_LABELS[product.product] ?? product.product;
+ const topics = product.sections.flatMap((section) =>
+ section.entries.filter((e) => e.topic !== 'overview')
+ );
+
+ if (topics.length === 0) return null;
+
+ return (
+
+
+ {label}
+
+
+ {topics.map((topicEntry) => {
+ const isActive =
+ topicEntry.product === entry.product &&
+ topicEntry.section === entry.section &&
+ topicEntry.topic === entry.topic &&
+ topicEntry.page === entry.page;
+
+ return (
+
+ {stripProductPrefix(topicEntry.title)}
+
+ );
+ })}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/apps/cockpit/src/components/sidebar/navigation-groups.tsx b/apps/cockpit/src/components/sidebar/navigation-groups.tsx
index 3095e8b57..81a86c8f1 100644
--- a/apps/cockpit/src/components/sidebar/navigation-groups.tsx
+++ b/apps/cockpit/src/components/sidebar/navigation-groups.tsx
@@ -4,22 +4,7 @@ import React, { useState } from 'react';
import type { CockpitManifestEntry } from '@cacheplane/cockpit-registry';
import type { NavigationProduct } from '../../lib/route-resolution';
import { toCockpitPath } from '../../lib/route-resolution';
-
-const PRODUCT_LABELS: Record = {
- 'deep-agents': 'Deep Agents',
- 'langgraph': 'LangGraph',
- 'render': 'Render',
- 'chat': 'Chat',
-};
-
-
-function stripProductPrefix(title: string): string {
- const prefixes = ['Deep Agents ', 'LangGraph ', 'Render ', 'Chat '];
- for (const p of prefixes) {
- if (title.startsWith(p)) return title.slice(p.length);
- }
- return title;
-}
+import { PRODUCT_LABELS, stripProductPrefix } from '../../lib/navigation-labels';
interface NavigationGroupsProps {
tree: NavigationProduct[];
diff --git a/apps/cockpit/src/lib/navigation-labels.ts b/apps/cockpit/src/lib/navigation-labels.ts
new file mode 100644
index 000000000..73641ee93
--- /dev/null
+++ b/apps/cockpit/src/lib/navigation-labels.ts
@@ -0,0 +1,14 @@
+export const PRODUCT_LABELS: Record = {
+ 'deep-agents': 'Deep Agents',
+ 'langgraph': 'LangGraph',
+ 'render': 'Render',
+ 'chat': 'Chat',
+};
+
+export function stripProductPrefix(title: string): string {
+ const prefixes = ['Deep Agents ', 'LangGraph ', 'Render ', 'Chat '];
+ for (const p of prefixes) {
+ if (title.startsWith(p)) return title.slice(p.length);
+ }
+ return title;
+}
diff --git a/docs/superpowers/plans/2026-04-09-cockpit-mobile-menu.md b/docs/superpowers/plans/2026-04-09-cockpit-mobile-menu.md
new file mode 100644
index 000000000..6c804da38
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-09-cockpit-mobile-menu.md
@@ -0,0 +1,442 @@
+# Cockpit Mobile Menu Redesign 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:** Replace the cockpit's narrow w-64 mobile drawer with a full-screen overlay using product cards and topic chips.
+
+**Architecture:** A new `MobileNavOverlay` client component renders a `fixed inset-0` overlay on mobile with product groups as glass cards and topics as pill-shaped `` links. It reuses the `LanguagePicker` component and `toCockpitPath` helper. The existing `CockpitSidebar` is untouched (desktop only). The `cockpit-shell.tsx` swaps the old mobile overlay JSX for the new component.
+
+**Tech Stack:** React 19, Next.js 15, Tailwind CSS v4, CSS custom properties (`--ds-*` design tokens), Vitest
+
+**Spec:** `docs/superpowers/specs/2026-04-09-cockpit-mobile-menu-design.md`
+
+---
+
+## File Map
+
+| File | Action | Responsibility |
+|------|--------|----------------|
+| `apps/cockpit/src/components/mobile-nav-overlay.tsx` | Create | Full-screen mobile nav overlay with product cards + topic chips |
+| `apps/cockpit/src/components/mobile-nav-overlay.spec.tsx` | Create | Tests for the overlay component |
+| `apps/cockpit/src/components/cockpit-shell.tsx` | Modify (lines 73-95) | Replace old mobile drawer with `MobileNavOverlay` |
+
+---
+
+### Task 1: Create MobileNavOverlay component with tests
+
+**Files:**
+- Create: `apps/cockpit/src/components/mobile-nav-overlay.spec.tsx`
+- Create: `apps/cockpit/src/components/mobile-nav-overlay.tsx`
+
+- [ ] **Step 1: Write the failing tests**
+
+Create `apps/cockpit/src/components/mobile-nav-overlay.spec.tsx`:
+
+```tsx
+import React from 'react';
+import { renderToStaticMarkup } from 'react-dom/server';
+import { describe, expect, it } from 'vitest';
+import { cockpitManifest } from '@cacheplane/cockpit-registry';
+import { buildNavigationTree } from '../lib/route-resolution';
+import { MobileNavOverlay } from './mobile-nav-overlay';
+
+const tree = buildNavigationTree(cockpitManifest);
+const entry = cockpitManifest.find(
+ (e) =>
+ e.product === 'render' &&
+ e.section === 'core-capabilities' &&
+ e.topic === 'spec-rendering' &&
+ e.language === 'python'
+)!;
+
+describe('MobileNavOverlay', () => {
+ it('renders nothing when closed', () => {
+ const html = renderToStaticMarkup(
+ {}}
+ />
+ );
+ expect(html).toBe('');
+ });
+
+ it('renders all four product groups when open', () => {
+ const html = renderToStaticMarkup(
+ {}}
+ />
+ );
+ expect(html).toContain('LangGraph');
+ expect(html).toContain('Render');
+ expect(html).toContain('Chat');
+ expect(html).toContain('Deep Agents');
+ });
+
+ it('renders topic chips as links', () => {
+ const html = renderToStaticMarkup(
+ {}}
+ />
+ );
+ expect(html).toContain('Streaming');
+ expect(html).toContain('Persistence');
+ expect(html).toContain('Messages');
+ expect(html).toContain('href="/');
+ });
+
+ it('highlights the active entry chip', () => {
+ const html = renderToStaticMarkup(
+ {}}
+ />
+ );
+ expect(html).toContain('aria-current="page"');
+ });
+
+ it('strips product prefix from topic titles', () => {
+ const html = renderToStaticMarkup(
+ {}}
+ />
+ );
+ // "Render Spec Rendering" should become "Spec Rendering"
+ // Should NOT contain the double-prefixed form
+ expect(html).toContain('Spec Rendering');
+ expect(html).not.toContain('>Render Spec Rendering<');
+ });
+
+ it('filters out overview topics', () => {
+ const html = renderToStaticMarkup(
+ {}}
+ />
+ );
+ // overview entries should be excluded
+ const overviewLinkCount = (html.match(/\/overview\//g) || []).length;
+ // Only the path segments that are part of other routes, not standalone overview chips
+ expect(html).not.toMatch(/data-topic="overview"/);
+ });
+
+ it('includes the language picker', () => {
+ const html = renderToStaticMarkup(
+ {}}
+ />
+ );
+ expect(html).toContain('Python');
+ });
+
+ it('includes a close button', () => {
+ const html = renderToStaticMarkup(
+ {}}
+ />
+ );
+ expect(html).toContain('aria-label="Close navigation"');
+ });
+});
+```
+
+- [ ] **Step 2: Run tests to verify they fail**
+
+Run: `npx nx test cockpit -- --run --reporter=verbose apps/cockpit/src/components/mobile-nav-overlay.spec.tsx`
+
+Expected: FAIL — `MobileNavOverlay` does not exist yet.
+
+- [ ] **Step 3: Implement MobileNavOverlay**
+
+Create `apps/cockpit/src/components/mobile-nav-overlay.tsx`:
+
+```tsx
+'use client';
+
+import React, { useEffect, useRef, useState } from 'react';
+import type { CockpitManifestEntry } from '@cacheplane/cockpit-registry';
+import type { NavigationProduct } from '../lib/route-resolution';
+import { toCockpitPath } from '../lib/route-resolution';
+import { LanguagePicker } from './sidebar/language-picker';
+
+const PRODUCT_LABELS: Record = {
+ 'deep-agents': 'Deep Agents',
+ 'langgraph': 'LangGraph',
+ 'render': 'Render',
+ 'chat': 'Chat',
+};
+
+function stripProductPrefix(title: string): string {
+ const prefixes = ['Deep Agents ', 'LangGraph ', 'Render ', 'Chat '];
+ for (const p of prefixes) {
+ if (title.startsWith(p)) return title.slice(p.length);
+ }
+ return title;
+}
+
+function CloseIcon() {
+ return (
+
+
+
+ );
+}
+
+interface MobileNavOverlayProps {
+ navigationTree: NavigationProduct[];
+ manifest: CockpitManifestEntry[];
+ entry: CockpitManifestEntry;
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+export function MobileNavOverlay({
+ navigationTree,
+ manifest,
+ entry,
+ isOpen,
+ onClose,
+}: MobileNavOverlayProps) {
+ const overlayRef = useRef(null);
+ const [state, setState] = useState<'closed' | 'open' | 'closing'>('closed');
+
+ useEffect(() => {
+ if (isOpen) {
+ setState('open');
+ } else if (state === 'open') {
+ setState('closing');
+ }
+ }, [isOpen]);
+
+ useEffect(() => {
+ if (state === 'closing') {
+ const timer = setTimeout(() => setState('closed'), 150);
+ return () => clearTimeout(timer);
+ }
+ }, [state]);
+
+ useEffect(() => {
+ if (state !== 'open') return;
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') onClose();
+ };
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, [state, onClose]);
+
+ if (state === 'closed') return null;
+
+ return (
+
+ {/* Header */}
+
+
+ Cockpit
+
+
+
+
+
+
+
+
+
+ {/* Scrollable product cards */}
+
+ {navigationTree.map((product) => {
+ const label = PRODUCT_LABELS[product.product] ?? product.product;
+ const topics = product.sections.flatMap((section) =>
+ section.entries.filter((e) => e.topic !== 'overview')
+ );
+
+ if (topics.length === 0) return null;
+
+ return (
+
+
+ {label}
+
+
+ {topics.map((topicEntry) => {
+ const isActive =
+ topicEntry.product === entry.product &&
+ topicEntry.section === entry.section &&
+ topicEntry.topic === entry.topic &&
+ topicEntry.page === entry.page;
+
+ return (
+
+ {stripProductPrefix(topicEntry.title)}
+
+ );
+ })}
+
+
+ );
+ })}
+
+
+ );
+}
+```
+
+- [ ] **Step 4: Run tests to verify they pass**
+
+Run: `npx nx test cockpit -- --run --reporter=verbose apps/cockpit/src/components/mobile-nav-overlay.spec.tsx`
+
+Expected: All 7 tests PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add apps/cockpit/src/components/mobile-nav-overlay.tsx apps/cockpit/src/components/mobile-nav-overlay.spec.tsx
+git commit -m "feat(cockpit): add full-screen mobile nav overlay component"
+```
+
+---
+
+### Task 2: Integrate MobileNavOverlay into cockpit-shell.tsx
+
+**Files:**
+- Modify: `apps/cockpit/src/components/cockpit-shell.tsx:1-148`
+
+- [ ] **Step 1: Add the import**
+
+At the top of `cockpit-shell.tsx`, add the import after line 12 (`import { CockpitSidebar }...`):
+
+```tsx
+import { MobileNavOverlay } from './mobile-nav-overlay';
+```
+
+- [ ] **Step 2: Replace the mobile sidebar overlay**
+
+Remove lines 73-95 (the `{isSidebarOpen && (<>...>)}` block) and replace with:
+
+```tsx
+ {/* Mobile full-screen nav overlay */}
+ setIsSidebarOpen(false)}
+ />
+```
+
+Everything else in the file stays the same — the `isSidebarOpen` state, the hamburger button, the desktop sidebar, the header, and the content area are all unchanged.
+
+- [ ] **Step 3: Run the full cockpit test suite**
+
+Run: `npx nx test cockpit -- --run --reporter=verbose`
+
+Expected: All existing tests PASS (sidebar, language-picker, mode-switcher, run-mode, etc.) plus the new mobile-nav-overlay tests.
+
+- [ ] **Step 4: Verify locally in the browser**
+
+Run: `npx nx serve cockpit --port 4201`
+
+Open `http://localhost:4201/render/core-capabilities/spec-rendering/overview/python` in a mobile viewport (Chrome DevTools device toolbar, ~375px wide):
+- Tap the hamburger menu icon
+- Overlay should appear full-screen with animated fade-in
+- All 4 product cards visible with topic chips
+- "Spec Rendering" chip should be highlighted
+- Tap X to close — should fade out
+- Tap a different chip — should navigate to that page
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add apps/cockpit/src/components/cockpit-shell.tsx
+git commit -m "feat(cockpit): integrate full-screen mobile nav overlay in shell"
+```