- [x] Task 8: Public submission route — `4004aec`. `POST /api/public/intake`. Sibling of `/api/client/intake` with `created_by_role='anonymous'` set in code. Dropped vestigial `tokenHeader` line per the plan's own comment.
- [x] Task 9: Minimal intake layout — `5137a6d`. Deviation: no `<html>`/`<body>` (Next.js root layout owns those). Intent preserved — no ThemeScript, ThemeSync, Inter, or globals.css.
- [x] Task 10: Public intake page — `c9be1d2`. 404 on unknown/revoked/expired with same wording. IntakePublic placeholder kept build green between Tasks 10/11.
- [x] Task 11: Public wizard + brief preview — `a3972f5`. Replaced Task 10 placeholder with the real wizard. POST to `/api/public/intake`, redirect to `/intake/[token]/sent/[cardId]`.
- [x] Task 12: Thanks page — `07ece87`. Resolves token only to confirm still alive — revoked tokens landing here still show "Thanks" so the submission cannot be probed.
- [x] Task 13: Card submitted-by badge — `bb78c9b`. CardSubmittedBy: nothing for owner, "Requested" for client, "Submitted via public link" for anonymous. Renders nothing when accepted.
- [x] Task 19: Full verification — automated parts only. `npm run build` clean; color check empty (no banned tailwind greys or hex literals). All 11 new integration tests pass in isolation. **Pre-existing failures** in `admin.test.ts` (1) and `plan-puzzle.test.ts` (3) are unrelated to phase 8 — they predate this work and concern a different surface. Manual walkthrough steps for Tasks 19.3–19.8 are listed in the chat.
- [x] Task 1: The source SVG — `8b40449`. Rounded-square brand tile, single white `!`, brand colour matches `src/lib/brand-default.ts`.
- [x] Task 2: The icon build script — `2b8...`. `@resvg/resvg-js` renders source.svg into the four PNGs the manifest + iOS expect (192/512/maskable-512/apple-touch-180). Build is one-shot; outputs committed.
- [x] Task 3: The manifest — `2b61f5c`. `start_url=/intake/` (trailing slash) so the browser falls back to last-visited token, or install-time URL on first open.
- [x] Task 4: The service worker — `dfcd673`. Cache-first for static allowlist, network-first for /intake/* HTML with offline fallback to cached shell, pass-through for /api/public/intake and Server Actions. `CACHE_NAME=intake-v1`.
- [x] Task 5: Manifest link in the intake layout — `4c4dbaf`. Deviation: route pieces through Next `metadata` export (manifest, apple-touch-icon, appleWebApp); only SW registration stays inline. Verified rendered head: theme-color, manifest link, apple-touch-icon, SW script.
- [x] Task 7: Final test run — `npm test` 450/450; new integration tests 11/11; `npm run build` clean.
## Phase 8b manual install walkthrough
PWA install behaviour is browser-state. To verify on your own device:
1. iOS Safari: open `/intake/<token>` → Share → Add to Home Screen. The icon should be the brand-coloured tile with `!`. Launching from the home screen opens standalone (no Safari chrome).
2. Android Chrome: open `/intake/<token>` → browser shows an install prompt at the bottom. Install. Icon appears in the launcher.
3. Once installed, kill the network briefly and reload — the cached shell should render with an offline-style fallback (the route's content fetch fails but the SW serves the cached HTML shell from the install).
4. The Wizard's POST to `/api/public/intake` and the Server Action both pass through the SW unchanged, so a flaky network only affects the SHELL load, not the submission.