diff --git a/.planning/phases/04-tickets-restyle/04-HUMAN-UAT.md b/.planning/phases/04-tickets-restyle/04-HUMAN-UAT.md
new file mode 100644
index 0000000..343e2a9
--- /dev/null
+++ b/.planning/phases/04-tickets-restyle/04-HUMAN-UAT.md
@@ -0,0 +1,74 @@
+---
+status: partial
+phase: 04-tickets-restyle
+source: [04-VERIFICATION.md]
+started: 2026-05-03T00:00:00Z
+updated: 2026-05-03T00:00:00Z
+---
+
+## Current Test
+
+[awaiting human testing]
+
+## Tests
+
+### 1. Filter strip default-collapsed and expand/collapse
+expected: |
+ Open `/mobile/tickets` on iPhone 15 Pro emulation (393×852).
+ - On load: filter strip is collapsed (only "Filters" toggle and search visible)
+ - Tap "Filters" → strip expands smoothly revealing status chips, priority chips, queue Select, and "Assigned to me" Switch
+ - Tap "Filters" again → collapses
+result: [pending]
+
+### 2. URL deep-link hydration
+expected: |
+ - Navigate to `/mobile/tickets?priority=2&mine=1` directly
+ - Filters hydrate: "High" priority chip selected, "Assigned to me" toggled on
+ - Adjust a filter — URL updates via router.replace (no new history entry)
+ - Browser back exits the page rather than reverting the filter mutation
+result: [pending]
+
+### 3. IntersectionObserver infinite scroll
+expected: |
+ - Load page, scroll to bottom of first ~25 rows
+ - Next page (~25 more) auto-loads when last row is ~200px from viewport
+ - No double-fetch when scrolling fast
+ - When `nextCursor` is null, no further fetches occur
+result: [pending]
+
+### 4. Load more fallback button (accessibility)
+expected: |
+ - Tab key reaches the "Load more" button below the sentinel
+ - Press Enter or Space triggers the next page fetch
+ - Button hides when `hasMore` is false
+result: [pending]
+
+### 5. Priority stripe colors render correctly
+expected: |
+ - Critical (priority=1) row has a red left border (`border-red-500`)
+ - High (priority=2) → orange left border
+ - Medium (priority=3) → amber left border
+ - Low (priority=4) → slate left border
+ - Confirms Tailwind didn't purge dynamic priority classes
+result: [pending]
+
+### 6. Detail page header layout
+expected: |
+ - Tap a ticket row → navigate to `/mobile/tickets/[id]`
+ - Shell HeaderBar (Wulf mark + bell + avatar) renders at top
+ - Below it: new in-page header with back chevron + "Tickets" + breadcrumb "Tickets / #{number}" + external-link icon
+ - Tapping back chevron returns to list (preserving filter URL state)
+ - Tapping external-link icon opens desktop ticket page in new tab
+ - No double-border or visual overlap between the two header levels
+result: [pending]
+
+## Summary
+
+total: 6
+passed: 0
+issues: 0
+pending: 6
+skipped: 0
+blocked: 0
+
+## Gaps
diff --git a/.planning/phases/04-tickets-restyle/04-VERIFICATION.md b/.planning/phases/04-tickets-restyle/04-VERIFICATION.md
new file mode 100644
index 0000000..89648b5
--- /dev/null
+++ b/.planning/phases/04-tickets-restyle/04-VERIFICATION.md
@@ -0,0 +1,206 @@
+---
+phase: 04-tickets-restyle
+verified: 2026-05-03T22:15:02Z
+status: human_needed
+score: 5/5 must-haves verified
+gaps: []
+human_verification:
+ - test: "Filter strip collapse/expand on real device or DevTools phone emulation"
+ expected: "Opening /mobile/tickets shows collapsed filter strip (search input + Filters button only); tapping Filters reveals status chips, priority chips, queue Select, and Assigned-to-me Switch"
+ why_human: "Collapsible open/close behavior and touch interaction require a browser with rendered DOM — cannot be verified from source code alone"
+ - test: "URL deep-link round-trip on reload"
+ expected: "Navigating to /mobile/tickets?priority=2 hydrates the filter strip with High selected; the list shows only priority-2 tickets; back button leaves /mobile/tickets rather than reverting filters"
+ why_human: "router.replace history behavior and URL param hydration require a running Next.js app in a real browser"
+ - test: "IntersectionObserver infinite scroll trigger"
+ expected: "Scrolling to within ~200px of the list end automatically fetches the next 25 tickets and appends them without a page navigation"
+ why_human: "IntersectionObserver is browser-native; rootMargin behavior cannot be simulated from static code inspection"
+ - test: "Load more button keyboard accessibility"
+ expected: "Tab key reaches the Load more button when hasMore is true; pressing Enter/Space triggers the next page fetch"
+ why_human: "Focus management and keyboard interaction require a rendered browser"
+ - test: "Priority stripe colors rendered correctly per priority value"
+ expected: "Priority 1 rows show red left border, priority 2 orange, priority 3 amber, priority 4 slate — matching the locked D-15 color map"
+ why_human: "Tailwind class purge in production build could drop dynamic class strings unless all four are explicitly referenced — visual confirmation validates they render"
+ - test: "Detail page three-slot header renders correctly under shell HeaderBar"
+ expected: "On /mobile/tickets/[id] the Wulf mark + Bell + avatar HeaderBar renders at top, then the in-page header with ArrowLeft+Tickets / breadcrumb / ExternalLink icon below it, then the unchanged body"
+ why_human: "Nested header layout and safe-area spacing require a rendered browser to confirm no visual overlap or double-border"
+---
+
+# Phase 4: Tickets Restyle Verification Report
+
+**Phase Goal:** A manager triages tickets on a phone with a collapsible filter bar that deep-links via URL, priority-coloured rows, and infinite scroll — and the detail page header matches the new shell.
+**Verified:** 2026-05-03T22:15:02Z
+**Status:** human_needed
+**Re-verification:** No — initial verification
+
+## Goal Achievement
+
+### Observable Truths (from Roadmap Success Criteria)
+
+| # | Truth | Status | Evidence |
+|---|-------|--------|----------|
+| 1 | Tickets page opens with filter strip collapsed; expanding reveals status, priority, queue, mine toggle; changing any filter updates the URL (deep link works on reload) | ✓ VERIFIED | `TicketFilterStrip` uses `useState(false)` as Collapsible default; `router.replace()` called in filter effect; `parseFilterFromSearch()` hydrates state from `useSearchParams()` on mount; `filterToSearch()` serializes all 5 params |
+| 2 | Each list row has a left-edge stripe matching priority (Critical/High/Medium/Low → red/orange/amber/slate) and shows ticket #, title, company, age, and assignee | ✓ VERIFIED | `PRIORITY_BORDER` record maps `{1: 'border-red-500', 2: 'border-orange-400', 3: 'border-amber-400', 4: 'border-slate-300'}`; `border-l-4` applied to Link wrapper; row renders `ticket_number`, `title`, `company_name`, `relTime(last_activity_date)`, initials avatar from `assigned_to` |
+| 3 | Single-tapping a row navigates to `/mobile/tickets/[id]` | ✓ VERIFIED | Each row is a `` — native Next.js navigation |
+| 4 | Scrolling to the bottom auto-loads next ~25 rows (no Next button); a Load more fallback button is visible/focusable for accessibility | ✓ VERIFIED | `IntersectionObserver` with `rootMargin: '200px'` on `sentinelRef`; guard `loadingMore \|\| !hasMore`; focusable `