diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 54bc405..c2ba87c 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -165,8 +165,8 @@ Decimal phases appear between their surrounding integers in numeric order.
2. The profile is a real page (not a modal) — the device/browser back gesture returns to the overview at the same scroll position
3. The profile renders single-column: identity header → period selector → key metrics (compact) → activity breakdown list → recent items, sourced from the existing engagement profile data endpoints (no new data)
**Plans**: 2 plans
-- [ ] 08-01-PLAN.md — MS Graph user-photo proxy at /api/mobile/engagement/user/[userId]/photo (ENG-06; D-25, D-26)
-- [ ] 08-02-PLAN.md — Mobile profile page at /mobile/engagement/[userId] + 6 EngagementProfile* components (ENG-06, ENG-07, ENG-08)
+- [x] 08-01-PLAN.md — MS Graph user-photo proxy at /api/mobile/engagement/user/[userId]/photo (ENG-06; D-25, D-26)
+- [x] 08-02-PLAN.md — Mobile profile page at /mobile/engagement/[userId] + 6 EngagementProfile* components (ENG-06, ENG-07, ENG-08)
**UI hint**: yes
### Phase 9: User Profile & Preferences (NEW)
diff --git a/.planning/STATE.md b/.planning/STATE.md
index 2f8a6b4..12ff3d0 100644
--- a/.planning/STATE.md
+++ b/.planning/STATE.md
@@ -4,14 +4,14 @@ milestone: v1.0
milestone_name: milestone
status: executing
stopped_at: Phase 8 UI-SPEC approved
-last_updated: "2026-05-07T23:45:22.929Z"
-last_activity: 2026-05-07 -- Phase 08 planning complete
+last_updated: "2026-05-08T13:58:28.740Z"
+last_activity: 2026-05-08
progress:
total_phases: 10
- completed_phases: 8
+ completed_phases: 9
total_plans: 24
- completed_plans: 22
- percent: 92
+ completed_plans: 24
+ percent: 100
---
# Project State
@@ -21,14 +21,14 @@ progress:
See: .planning/PROJECT.md (updated 2026-05-03)
**Core value:** A manager can open Pulse on their phone and, in under 30 seconds, see the state of the business and triage tickets — without ever needing to switch to desktop for read-only awareness.
-**Current focus:** Phase 07 — Engagement Overview (NEW)
+**Current focus:** Phase 08 — engagement-user-profile-new
## Current Position
-Phase: 8
+Phase: 9
Plan: Not started
-Status: Ready to execute
-Last activity: 2026-05-07 -- Phase 08 planning complete
+Status: Executing Phase 08
+Last activity: 2026-05-08
Progress: [░░░░░░░░░░] 0%
@@ -36,7 +36,7 @@ Progress: [░░░░░░░░░░] 0%
**Velocity:**
-- Total plans completed: 22
+- Total plans completed: 24
- Average duration: —
- Total execution time: 0.0 hours
@@ -52,6 +52,7 @@ Progress: [░░░░░░░░░░] 0%
| 06 | 3 | - | - |
| 07 | 3 | - | - |
| 07.1 | 5 | - | - |
+| 08 | 2 | - | - |
**Recent Trend:**
diff --git a/.planning/phases/08-engagement-user-profile-new/08-VERIFICATION.md b/.planning/phases/08-engagement-user-profile-new/08-VERIFICATION.md
new file mode 100644
index 0000000..bc69290
--- /dev/null
+++ b/.planning/phases/08-engagement-user-profile-new/08-VERIFICATION.md
@@ -0,0 +1,113 @@
+---
+phase: 08-engagement-user-profile-new
+verified: 2026-05-08T00:00:00Z
+status: human_needed
+score: 12/13 must-haves verified
+re_verification: false
+human_verification:
+ - test: "Navigate to /mobile/engagement, tap an employee row, press the device back button"
+ expected: "Returns to /mobile/engagement at the same scroll position as before tapping"
+ why_human: "Scroll restoration (SC#2) is programmatic — the sessionStorage shim was added to app/mobile/engagement/page.tsx but the SUMMARY documents it as partially working. Browser-back scroll position cannot be verified by grep or static analysis. User has accepted the partial behavior as a known limitation."
+---
+
+# Phase 8: Engagement User Profile (NEW) — Verification Report
+
+**Phase Goal:** From the Engagement overview, a manager taps an employee row and arrives at a real, shareable profile page — single-column phone-first — and the device back gesture returns them to the overview.
+**Verified:** 2026-05-08
+**Status:** human_needed
+**Re-verification:** No — initial verification
+
+## Goal Achievement
+
+### Observable Truths
+
+| # | Truth | Status | Evidence |
+|---|-------|--------|----------|
+| 1 | Tapping a row in /mobile/engagement navigates to /mobile/engagement/{graphUserId} | ✓ VERIFIED | `EngagementUserRow.tsx:48` has `href={'/mobile/engagement/${user.graphUserId}'}` pointing to the real route at `app/mobile/engagement/[userId]/page.tsx` |
+| 2 | The browser back gesture returns to the overview at the prior scroll position (SC#2) | ? HUMAN NEEDED | sessionStorage shim exists at `app/mobile/engagement/page.tsx:175-225` (rAF save + retry restore), but SUMMARY and context notes both document this as partial — does not reliably restore scroll. User accepted as known limitation. |
+| 3 | The profile renders single-column in the specified order: H1 → period chips → identity header → 2×2 metric grid → activity breakdown → recent entries → recent meetings | ✓ VERIFIED | `page.tsx:183-224` confirms exact render order: `
`, ``, ``, ``, ``, ``, `` |
+| 4 | Identity header shows photo (or initials fallback), display name, jobTitle, department (omitted if null), email as mailto, last-active row when signal exists | ✓ VERIFIED | `EngagementProfileHeader.tsx:35-88` — `photoFailed` useState toggle, `onError={() => setPhotoFailed(true)}`, department conditional at line 78, `href={mailto:${email}}` at line 82, `lastActiveLabel &&` at line 87 |
+| 5 | 2×2 hero metric cards (Hours worked / Billable hours / Days worked / Meetings attended) render in grid-cols-2 from /api/engagement/user/[userId]?period= | ✓ VERIFIED | `EngagementProfileMetricGrid.tsx:24` has `className="grid grid-cols-2 gap-3"` with all four metric labels at lines 28, 34, 40, 46; data flows from page.tsx via props |
+| 6 | Period chip selection refetches data; recent-items remain bound to 10 regardless of period | ✓ VERIFIED | `page.tsx:117` — useEffect deps `[userId, period, retryNonce]`; fetch at line 93 uses `?period=${period}`; EngagementRecentEntries/Meetings components receive raw `recentEntries` / `recentTeamsMeetings` arrays not filtered by period |
+| 7 | Activity breakdown renders Time / Communication / Meetings subsections with after-hours and Zoom conditional rows | ✓ VERIFIED | `EngagementProfileBreakdown.tsx:51-91` — three labeled `
` subsections; `showAfterHours` at line 45 gates after-hours row; `showZoom` at line 46 gates Zoom row |
+| 8 | Recent entries and meetings collapse/expand via shadcn Collapsible; Set state; period changes don't reset expansion | ✓ VERIFIED | Both components use `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent` from shadcn; `useState>(new Set())` for expansion tracking; period changes propagate to page, not through the component state |
+| 9 | 404 from data endpoint renders inline "User not found" with back-to-Engagement link | ✓ VERIFIED | `page.tsx:120-132` — `errorState === 'not-found'` branch renders `
User not found
` and `Back to Engagement` |
+| 10 | 500/network failure renders sonner toast + inline Retry button via retryNonce increment | ✓ VERIFIED | `page.tsx:5` imports `toast from 'sonner'`; `page.tsx:111` calls `toast.error()`; Retry button at line 143 calls `setRetryNonce((n) => n + 1)` |
+| 11 | Photo endpoint non-200 silently falls back to initials — no toast, no error UI | ✓ VERIFIED | `EngagementProfileHeader.tsx:61` — `onError={() => setPhotoFailed(true)}` toggles the `photoFailed` state, rendering initials only; no toast import in that component |
+| 12 | EngagementUserRow.tsx was NOT modified by this plan | ✓ VERIFIED | `git log components/mobile/EngagementUserRow.tsx` — last commit is `d637892` (Phase 7); no Phase 8 commits touch this file |
+| 13 | /api/engagement/user/[userId]/route.ts was NOT modified by this plan | ✓ VERIFIED | `git log app/api/engagement/user/[userId]/route.ts` — last commit is `c518eef` (unrelated); no Phase 8 commits touch this file |
+
+**Score:** 12/13 truths verified (1 requires human testing — SC#2 scroll restoration)
+
+### Required Artifacts
+
+| Artifact | Expected | Status | Details |
+|----------|----------|--------|---------|
+| `lib/services/msgraph-client.ts` | getUserPhotoBytes() method | ✓ VERIFIED | Line 419 — `async getUserPhotoBytes(userId)` exists; returns ArrayBuffer on 2xx, null on 404, throws on other non-2xx; uses `this.getToken()` |
+| `app/api/mobile/engagement/user/[userId]/photo/route.ts` | Photo proxy GET handler | ✓ VERIFIED | 67 lines; exports `GET`; `requireAuth` is first call (line 10); all status codes present (400, 404, 503) |
+| `app/mobile/engagement/[userId]/page.tsx` | Mobile profile page | ✓ VERIFIED | 230 lines (>= 120 minimum); `'use client'` at line 1; all 7 components wired |
+| `components/mobile/EngagementProfileSkeleton.tsx` | Full-page skeleton | ✓ VERIFIED | 77 lines; exports `EngagementProfileSkeleton`; contains `Skeleton` |
+| `components/mobile/EngagementProfileHeader.tsx` | Identity header card | ✓ VERIFIED | 94 lines; exports `EngagementProfileHeader`; photo + initials fallback wired |
+| `components/mobile/EngagementProfileMetricGrid.tsx` | 2×2 metric grid | ✓ VERIFIED | 51 lines; exports `EngagementProfileMetricGrid`; contains `grid-cols-2` |
+| `components/mobile/EngagementProfileBreakdown.tsx` | Activity breakdown card | ✓ VERIFIED | 96 lines; exports `EngagementProfileBreakdown`; three subsections present |
+| `components/mobile/EngagementRecentEntries.tsx` | Collapsible time entries list | ✓ VERIFIED | 103 lines; exports `EngagementRecentEntries`; Collapsible from shadcn; `Number(entry.hours_worked).toFixed(1)` coercion |
+| `components/mobile/EngagementRecentMeetings.tsx` | Collapsible meetings list | ✓ VERIFIED | 142 lines; exports `EngagementRecentMeetings`; Collapsible from shadcn; `Number(te.hours_worked).toFixed(1)` coercion |
+
+### Key Link Verification
+
+| From | To | Via | Status | Details |
+|------|----|-----|--------|---------|
+| `photo/route.ts` | `lib/services/msgraph-factory.ts` | `import { getMsgraphClient, isMsgraphConfigured }` | ✓ WIRED | Lines 3, 14, 39 — both functions imported and called |
+| `photo/route.ts` | `lib/auth-utils.ts` | `import { requireAuth }` | ✓ WIRED | Line 2 imports, line 10 calls `requireAuth()` as first handler action |
+| `photo/route.ts` | `MsGraphClient.getUserPhotoBytes` | method call | ✓ WIRED | Line 40 — `client.getUserPhotoBytes(userId)` |
+| `page.tsx` | `/api/engagement/user/[userId]` | fetch in useEffect | ✓ WIRED | Line 93 — `fetch('/api/engagement/user/${userId}?period=${period}')` in useEffect with deps `[userId, period, retryNonce]` |
+| `page.tsx` | `EngagementPeriodChips` | import + JSX | ✓ WIRED | Line 6 imports, lines 138 and 186 render `` |
+| `EngagementProfileHeader.tsx` | `/api/mobile/engagement/user/[userId]/photo` | `