+
+
+ );
+}
+```
+
+Notes:
+- `side="right"` — locked decision (CONTEXT.md, DRAWER-02).
+- Engagement intentionally has no `ExternalLink` icon (it's an in-shell route per DRAWER-03).
+- Quotes/Configuration Items/Backup Status/Ticket Digest/Admin/Sync each carry `ExternalLink` (DRAWER-04). Do NOT include Engagement in the desktop list (it migrated to "Mobile sections").
+- `SheetClose asChild` wraps each link so tapping a row closes the drawer (better UX; Radix Sheet pattern).
+- `pb-safe` on Section 3 keeps the Sign out row clear of the home indicator on iOS.
+- `` satisfies Radix's a11y requirement when the description is non-visual.
+
+
+ test -f components/mobile/MoreDrawer.tsx && grep -q "side=\"right\"" components/mobile/MoreDrawer.tsx && grep -q "signOut()" components/mobile/MoreDrawer.tsx && grep -q "/auth/sign-in" components/mobile/MoreDrawer.tsx && grep -q "Mobile sections" components/mobile/MoreDrawer.tsx && grep -q "Full site" components/mobile/MoreDrawer.tsx && grep -q "Account" components/mobile/MoreDrawer.tsx
+
+
+ - `test -f components/mobile/MoreDrawer.tsx` exits 0
+ - `grep -E "export function MoreDrawer" components/mobile/MoreDrawer.tsx` matches
+ - `grep -E "side=\"right\"" components/mobile/MoreDrawer.tsx` matches (DRAWER-02)
+ - `grep -E "/mobile/engagement" components/mobile/MoreDrawer.tsx` matches (DRAWER-03)
+ - `grep -E "/quotes" components/mobile/MoreDrawer.tsx` matches AND `grep -E "/configuration-items" components/mobile/MoreDrawer.tsx` matches AND `grep -E "/backup-status" components/mobile/MoreDrawer.tsx` matches AND `grep -E "/admin/ticket-digest" components/mobile/MoreDrawer.tsx` matches AND `grep -E "/admin/sync" components/mobile/MoreDrawer.tsx` matches (DRAWER-04)
+ - `grep -E "ExternalLink" components/mobile/MoreDrawer.tsx` matches (DRAWER-04 hint)
+ - `grep -E "signOut\(\)" components/mobile/MoreDrawer.tsx` matches AND `grep -E "/auth/sign-in" components/mobile/MoreDrawer.tsx` matches (DRAWER-05)
+ - `grep -E "open: boolean" components/mobile/MoreDrawer.tsx` matches AND `grep -E "onOpenChange" components/mobile/MoreDrawer.tsx` matches (controlled drawer)
+
+ The drawer file exists, exports `MoreDrawer({ open, onOpenChange })`, contains all three sections with correct routes, calls `signOut()` then `router.push('/auth/sign-in')`, and uses `side="right"`.
+
+
+
+ Task 2: Create components/mobile/HeaderBar.tsx (sticky top header — brand, Bell, avatar)
+ components/mobile/HeaderBar.tsx
+
+ - components/branding/wulf-mark.tsx (WulfMark prop signature)
+ - components/navigation/user-menu.tsx (initials pattern reference)
+ - app/styles/brand.css (confirm `pt-safe` utility exists)
+ - components/ui/button.tsx (Button variant/size API)
+ - .planning/phases/02-mobile-shell-more-drawer/02-CONTEXT.md (header decisions: SHELL-02..04, no page title)
+
+
+Create the file `components/mobile/HeaderBar.tsx` with literal contents below. The header takes `onAvatarClick` so the parent layout can wire it to the same drawer state used by `BottomNav`.
+
+```tsx
+'use client';
+
+/* HeaderBar — phase 02 (SHELL-02..04).
+ *
+ * Sticky top bar inside the /mobile shell. Three slots:
+ * left: WulfMark + "Pulse" wordmark, linked to /mobile/dashboard
+ * right: Bell icon button (placeholder, aria-label="Notifications")
+ * right: compact avatar circle — opens the More drawer (parent owns state)
+ *
+ * No page title in the header — pages render their own H1. */
+
+import Link from 'next/link';
+import { Bell } from 'lucide-react';
+import { WulfMark } from '@/components/branding/wulf-mark';
+import { useSession } from '@/lib/auth-client';
+
+interface HeaderBarProps {
+ onAvatarClick: () => void;
+}
+
+export function HeaderBar({ onAvatarClick }: HeaderBarProps) {
+ const { data: session } = useSession();
+ const user = session?.user as
+ | { name?: string; email?: string }
+ | undefined;
+
+ const initials = (user?.name ?? user?.email ?? '?')
+ .split(/[\s@]/)
+ .filter(Boolean)
+ .slice(0, 2)
+ .map((p) => p[0]?.toUpperCase())
+ .join('');
+
+ return (
+
+
+ {/* Left: brand mark + wordmark, linked to /mobile/dashboard */}
+
+
+ Pulse
+
+
+ {/* Right: Bell placeholder, then avatar trigger */}
+
+
+
+
+
+
+
+ );
+}
+```
+
+Notes:
+- `pt-safe` is added on the sticky header so the notch/dynamic-island doesn't overlap content (SHELL-02 + Phase 1 PWA-04).
+- `bg-background/95 backdrop-blur` matches CONTEXT.md SHELL-02.
+- Bell `onClick` is intentionally empty (SHELL-03 placeholder); future phase wires real notifications.
+- Avatar is `h-7 w-7` per SHELL-04 — wrapped in a `h-9 w-9` button to give a 36px touch target.
+- No `
` / no page title in header (SHELL-02 explicit).
+- We do NOT use the shadcn avatar primitive — initials-circle pattern matches existing `UserMenu`.
+
+ HeaderBar renders WulfMark+wordmark linked to /mobile/dashboard, a Bell button with `aria-label="Notifications"` and empty onClick, and an avatar-circle button that calls `onAvatarClick` (parent wires this to the drawer state).
+
+
+
+ Task 3: Create components/mobile/BottomNav.tsx (5-cell bottom bar — 4 tabs + More)
+ components/mobile/BottomNav.tsx
+
+ - app/mobile/layout.tsx (current 3-tab pattern; we extend to 4 tabs + More)
+ - app/styles/brand.css (confirm `pb-safe` utility exists)
+ - .planning/phases/02-mobile-shell-more-drawer/02-CONTEXT.md (locked: SHELL-06, NAV-01..03 — Dashboard/Tickets/Finance/Analyzer + More)
+
+
+Create the file `components/mobile/BottomNav.tsx`:
+
+```tsx
+'use client';
+
+/* BottomNav — phase 02 (SHELL-06, NAV-01..03, DRAWER-01).
+ *
+ * Fixed bottom bar with five cells:
+ * - Dashboard (LayoutDashboard) -> /mobile/dashboard
+ * - Tickets (Ticket) -> /mobile/tickets
+ * - Finance (DollarSign) -> /mobile/finance
+ * - Analyzer (Sparkles) -> /mobile/analyzer
+ * - More (Menu) -> opens the MoreDrawer (parent state)
+ *
+ * Active tab detected via pathname.startsWith(href). Active = text-primary,
+ * inactive = text-muted-foreground. */
+
+import Link from 'next/link';
+import { usePathname } from 'next/navigation';
+import {
+ LayoutDashboard,
+ Ticket,
+ DollarSign,
+ Sparkles,
+ Menu,
+} from 'lucide-react';
+
+const TABS = [
+ { href: '/mobile/dashboard', label: 'Dashboard', icon: LayoutDashboard },
+ { href: '/mobile/tickets', label: 'Tickets', icon: Ticket },
+ { href: '/mobile/finance', label: 'Finance', icon: DollarSign },
+ { href: '/mobile/analyzer', label: 'Analyzer', icon: Sparkles },
+] as const;
+
+interface BottomNavProps {
+ onMoreClick: () => void;
+}
+
+export function BottomNav({ onMoreClick }: BottomNavProps) {
+ const pathname = usePathname();
+
+ return (
+
+ );
+}
+```
+
+Notes:
+- `max-w-lg mx-auto` keeps the nav width-aligned with the content gutter (SHELL-06 + CONTEXT.md).
+- `pb-safe` on the outer `
+
+ test -f components/mobile/BottomNav.tsx && grep -q "/mobile/dashboard" components/mobile/BottomNav.tsx && grep -q "/mobile/tickets" components/mobile/BottomNav.tsx && grep -q "/mobile/finance" components/mobile/BottomNav.tsx && grep -q "/mobile/analyzer" components/mobile/BottomNav.tsx && grep -q "max-w-lg mx-auto" components/mobile/BottomNav.tsx && grep -q "pathname.*startsWith" components/mobile/BottomNav.tsx && grep -q "text-primary" components/mobile/BottomNav.tsx && grep -q "pb-safe" components/mobile/BottomNav.tsx
+
+
+ - `test -f components/mobile/BottomNav.tsx` exits 0
+ - `grep -E "export function BottomNav" components/mobile/BottomNav.tsx` matches
+ - `grep -E "/mobile/dashboard" components/mobile/BottomNav.tsx` matches AND `grep -E "/mobile/tickets" components/mobile/BottomNav.tsx` matches AND `grep -E "/mobile/finance" components/mobile/BottomNav.tsx` matches AND `grep -E "/mobile/analyzer" components/mobile/BottomNav.tsx` matches (NAV-02)
+ - `grep -E "LayoutDashboard" components/mobile/BottomNav.tsx` matches AND `grep -E "\\bTicket\\b" components/mobile/BottomNav.tsx` matches AND `grep -E "DollarSign" components/mobile/BottomNav.tsx` matches AND `grep -E "Sparkles" components/mobile/BottomNav.tsx` matches AND `grep -E "\\bMenu\\b" components/mobile/BottomNav.tsx` matches (NAV-01 + DRAWER-01 icons)
+ - `grep -E "pathname.*startsWith" components/mobile/BottomNav.tsx` matches (NAV-03)
+ - `grep -E "text-primary" components/mobile/BottomNav.tsx` matches AND `grep -E "text-muted-foreground" components/mobile/BottomNav.tsx` matches (NAV-03 active/inactive)
+ - `grep -E "fixed bottom-0" components/mobile/BottomNav.tsx` matches AND `grep -E "border-t" components/mobile/BottomNav.tsx` matches AND `grep -E "max-w-lg mx-auto" components/mobile/BottomNav.tsx` matches (SHELL-06)
+ - `grep -E "pb-safe" components/mobile/BottomNav.tsx` matches (safe-area for home indicator)
+ - `grep -E "onMoreClick" components/mobile/BottomNav.tsx` matches (DRAWER-01 trigger via parent state)
+
+ BottomNav exports a 5-cell nav: 4 routed Links (Dashboard, Tickets, Finance, Analyzer) with active-state via `pathname.startsWith(href)`, plus a More button that calls `onMoreClick`.
+
+
+
+ Task 4: Create app/mobile/analyzer/page.tsx (placeholder so Analyzer tab does not 404 before Phase 6)
+ app/mobile/analyzer/page.tsx
+
+ - .planning/phases/02-mobile-shell-more-drawer/02-CONTEXT.md (decisions §Routes & files: "Add a placeholder app/mobile/analyzer/page.tsx so the new bottom-nav Analyzer tab doesn't 404 before Phase 6 lands. Minimal 'coming soon' component is sufficient.")
+ - app/mobile/page.tsx (style reference for a minimal mobile page)
+
+
+Create `app/mobile/analyzer/page.tsx`:
+
+```tsx
+/* Placeholder for /mobile/analyzer.
+ *
+ * Phase 02 only adds the Analyzer tab to the bottom nav — the real feed
+ * lands in Phase 6 (`docs/superpowers/specs/2026-05-03-mobile-shell-design.md`
+ * §6.4). This file exists so tapping the Analyzer tab resolves to a real
+ * route instead of 404. Phase 6 will replace this file with the actual
+ * read-only feed page.
+ *
+ * DO NOT add features, data fetching, or UI beyond the "Coming soon"
+ * card here — Phase 6 owns the real implementation. */
+
+import { Sparkles } from 'lucide-react';
+
+export const metadata = {
+ title: 'Analyzer · Pulse',
+};
+
+export default function MobileAnalyzerPlaceholder() {
+ return (
+
+
+
+
+
+
Analyzer feed coming soon
+
+ The mobile Analyzer feed is on its way. Until then, view full
+ analyses on the desktop Analyzer.
+
+
+
+ );
+}
+```
+
+Notes:
+- This is a server component (no `'use client'` needed) — keeps it cheap.
+- Deliberately stubbed; Phase 6 (ANL-01..06) replaces this entire file.
+- No data fetching, no `/api/mobile/analyzer/feed` call — those belong in Phase 6.
+
+
+ test -f app/mobile/analyzer/page.tsx && grep -q "export default function" app/mobile/analyzer/page.tsx && grep -q "coming soon" app/mobile/analyzer/page.tsx
+
+
+ - `test -f app/mobile/analyzer/page.tsx` exits 0
+ - `grep -E "export default function" app/mobile/analyzer/page.tsx` matches
+ - `grep -iE "coming soon" app/mobile/analyzer/page.tsx` matches (placeholder copy present)
+ - `! grep -E "/api/mobile/analyzer" app/mobile/analyzer/page.tsx` exits 0 (no Phase 6 data fetching)
+
+ Visiting `/mobile/analyzer` after build renders a small "coming soon" card; no 404.
+
+
+
+ Task 5: Type-check and build to confirm new components compile cleanly without breaking anything
+ (no files written — gate task)
+
+ - components/mobile/HeaderBar.tsx (just authored)
+ - components/mobile/BottomNav.tsx (just authored)
+ - components/mobile/MoreDrawer.tsx (just authored)
+ - app/mobile/analyzer/page.tsx (just authored)
+
+
+Run `npx tsc --noEmit --pretty` and `npm run build` to confirm the four new files compile in the existing project. The current `app/mobile/layout.tsx` and `app/mobile/nav/page.tsx` are untouched, so existing routes must still build.
+
+If `tsc` reports errors, fix them in the offending file(s) and rerun until both pass. Common issues to expect:
+- Missing import → re-add the import
+- `any` cast on `session.user` → keep the typed cast pattern from `UserMenu.tsx`
+- JSX-runtime / `JSX` namespace not found → not expected (tsconfig has it); if it appears, leave it for the executor to investigate
+
+Do **not** modify any other files in this task.
+
+
+ npx tsc --noEmit --pretty && npm run build
+
+
+ - `npx tsc --noEmit --pretty` exits 0
+ - `npm run build` exits 0
+ - The four new files exist (re-confirmed) and no existing file was modified by this task: `git status --short components/mobile app/mobile/analyzer` shows only the four new files, no modifications to anything else
+
+ TypeScript and Next.js build both pass with the four new files in place; existing routes unchanged.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Browser → drawer Sign out | Calls `signOut()` on the existing Better Auth client; an authenticated session already exists |
+| Browser → all Link routes | Standard client-side navigation; no new endpoints, no new data |
+| Browser → header Bell | Empty handler (placeholder per SHELL-03); not a trust boundary in this iteration |
+
+## STRIDE Threat Register (ASVS-L1 baseline)
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-02-01 | Tampering | MoreDrawer Sign out button | accept | No new endpoint introduced; reuses Better Auth `signOut()` from `lib/auth-client.ts`. CSRF protection is provided by the existing Better Auth cookie + SameSite policy. |
+| T-02-02 | Information Disclosure | Account section showing user email | accept | Email is already visible in the existing top-bar `UserMenu` on every desktop page; no new data surface or endpoint. Read-only display only. |
+| T-02-03 | Spoofing | Avatar trigger opens drawer that contains Sign out | mitigate | Drawer state is local React state, not URL-driven; an attacker cannot pre-open the drawer via crafted URL. Sign out always navigates to `/auth/sign-in` server-rendered route, which Better Auth controls. |
+| T-02-04 | Denial of Service | Bell button placeholder | accept | Empty handler — no fetch, no work, no DOS surface. Phase 7+ will revisit when the real notification list ships (NOTIF-01). |
+
+
+
+After this plan completes:
+
+1. The four new files exist:
+ - `test -f components/mobile/HeaderBar.tsx`
+ - `test -f components/mobile/BottomNav.tsx`
+ - `test -f components/mobile/MoreDrawer.tsx`
+ - `test -f app/mobile/analyzer/page.tsx`
+2. `npx tsc --noEmit --pretty` exits 0
+3. `npm run build` exits 0
+4. `app/mobile/layout.tsx` is unchanged from start (still imports `LayoutDashboard, Ticket, DollarSign, Menu` only — not `Sparkles`):
+ - `! grep -E "Sparkles" app/mobile/layout.tsx` exits 0 (we have NOT yet wired the new bottom nav — Plan 02 does that)
+5. `app/mobile/nav/page.tsx` still exists (Plan 02 deletes it)
+6. No new dependencies added: `git diff package.json package-lock.json` is empty
+
+
+
+- All 5 tasks complete
+- 4 new files exist (HeaderBar, BottomNav, MoreDrawer, analyzer placeholder)
+- Each component matches its locked decisions from CONTEXT.md (D-locked: side="right", 5-cell nav, three drawer sections, Bell placeholder, h-7 w-7 avatar, max-w-lg mx-auto bottom nav)
+- TypeScript + Next build both pass
+- No existing files modified by this plan (verifiable via `git status --short`)
+- Plan 02 will pick up these components and wire them into the layout
+
+
+
diff --git a/.planning/phases/02-mobile-shell-more-drawer/02-02-PLAN.md b/.planning/phases/02-mobile-shell-more-drawer/02-02-PLAN.md
new file mode 100644
index 0000000..1c59351
--- /dev/null
+++ b/.planning/phases/02-mobile-shell-more-drawer/02-02-PLAN.md
@@ -0,0 +1,406 @@
+---
+phase: 02-mobile-shell-more-drawer
+plan: 02
+type: execute
+wave: 2
+depends_on:
+ - 02-01
+files_modified:
+ - app/mobile/layout.tsx
+ - app/mobile/nav/page.tsx
+autonomous: false
+requirements:
+ - SHELL-01
+ - SHELL-05
+ - DRAWER-06
+
+must_haves:
+ truths:
+ - "app/mobile/layout.tsx is rewritten to import HeaderBar, BottomNav, MoreDrawer from components/mobile/* and renders them around {children}"
+ - "The layout owns a single React.useState boolean that opens/closes the MoreDrawer; HeaderBar's onAvatarClick and BottomNav's onMoreClick both flip this state to true"
+ - "The content area scrolls and has bottom padding equal to bottom-nav height (h-16 = 64px) plus env(safe-area-inset-bottom) so content does not hide under the nav"
+ - "app/mobile/nav/page.tsx no longer exists — the file is deleted in this same change"
+ - "Visiting /mobile/dashboard, /mobile/tickets, /mobile/finance, and /mobile/analyzer all render inside the new layout (header + bottom nav visible, no 404)"
+ - "TypeScript compiles (npx tsc --noEmit) and Next.js builds (npm run build) successfully"
+ artifacts:
+ - path: "app/mobile/layout.tsx"
+ provides: "New mobile shell wiring HeaderBar + BottomNav + MoreDrawer with shared drawer state"
+ contains: "MoreDrawer"
+ - path: "app/mobile/nav/page.tsx"
+ provides: "DELETED — drawer fully replaces the standalone nav page (DRAWER-06)"
+ deleted: true
+ key_links:
+ - from: "app/mobile/layout.tsx"
+ to: "components/mobile/HeaderBar.tsx"
+ via: "import + render with onAvatarClick"
+ pattern: "from ['\"]@/components/mobile/HeaderBar['\"]"
+ - from: "app/mobile/layout.tsx"
+ to: "components/mobile/BottomNav.tsx"
+ via: "import + render with onMoreClick"
+ pattern: "from ['\"]@/components/mobile/BottomNav['\"]"
+ - from: "app/mobile/layout.tsx"
+ to: "components/mobile/MoreDrawer.tsx"
+ via: "import + render with shared open/onOpenChange state"
+ pattern: "from ['\"]@/components/mobile/MoreDrawer['\"]"
+ - from: "Header avatar AND Bottom-nav More button"
+ to: "MoreDrawer open state"
+ via: "Single useState in app/mobile/layout.tsx"
+ pattern: "useState"
+---
+
+
+Replace `app/mobile/layout.tsx` with the new shell that wires HeaderBar + BottomNav + MoreDrawer (built in Plan 01) around `{children}`, owning a single shared drawer-open state. Delete `app/mobile/nav/page.tsx` in the same change so the drawer fully replaces the old standalone nav page.
+
+Purpose: Land SHELL-01 (replace in place), SHELL-05 (scrollable content with bottom-nav-aware padding), and DRAWER-06 (delete the old nav route). After this plan, every `/mobile/*` page renders under the new shell and the four primary tabs + avatar + More all behave per spec.
+
+Output: Modified `app/mobile/layout.tsx`, deleted `app/mobile/nav/page.tsx`. Build passes. Visual checkpoint confirms the shell renders correctly on at least one mobile route.
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/ROADMAP.md
+@.planning/REQUIREMENTS.md
+@.planning/phases/02-mobile-shell-more-drawer/02-CONTEXT.md
+@.planning/phases/02-mobile-shell-more-drawer/02-01-PLAN.md
+@docs/superpowers/specs/2026-05-03-mobile-shell-design.md
+@CLAUDE.md
+@app/mobile/layout.tsx
+@app/mobile/nav/page.tsx
+@app/styles/brand.css
+
+
+
+
+From components/mobile/HeaderBar.tsx (Plan 01):
+```typescript
+export function HeaderBar(props: { onAvatarClick: () => void }): JSX.Element;
+```
+
+From components/mobile/BottomNav.tsx (Plan 01):
+```typescript
+export function BottomNav(props: { onMoreClick: () => void }): JSX.Element;
+```
+
+From components/mobile/MoreDrawer.tsx (Plan 01):
+```typescript
+export function MoreDrawer(props: {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}): JSX.Element;
+```
+
+CSS utilities available in `app/styles/brand.css`:
+- `pt-safe`, `pb-safe`
+
+
+
+This plan **only** touches:
+- `app/mobile/layout.tsx` (full rewrite)
+- `app/mobile/nav/page.tsx` (delete)
+
+Do NOT modify:
+- The three new components (Plan 01 owns them)
+- Any page under `app/mobile/dashboard|tickets|finance|analyzer|page.tsx` (out of phase)
+- `components/navigation/app-navigation.tsx` (desktop nav)
+- `app/layout.tsx` (root, owned by Phase 1)
+
+
+
+
+
+
+ Task 1: Rewrite app/mobile/layout.tsx to wire HeaderBar + BottomNav + MoreDrawer with shared state
+ app/mobile/layout.tsx
+
+ - app/mobile/layout.tsx (current 3-tab layout being replaced — read fully so executor knows what's there)
+ - components/mobile/HeaderBar.tsx (Plan 01 output — confirms onAvatarClick prop)
+ - components/mobile/BottomNav.tsx (Plan 01 output — confirms onMoreClick prop)
+ - components/mobile/MoreDrawer.tsx (Plan 01 output — confirms open/onOpenChange props)
+ - app/styles/brand.css (confirm `pt-safe` and `pb-safe` are available)
+ - .planning/phases/02-mobile-shell-more-drawer/02-CONTEXT.md (SHELL-05: bottom padding = nav height + safe-area)
+
+
+**Replace the entire contents** of `app/mobile/layout.tsx` with:
+
+```tsx
+'use client';
+
+/* Mobile shell — phase 02 (SHELL-01, SHELL-05).
+ *
+ * Header: (sticky, brand + Bell + avatar)
+ * Body: (scrollable, padded so content clears the bottom nav)
+ * Foot: (fixed, 4 tabs + More)
+ * Drawer: opened from BOTH the header avatar and the More cell.
+ *
+ * The drawer's open state lives here so a single Sheet instance is shared
+ * between the two triggers — no duplicate Sheets, no prop-drilling sagas. */
+
+import { useState } from 'react';
+import { HeaderBar } from '@/components/mobile/HeaderBar';
+import { BottomNav } from '@/components/mobile/BottomNav';
+import { MoreDrawer } from '@/components/mobile/MoreDrawer';
+
+export default function MobileLayout({ children }: { children: React.ReactNode }) {
+ const [drawerOpen, setDrawerOpen] = useState(false);
+
+ return (
+
+ setDrawerOpen(true)} />
+
+ {/* SHELL-05: scrollable content area; bottom padding = bottom-nav (h-16
+ = 64px = pb-16) plus the device safe-area inset, so content never
+ hides under the bar. */}
+
+ {children}
+
+
+ setDrawerOpen(true)} />
+
+
+
+ );
+}
+```
+
+Notes:
+- This file replaces the existing 3-tab layout entirely. No legacy imports, no dead code paths.
+- `'use client'` is required because we use `useState`.
+- The `pb-[calc(...)]` arbitrary value gives `` enough bottom padding to clear the 64px nav plus the home-indicator inset (SHELL-05 + PWA-04 reuse). Tailwind 4 supports the `calc()` arbitrary value here.
+- A single `useState` is the entire shared-state mechanism — no Zustand, no Context, no third-party state lib (per CLAUDE.md "no new state libraries").
+- Both triggers set the same boolean. The `MoreDrawer` itself controls its close (Radix `onOpenChange` fires when overlay is clicked or Esc is pressed) and propagates back through `setDrawerOpen`.
+
+
+ grep -q "from '@/components/mobile/HeaderBar'" app/mobile/layout.tsx && grep -q "from '@/components/mobile/BottomNav'" app/mobile/layout.tsx && grep -q "from '@/components/mobile/MoreDrawer'" app/mobile/layout.tsx && grep -q "useState" app/mobile/layout.tsx && grep -q "onAvatarClick" app/mobile/layout.tsx && grep -q "onMoreClick" app/mobile/layout.tsx && grep -q "drawerOpen" app/mobile/layout.tsx && grep -q "safe-area-inset-bottom" app/mobile/layout.tsx
+
+
+ - `grep -E "from ['\"]@/components/mobile/HeaderBar['\"]" app/mobile/layout.tsx` matches
+ - `grep -E "from ['\"]@/components/mobile/BottomNav['\"]" app/mobile/layout.tsx` matches
+ - `grep -E "from ['\"]@/components/mobile/MoreDrawer['\"]" app/mobile/layout.tsx` matches
+ - `grep -E "useState" app/mobile/layout.tsx` matches (single shared state)
+ - `grep -E "onAvatarClick" app/mobile/layout.tsx` matches AND `grep -E "onMoreClick" app/mobile/layout.tsx` matches (both triggers wired)
+ - `grep -E "open=" app/mobile/layout.tsx` matches AND `grep -E "onOpenChange=" app/mobile/layout.tsx` matches (drawer controlled)
+ - `grep -E "safe-area-inset-bottom" app/mobile/layout.tsx` matches (SHELL-05 padding for bottom nav clearance)
+ - `grep -E "max-w-lg mx-auto" app/mobile/layout.tsx` matches (CONTEXT.md container width)
+ - `! grep -E "Menu, " app/mobile/layout.tsx` exits 0 (the legacy `Menu`-as-link import from the old layout is gone)
+ - `! grep -E "/mobile/nav" app/mobile/layout.tsx` exits 0 (no link to the deleted standalone nav route)
+
+ The layout renders the three new components, owns a single useState for drawer open/close, and pads `` to clear the bottom nav + safe area.
+
+
+
+ Task 2: Delete app/mobile/nav/page.tsx (DRAWER-06)
+ app/mobile/nav/page.tsx
+
+ - app/mobile/nav/page.tsx (final read of the file being deleted, so the executor knows what is leaving the codebase)
+ - .planning/phases/02-mobile-shell-more-drawer/02-CONTEXT.md (DRAWER-06 — delete in same change as drawer ships; no redirect)
+ - components/mobile/MoreDrawer.tsx (Plan 01 output — verifies the drawer already covers everything the old page did)
+
+
+Delete the file:
+
+```bash
+rm app/mobile/nav/page.tsx
+```
+
+Then check the directory is empty (or only contains other files we don't care about) and remove it if it became empty:
+
+```bash
+# If app/mobile/nav is now empty, remove the directory too.
+if [ -d app/mobile/nav ] && [ -z "$(ls -A app/mobile/nav)" ]; then
+ rmdir app/mobile/nav
+fi
+```
+
+Notes:
+- Per CONTEXT.md DRAWER-06: "Recommend NO redirect (just delete) — the URL was never bookmarked-worthy." Visiting `/mobile/nav` after this change yields Next.js's standard 404, which is the desired behavior.
+- Confirm no other file in the repo references `/mobile/nav` or imports from `app/mobile/nav/...`. Run a quick grep before deletion (the old `app/mobile/layout.tsx` had the only known reference, and Task 1 already removed it).
+
+
+ ! test -f app/mobile/nav/page.tsx
+
+
+ - `! test -f app/mobile/nav/page.tsx` exits 0 (file deleted)
+ - `! grep -r --include="*.ts" --include="*.tsx" "/mobile/nav" app components 2>/dev/null` exits 0 (no remaining references in source)
+ - The deletion shows up in `git status` as a deleted file
+
+ `app/mobile/nav/page.tsx` no longer exists; no source file references `/mobile/nav` anywhere.
+
+
+
+ Task 3: Type-check and full build to confirm the new shell compiles end-to-end
+ (no files written — gate task)
+
+ - app/mobile/layout.tsx (the file just rewritten)
+ - components/mobile/HeaderBar.tsx (Plan 01)
+ - components/mobile/BottomNav.tsx (Plan 01)
+ - components/mobile/MoreDrawer.tsx (Plan 01)
+
+
+Run:
+
+```bash
+npx tsc --noEmit --pretty
+npm run build
+```
+
+Both must exit 0. If either fails, fix the offending file and rerun until clean. Common things to check if it fails:
+
+- Did Task 2 leave a dangling import to the deleted `nav/page.tsx`? (Should be impossible, but grep `/mobile/nav` if a build error names that path.)
+- Did the `'use client'` directive end up below an import? (Must be the very first line.)
+- Is the `pb-[calc(theme(spacing.16)+env(safe-area-inset-bottom))]` Tailwind 4 arbitrary value valid? If Tailwind rejects it, fall back to an inline style on the ``: `style={{ paddingBottom: 'calc(4rem + env(safe-area-inset-bottom))' }}` and remove the `pb-[...]` class.
+
+Do NOT modify any file other than `app/mobile/layout.tsx` to fix build issues.
+
+
+ npx tsc --noEmit --pretty && npm run build
+
+
+ - `npx tsc --noEmit --pretty` exits 0
+ - `npm run build` exits 0
+ - `git status --short app components` shows: 1 modified (`app/mobile/layout.tsx`) and 1 deleted (`app/mobile/nav/page.tsx`); no other unexpected modifications
+
+ Type-check + build both pass with the new shell wired and the old nav page deleted.
+
+
+
+ Task 4: Visual verification of the new mobile shell on a real device or DevTools mobile preview
+ (no files written — human checkpoint)
+
+ - app/mobile/layout.tsx (the file just rewritten — executor confirms what was shipped)
+ - .planning/ROADMAP.md (Phase 2 success criteria #1–6 — these are what the human is verifying)
+ - .planning/REQUIREMENTS.md (SHELL-01..06, NAV-01..03, DRAWER-01..06)
+
+
+Pause and surface a checkpoint to the user. Present this exact verification script and wait for the user's "approved" response.
+
+**What was built (summary for the user):**
+The new mobile shell is fully wired:
+- `app/mobile/layout.tsx` rewritten — sticky header (Wulf mark + "Pulse" wordmark, Bell, avatar), scrollable ``, fixed bottom nav (Dashboard / Tickets / Finance / Analyzer / More)
+- `app/mobile/nav/page.tsx` deleted
+- `app/mobile/analyzer/page.tsx` placeholder ("Coming soon" card) so the Analyzer tab resolves until Phase 6
+- `` opens from BOTH the header avatar and the bottom-nav More button, with three sections (Mobile sections / Full site / Account + Sign out)
+
+All built on existing shadcn primitives, the `WulfMark` component, and Better Auth's `signOut()` — no new state libs, no shadcn avatar primitive added.
+
+**How to verify (user runs through this on a phone-sized viewport):**
+
+1. **Start the dev server** if not already running: `npm run dev`. Pulse should start on http://localhost:3100.
+
+2. **Open the mobile shell in a phone-sized viewport** — Chrome DevTools (F12) → toggle device toolbar (Ctrl+Shift+M / Cmd+Shift+M) → pick "iPhone 15 Pro" or any 390-414px wide device. Visit `http://localhost:3100/mobile/dashboard`.
+
+3. **Verify the header (SHELL-02..04):**
+ - [ ] Sticky bar at the top with `bg-background/95 backdrop-blur` + bottom border
+ - [ ] Left side: Wulf "W" mark + "Pulse" wordmark; tapping it navigates to `/mobile/dashboard`
+ - [ ] Right side: a Bell icon button next to a small avatar circle (initials)
+ - [ ] No page title text in the header itself
+ - [ ] Bell button is keyboard-focusable (Tab to it, then Space/Enter — should not throw or navigate; it's a placeholder, no menu)
+ - [ ] Tapping the avatar opens the right-side Sheet drawer
+
+4. **Verify the bottom nav (SHELL-06, NAV-01..03):**
+ - [ ] Fixed bar at the bottom, full width, `border-t bg-background`
+ - [ ] Five cells in order: Dashboard, Tickets, Finance, Analyzer, More
+ - [ ] Active tab uses `text-primary` (Wulf blue); inactive use `text-muted-foreground`
+ - [ ] Tapping each tab routes to its URL: `/mobile/dashboard`, `/mobile/tickets`, `/mobile/finance`, `/mobile/analyzer`
+ - [ ] Tapping a row INSIDE `/mobile/tickets/[id]` (e.g., open any ticket) keeps Tickets highlighted (active detection via `pathname.startsWith`)
+ - [ ] Tapping More opens the same drawer the avatar opens
+ - [ ] Visiting `/mobile/analyzer` shows the "Coming soon" placeholder card (NOT a 404)
+
+5. **Verify the drawer (DRAWER-01..05):**
+ Open the drawer (avatar OR More).
+ - [ ] Drawer slides in from the right (`side="right"`)
+ - [ ] Section 1 "Mobile sections" — single row: Engagement (no `ExternalLink` hint icon)
+ - [ ] Section 2 "Full site" — five rows: Quotes, Configuration Items, Backup Status, Ticket Digest, Admin / Sync (each row has the `ExternalLink` icon on the right)
+ - [ ] Section 3 "Account" — shows the signed-in user's initials, name, and email; below it, a red "Sign out" button
+ - [ ] Tapping any row inside the drawer navigates AND closes the drawer
+ - [ ] Tapping the X / outside the drawer / pressing Esc closes it
+ - [ ] Sign out: tap it → page navigates to `/auth/sign-in` AND the user is signed out (refreshing brings you to the sign-in page; no auto-redirect to `/mobile`)
+
+6. **Verify content does not hide under the bottom nav (SHELL-05):**
+ - [ ] On `/mobile/dashboard` (or any mobile page), scroll to the bottom of the content. The last visible content sits ABOVE the bottom nav, not under it.
+ - [ ] On a phone with a home indicator (or in DevTools with iPhone preset), the bottom nav has extra space below for the indicator inset (no overlap).
+
+7. **Verify the old nav route is gone (DRAWER-06):**
+ - [ ] Visit `http://localhost:3100/mobile/nav` directly. It returns Next.js's 404 page (NOT the old standalone nav UI).
+
+8. **Quick regression on existing pages:**
+ - [ ] `/mobile/dashboard`, `/mobile/tickets`, `/mobile/finance` all still render their previous content unchanged — only the chrome around them is new.
+ - [ ] `/mobile/tickets/[id]` (open a ticket) still renders inside the new shell.
+
+**Resume signal:** Reply "approved" once all checks pass. If something is broken or off-spec, describe what you saw and which check failed (e.g., "Drawer opens from the bottom, not the right" or "Bottom nav overlaps the last content row on /mobile/finance"). The executor will fix and re-verify.
+
+
+ echo "Manual verification — user must reply 'approved' or describe a failure. No automated check applicable; preceding tasks (1-3) verify code-level invariants."
+
+
+ - User replies "approved" after running the verification script above
+ - All 8 verification sections pass on the user's device/preview
+ - If any check fails, the executor returns to Task 1 or Task 2 to fix and re-runs Task 3 (build) and Task 4 (re-verify) before requesting approval again
+
+ User has explicitly replied "approved", confirming the new shell renders correctly on a phone-sized viewport and all 6 ROADMAP success criteria for Phase 2 are met.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Browser → drawer Sign out | Reuses Plan 01's MoreDrawer; calls Better Auth `signOut()` and navigates to `/auth/sign-in` — same trust boundary as the existing top-bar `UserMenu`. |
+| Browser → all Link routes | All routes already exist or are placeholders (`/mobile/analyzer` placeholder shipped in Plan 01). No new endpoints. |
+
+## STRIDE Threat Register (ASVS-L1 baseline)
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-02-05 | Tampering | layout.tsx drawer-state useState | accept | Local React state, not URL-driven. An attacker cannot pre-open the drawer via crafted URL. State has no security relevance — it merely toggles UI visibility. |
+| T-02-06 | Information Disclosure | Deletion of `/mobile/nav` route | accept | The deleted page surfaced no PII beyond what the new drawer surfaces (same email field). Net change: identical surface area. |
+| T-02-07 | Denial of Service | New shell mounts on every `/mobile/*` request | accept | Layout is lightweight: 1 useState, 3 component imports, no fetches. Cost is negligible vs. the existing layout. |
+| T-02-08 | Repudiation | Sign out action | mitigate | Better Auth records sign-out in its session table; not a Pulse-introduced repudiation surface. Inherited from `lib/auth-client.ts`. |
+
+
+
+After this plan completes:
+
+1. `app/mobile/layout.tsx` imports HeaderBar, BottomNav, MoreDrawer:
+ - `grep -E "@/components/mobile/HeaderBar" app/mobile/layout.tsx` matches
+ - `grep -E "@/components/mobile/BottomNav" app/mobile/layout.tsx` matches
+ - `grep -E "@/components/mobile/MoreDrawer" app/mobile/layout.tsx` matches
+2. `app/mobile/nav/page.tsx` does not exist:
+ - `! test -f app/mobile/nav/page.tsx` exits 0
+3. No source file references `/mobile/nav`:
+ - `! grep -r --include="*.ts" --include="*.tsx" "/mobile/nav" app components 2>/dev/null` exits 0
+4. Build is clean:
+ - `npx tsc --noEmit --pretty` exits 0
+ - `npm run build` exits 0
+5. Visual checkpoint passed (Task 4):
+ - Header sticky, brand link goes to `/mobile/dashboard`, Bell focusable with no menu, avatar opens drawer
+ - Bottom nav shows 5 cells with correct icons + routes; active tab uses `text-primary`; `/mobile/tickets/123` highlights Tickets
+ - Drawer (`side="right"`) shows 3 sections; Sign out signs out and lands on `/auth/sign-in`
+ - `/mobile/analyzer` renders the placeholder, NOT a 404
+ - `/mobile/nav` returns 404
+ - Content does not hide under the bottom nav (SHELL-05)
+
+
+
+- All 4 tasks complete (3 auto + 1 visual checkpoint with explicit "approved")
+- `app/mobile/layout.tsx` is rewritten to use the new components with shared drawer state
+- `app/mobile/nav/page.tsx` is deleted
+- Type-check + build both pass
+- Visual checkpoint approved by user
+- Phase 2 ROADMAP success criteria #1–6 are all satisfied (header, 5-cell nav, drawer with 3 sections, sign-out flow, /mobile/nav gone, content not hidden under bar)
+- All 15 phase requirements (SHELL-01..06, NAV-01..03, DRAWER-01..06) are now closed across Plan 01 + Plan 02
+
+
+