diff --git a/.planning/phases/08-engagement-user-profile-new/08-02-SUMMARY.md b/.planning/phases/08-engagement-user-profile-new/08-02-SUMMARY.md
new file mode 100644
index 0000000..9429f2a
--- /dev/null
+++ b/.planning/phases/08-engagement-user-profile-new/08-02-SUMMARY.md
@@ -0,0 +1,99 @@
+---
+phase: 08-engagement-user-profile-new
+plan: "02"
+subsystem: mobile-engagement
+tags: [mobile, engagement, profile, page, components]
+dependency_graph:
+ requires:
+ - GET /api/mobile/engagement/user/[userId]/photo
+ - GET /api/engagement/user/[userId]
+ - components/mobile/EngagementPeriodChips.tsx
+ - components/mobile/EngagementUserRow.tsx#getInitials
+ provides:
+ - GET /mobile/engagement/[userId] (real route, not modal)
+ affects:
+ - app/mobile/engagement/[userId]/page.tsx
+ - components/mobile/EngagementProfileSkeleton.tsx
+ - components/mobile/EngagementProfileHeader.tsx
+ - components/mobile/EngagementProfileMetricGrid.tsx
+ - components/mobile/EngagementProfileBreakdown.tsx
+ - components/mobile/EngagementRecentEntries.tsx
+ - components/mobile/EngagementRecentMeetings.tsx
+ - app/mobile/engagement/page.tsx (sessionStorage scroll shim only)
+tech_stack:
+ added: []
+ patterns:
+ - "App Router dynamic route `[userId]` with `params: Promise<{userId}>` unwrapped via React.use"
+ - "Image error → initials fallback via local useState toggle"
+ - "rAF-throttled scroll capture + retry-on-restore (D-04 fallback)"
+key_files:
+ modified:
+ - app/mobile/engagement/page.tsx
+ - components/mobile/EngagementRecentEntries.tsx
+ - components/mobile/EngagementRecentMeetings.tsx
+ created:
+ - app/mobile/engagement/[userId]/page.tsx
+ - components/mobile/EngagementProfileSkeleton.tsx
+ - components/mobile/EngagementProfileHeader.tsx
+ - components/mobile/EngagementProfileMetricGrid.tsx
+ - components/mobile/EngagementProfileBreakdown.tsx
+ - components/mobile/EngagementRecentEntries.tsx
+ - components/mobile/EngagementRecentMeetings.tsx
+decisions:
+ - "Profile is a real route, not a modal — preserves device-back behavior (D-04 intent)"
+ - "Photo fallback to initials is a render-time `
` onError → useState toggle (no double-fetch)"
+ - "Coerce Postgres NUMERIC strings to Number() at render — pg returns numeric columns as strings; period totals are parseFloat'd in the API but recentEntries[]/matchedEntries[] hours_worked are passed through verbatim"
+ - "Scroll restoration: SC#2 is partial — sessionStorage shim with rAF retry was added but did not reliably restore window scroll on this layout in user testing. User accepted as a known limitation; no follow-up plan filed."
+metrics:
+ duration_minutes: 60
+ completed_date: "2026-05-08"
+ tasks_completed: 4
+ files_modified: 3
+ files_created: 7
+requirements_addressed: [ENG-06, ENG-07, ENG-08]
+---
+
+# Phase 8 Plan 02: Mobile Engagement User Profile Summary
+
+**One-liner:** Mobile engagement user profile at `/mobile/engagement/[userId]` — a real Next.js route (not a modal) composing six new components on top of the existing `/api/engagement/user/[userId]` endpoint plus the Plan 01 photo proxy.
+
+## Tasks Completed
+
+| Task | Name | Commit | Files |
+|------|------|--------|-------|
+| 1a | Page shell + Skeleton + period/fetch wiring | 3247c92 | app/mobile/engagement/[userId]/page.tsx, components/mobile/EngagementProfileSkeleton.tsx |
+| 1b | Identity header + 2×2 metric grid + page wiring | df78ab8 | components/mobile/EngagementProfileHeader.tsx, components/mobile/EngagementProfileMetricGrid.tsx, app/mobile/engagement/[userId]/page.tsx |
+| 2 | Activity breakdown + Recent entries + Recent meetings | 0be0c1f | components/mobile/EngagementProfileBreakdown.tsx, components/mobile/EngagementRecentEntries.tsx, components/mobile/EngagementRecentMeetings.tsx, app/mobile/engagement/[userId]/page.tsx |
+| 3 | Manual verification (scroll restoration) | n/a | (user-tested) |
+
+### Post-test fixes
+
+| Fix | Commit | Files | Reason |
+|-----|--------|-------|--------|
+| Coerce hours_worked to Number before .toFixed | 81079ad | components/mobile/EngagementRecentEntries.tsx, components/mobile/EngagementRecentMeetings.tsx | Postgres NUMERIC arrives as string via pg — runtime TypeError on render |
+| Scroll-restoration shim attempt | 6bdc937, then revised in subsequent commits | app/mobile/engagement/page.tsx | Mobile shell uses overflow-y-auto on `` but the document scrolls in practice; sessionStorage save+restore added with rAF retry. SC#2 still partial — see Decisions. |
+
+## What Was Built
+
+**`/mobile/engagement/[userId]` page** — full profile surface composed of:
+
+1. **H1** — display name from Graph user
+2. **Sticky period chips** (D7/D30/D90) — defaults to D30, refetches on change
+3. **Identity card** — Graph photo via the Plan 01 proxy with onError → `getInitials(displayName)` fallback; title, department, mailto link, last-active label (relative ≤7d via `date-fns`, absolute >7d via `formatInUserTimezone`)
+4. **2×2 metric grid** — Hours worked, Billable hours, Days worked, Meetings attended (period-scoped)
+5. **Activity breakdown card** — Time / Communication / Meetings subsections with conditional Zoom + after-hours rows
+6. **Recent time entries** — collapsed list of recent Autotask `time_entries` with billable badge + truncated note
+7. **Recent Teams meetings** — list with participant names, client attendees, inline matched time entries
+
+Loading state renders `EngagementProfileSkeleton`. Error states: 404 → "User not found" with back link; 5xx/network → skeleton + Retry button (uses `retryNonce` to re-trigger the fetch effect).
+
+Reuses `/api/engagement/user/[userId]` verbatim — no API modifications. The existing `EngagementUserRow.tsx` link to `/mobile/engagement/${graphUserId}` (already present from Phase 7) now reaches a real destination.
+
+## Verification Results
+
+**SC#1 — Page renders all sections:** ✓ Passed (user-confirmed)
+**SC#2 — Scroll restoration on back gesture:** ⚠ Partial — list scrolls back to top, not to the previous row position. The mobile shell's layout has `` with `overflow-y-auto` but in practice the document scrolls (`window.scrollY` carries the value, `.scrollTop` stays 0). A sessionStorage shim with rAF retry was added but did not consistently restore `window.scrollY` after the row list re-rendered. User accepted as a known limitation.
+
+## Notable Deviations
+
+None. Plan was followed; the only adjustments were the post-test fixes documented above.