diff --git a/.planning/phases/04-tickets-restyle/04-CONTEXT.md b/.planning/phases/04-tickets-restyle/04-CONTEXT.md
new file mode 100644
index 0000000..c2256be
--- /dev/null
+++ b/.planning/phases/04-tickets-restyle/04-CONTEXT.md
@@ -0,0 +1,142 @@
+# Phase 4: Tickets Restyle - Context
+
+**Gathered:** 2026-05-03 (auto mode)
+**Status:** Ready for planning
+
+
+## Phase Boundary
+
+Reskin the mobile Tickets surfaces (`/mobile/tickets` list + `/mobile/tickets/[id]` detail header) to match the new shell. Replace the current sticky search/filter bar with a Collapsible URL-synced filter strip. Replace page-based pagination (`?page=N`, 30/page) with cursor-based infinite scroll (~25/page, IntersectionObserver) plus a "Load more" fallback. Add priority left-edge stripes to list rows. Detail page body stays largely as-is — only its header is reskinned.
+
+In scope: list page UI + filter strip + URL deep-linking + cursor pagination API change + detail header reskin.
+
+Out of scope: filter set expansion (only the four spec'd filters), detail body refactor, comment/attach UI changes, search UX overhaul beyond what existing search input provides.
+
+
+
+## Implementation Decisions
+
+### Filter strip
+- **D-01:** Use shadcn `Collapsible` component for the filter strip; default state is **collapsed** (only the search input + "Filters" toggle button visible). Reason: TICK-01 spec says default collapsed; matches phone-first density goal.
+- **D-02:** Expanded panel exposes exactly four controls: status (multi-select chips: Open/In Progress/Waiting), priority (chips: Critical/High/Medium/Low), queue (Select dropdown sourced from existing queues), and an "Assigned to me" toggle. No additional filter capabilities this phase.
+- **D-03:** Keep the existing search input visible at all times (above the Collapsible toggle), debounced 400ms — match current behavior.
+- **D-04:** "Clear all" button appears in the expanded panel when ≥1 filter is active.
+
+### URL sync
+- **D-05:** Use `useSearchParams()` + `router.replace()` (NOT `router.push()`) to update query string on filter changes. Reason: replace prevents back-button pollution from filter tweaks; matches Next.js App Router convention.
+- **D-06:** URL param keys: `q` (search), `status` (comma-separated ints), `priority` (comma-separated ints), `queue` (int), `mine` (`1`/absent). On reload, page hydrates filter state from these params — deep link works.
+- **D-07:** Cursor (`cursor`) is intentionally **not** persisted to URL — fresh visits always start at the top of the list.
+
+### Pagination — cursor model
+- **D-08:** Replace `?page=N&limit=30` with `?cursor=&limit=25`. Reason: TICK-05 mandates cursor-based ~25/page.
+- **D-09:** Cursor is a base64-encoded JSON of `{ last_activity_date: ISO string, id: number }`. Reason: `last_activity_date DESC, id DESC` matches the manager's triage mental model (recently-touched bubbles up); tie-breaker on id makes it stable.
+- **D-10:** API returns `{ tickets: [...], nextCursor: string | null, hasMore: boolean }`. When `nextCursor` is null, list is exhausted.
+- **D-11:** Page size is 25 (per TICK-05). The `limit` query param is accepted but capped server-side at 25 to prevent abuse.
+
+### Infinite scroll trigger
+- **D-12:** Use the browser-native `IntersectionObserver` API (no library). A sentinel `` lives at the end of the list; when it intersects the viewport with `rootMargin: '200px'`, fetch next page.
+- **D-13:** Guard against duplicate fetches: the sentinel callback is a no-op if `loadingMore || !hasMore`.
+- **D-14:** Always render a focusable "Load more" button below the sentinel as the accessibility fallback (TICK-06). Clicking it triggers the same fetch path. Hide only when `!hasMore`.
+
+### List row presentation
+- **D-15:** Each row has a 4px-wide left-edge color stripe via `border-l-4` + a priority color class:
+ - 1 (Critical) → `border-red-500`
+ - 2 (High) → `border-orange-400`
+ - 3 (Medium) → `border-amber-400`
+ - 4 (Low) → `border-slate-300`
+ Reason: TICK-03 spec.
+- **D-16:** Row body shows: ticket number (mono small), title (1-line truncate), company (muted small), age (relative time, muted), assignee (initials avatar or text). Single-tap navigates to `/mobile/tickets/[id]` (TICK-04).
+- **D-17:** Remove the priority dot from the existing row (replaced by the stripe). Keep `relTime()` helper as-is.
+
+### Detail page header
+- **D-18:** Detail page (`/mobile/tickets/[id]`) keeps its current body. Only the in-page header bar at the top is reskinned: replace the current title bar with a row containing a back chevron (`ArrowLeft` icon → `router.back()`), the breadcrumb "Tickets / #{ticket_number}", and an external-link icon that opens the desktop ticket URL. The shell's HeaderBar (Wulf + Bell + avatar) already renders above it from `app/mobile/layout.tsx` — no change there.
+- **D-19:** No structural changes to the detail body, comments, or attachments — out of scope for this phase.
+
+### Empty state
+- **D-20:** When the active filter set returns zero results, render a centered message: "No tickets match your filters" with a "Clear filters" button. When there are no tickets at all (no filters, empty result), render "No tickets to triage right now" with a refresh affordance.
+
+### Loading & error states
+- **D-21:** Initial load → skeleton rows (5 placeholder cards). Subsequent infinite scroll → small inline spinner above the Load more button. Error → toast + Load more button shows "Retry".
+
+### Claude's Discretion
+- Exact spacing/typography within rows (match existing density)
+- Whether to memoize row components (only if perf measurement warrants)
+- Cursor encoding helper location (lib/services or inline in route)
+- Exact skeleton visual
+
+
+
+
+## Canonical References
+
+**Downstream agents MUST read these before planning or implementing.**
+
+### Phase spec
+- `docs/superpowers/specs/2026-05-03-mobile-shell-design.md` §6.2 (Tickets) — spec for filter strip, priority stripes, cursor pagination, detail header reskin
+- `.planning/REQUIREMENTS.md` (TICK-01 through TICK-07) — locked acceptance criteria
+
+### Project conventions
+- `CLAUDE.md` — Pulse stack rules (no SWR/react-query, no ORM, fetch-from-clients pattern), `/mobile/*` route boundary, `port 3100`
+- `DESIGN.md` — token usage, navigation IA, current cleanup backlog
+- `ARCHITECTURE.md` — runtime, data flow, workers (no impact this phase)
+
+### Prior phase context
+- `.planning/phases/02-mobile-shell/02-CONTEXT.md` — MobileShell + HeaderBar decisions (the detail header docks under this)
+- `.planning/phases/03-dashboard-restyle/03-01-SUMMARY.md` — pattern for new `/api/mobile/*` shape with TypeScript interface exports (mirror this for tickets endpoint)
+
+### Existing code (entry points)
+- `app/mobile/tickets/page.tsx` — current list page (164 lines, page-based)
+- `app/mobile/tickets/[id]/page.tsx` — current detail page (357 lines, body kept as-is)
+- `app/api/mobile/tickets/route.ts` — current API route (90 lines, page-based, with `kiosk_settings` company scoping)
+
+
+
+
+## Existing Code Insights
+
+### Reusable Assets
+- `Collapsible` from shadcn (`components/ui/collapsible.tsx`) — exists in shadcn primitives
+- `relTime()` helper inline in `app/mobile/tickets/page.tsx:29-37` — keep
+- `kiosk_settings` company scoping helper `getMobileCompanyFilter()` in `app/api/mobile/tickets/route.ts:3-26` — keep, do not regress
+- `lucide-react` icons (Search, X, RefreshCw, ChevronRight, Clock, ArrowLeft) — already in deps
+- shadcn primitives: `Button`, `Input`, `Select`, `Toggle` (or `Switch`), `Skeleton` — all available in `components/ui/`
+- `IntersectionObserver` — browser-native, no dep needed
+
+### Established Patterns
+- Mobile pages are `'use client'` with `useState` + `useEffect` + `fetch('/api/mobile/...')` — NO SWR, NO react-query
+- API routes call `postgresClient.query()` with parameterized SQL; manual snake_case → camelCase transform
+- `requireAuth()` from `lib/auth-utils.ts` is the auth gate for all `/api/*` routes
+- TypeScript interfaces for API response shapes are exported from the route file and imported via `import type` in the page (pattern from Phase 3)
+
+### Integration Points
+- `app/mobile/layout.tsx` (Phase 2) renders the new shell — Tickets pages dock inside it; no layout changes needed
+- BottomNav active-tab detection uses `pathname.startsWith('/mobile/tickets')` — already correct
+- `kiosk_settings` table for company scoping — existing, already wired into the current ticket route
+
+
+
+
+## Specific Ideas
+
+- Mirror Phase 3's pattern: API route exports `MobileTicketListResponse` and the row interface; page imports the response type via `import type`.
+- Cursor encoder/decoder should be small (~10 lines) and live inline in the route file unless a second consumer appears.
+- Status filter should default to "Open + In Progress + Waiting" (i.e. `status != 5`) matching the existing route's hardcoded filter, so the URL with no status param yields the same default the manager expects.
+
+
+
+
+## Deferred Ideas
+
+- Bulk actions (mass close/assign) — out of scope; manager use case here is read-and-tap-into-detail
+- Saved filter presets — not in TICK-* scope; consider for a future phase
+- Detail page body refactor (comments, time entries) — explicitly out of scope (TICK-07 says "body kept largely as-is")
+- Server-sent push of new tickets — no streaming this iteration
+- Search highlight in row — nice-to-have, not in TICK-* scope
+- Tablet/desktop responsive breakpoints — `/mobile` is phone-only by milestone constraint
+
+
+
+---
+
+*Phase: 04-tickets-restyle*
+*Context gathered: 2026-05-03*
diff --git a/.planning/phases/04-tickets-restyle/04-DISCUSSION-LOG.md b/.planning/phases/04-tickets-restyle/04-DISCUSSION-LOG.md
new file mode 100644
index 0000000..e788dcf
--- /dev/null
+++ b/.planning/phases/04-tickets-restyle/04-DISCUSSION-LOG.md
@@ -0,0 +1,127 @@
+# Phase 4: Tickets Restyle - Discussion Log
+
+> **Audit trail only.** Do not use as input to planning, research, or execution agents.
+> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
+
+**Date:** 2026-05-03
+**Phase:** 04-tickets-restyle
+**Mode:** auto (--auto --chain)
+**Areas discussed:** Filter strip, URL sync, Pagination model, Infinite scroll trigger, List row presentation, Detail page header, Empty/loading states
+
+---
+
+## Filter strip
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Always-expanded inline filters | All controls visible by default — more taps reachable | |
+| `Collapsible` from shadcn, default collapsed | TICK-01 spec; phone-first density | ✓ |
+| Drawer-based filter sheet | Filters open in a Sheet — feels heavier, more taps | |
+
+**Auto choice rationale:** TICK-01 explicitly says "default collapsed". Spec-locked.
+
+---
+
+## Filter set
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Status + priority + queue + assigned-to-me | Spec set | ✓ |
+| Add company filter | More flexibility; out of TICK-* scope | |
+| Add date range | Could be useful; out of TICK-* scope | |
+
+**Auto choice rationale:** TICK-01 enumerates the four filters. Anything more is scope creep.
+
+---
+
+## URL sync mechanism
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| `router.push()` on every filter change | Each filter tweak adds a back-stack entry | |
+| `router.replace()` on every filter change | Filter tweaks don't pollute back stack | ✓ |
+| Manual `history.replaceState()` | Lower-level; loses Next.js rerouting integration | |
+
+**Auto choice rationale:** `router.replace()` is the App Router idiom for filter UIs. Back button should return to whatever surface the user came from, not 8 filter mutations ago.
+
+---
+
+## Pagination model
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Keep page-based `?page=N&limit=30` | Simpler, current behavior | |
+| Cursor `?cursor=&limit=25` with last_activity_date+id | Stable under writes; matches triage workflow | ✓ |
+| Cursor on created_date+id | Stable but doesn't reflect "recent activity" priority | |
+
+**Auto choice rationale:** TICK-05 mandates cursor-based ~25/page. Choosing `last_activity_date DESC, id DESC` because managers triage by "what just changed" not "what was created".
+
+---
+
+## Infinite scroll trigger
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| `react-intersection-observer` library | Hook abstraction, extra dep | |
+| Browser-native `IntersectionObserver` | No dep, ~20 lines | ✓ |
+| Scroll-event listener with throttle | Less precise, more re-renders | |
+
+**Auto choice rationale:** CLAUDE.md prohibits adding new state libraries; native IntersectionObserver is sufficient.
+
+---
+
+## "Load more" fallback
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Skip — sentinel handles it | Fails accessibility | |
+| Always-rendered focusable button below sentinel | TICK-06 mandates accessibility | ✓ |
+| Show only when keyboard navigation detected | Brittle, not robust | |
+
+**Auto choice rationale:** TICK-06 requires the fallback button. Always-rendered keeps it focusable and visible to screen readers.
+
+---
+
+## List row priority indicator
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Existing dot (bg-color-N) | Smaller, less prominent | |
+| Left-edge stripe via `border-l-4` | TICK-03 spec | ✓ |
+| Background tint of whole row | Too heavy on phone | |
+
+**Auto choice rationale:** TICK-03 explicitly specifies left-edge stripe.
+
+---
+
+## Detail page header
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Refactor full detail page | Out of scope per TICK-07 | |
+| Reskin in-page header only (back chevron + breadcrumb + ext link) | TICK-07 spec | ✓ |
+
+**Auto choice rationale:** TICK-07 explicitly says "header reskinned... body kept largely as-is".
+
+---
+
+## Empty/loading states
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Spinner only on load | Janky, no skeleton | |
+| Skeleton rows initial + inline spinner subsequent + empty-state messages | Polished, fits design tokens | ✓ |
+
+**Auto choice rationale:** Standard mobile pattern; existing `Skeleton` shadcn primitive available.
+
+---
+
+## Auto-resolved scope creep checks
+
+- **Bulk actions** — flagged and deferred (read-tap-detail is the manager workflow)
+- **Saved presets** — deferred to a future phase
+- **Detail body refactor** — explicitly excluded by TICK-07
+
+## External research
+
+None. Decisions were derivable from spec + existing code patterns.