diff --git a/dev/README.md b/dev/README.md
index 489708e..e7c0b0c 100644
--- a/dev/README.md
+++ b/dev/README.md
@@ -9,6 +9,7 @@ agent-runnable prompt files split into two parallel tracks.
| [`phases/`](phases/README.md) | The prompt chain — `backend/` (b0–b15) and `frontend/` (f0–f15), plus the shared rules/template in `phases/_shared/`. **Start at [`phases/README.md`](phases/README.md).** |
| [`contracts/`](contracts/README.md) | The shared API/flow contract between the two independent projects. Backend writes, frontend reads. |
| [`shared-working-context/`](shared-working-context/README.md) | The parallel-agent handoff + per-phase reports + the mock registry. Each lane writes only its own files. |
+| [`post-phase/`](post-phase/refinement/README.md) | Follow-up chains run after the 16+16 phases: the server audit ([`server/`](post-phase/server/README.md)), the integration/production [`refinement/`](post-phase/refinement/README.md) chain (complete), and the [`ui/`](post-phase/ui/README.md) design chain (UI phases 0–13 + the design audit that produced them). |
## How to use it
diff --git a/dev/post-phase/hardening/LOOP-PROMPT.md b/dev/post-phase/hardening/LOOP-PROMPT.md
new file mode 100644
index 0000000..b44d590
--- /dev/null
+++ b/dev/post-phase/hardening/LOOP-PROMPT.md
@@ -0,0 +1,59 @@
+# The hardening loop prompt
+
+Two ways to drive the chain. Both are self-terminating: when nothing is left unchecked, the agent
+reports "chain complete" and stops instead of doing work.
+
+---
+
+## A. One phase per session (recommended)
+
+Paste this into a **fresh** Claude Code session, once per iteration:
+
+```
+Read dev/post-phase/hardening/README.md and dev/post-phase/hardening/issues.md.
+
+Find the FIRST phase (0→5) whose issues.md items are not all checked off, and whose dependencies
+(per the README chain table) are already complete. Execute that phase's file
+(dev/post-phase/hardening/hardening-phase-N-*.md) end to end:
+
+1. Do the phase's required reading first. Do not re-audit findings — issues.md evidence is verified;
+ re-locate line numbers if they drifted.
+2. Implement the full scope. Stay inside the phase's track (client/ or server/) unless the file says
+ otherwise.
+3. Run that project's gate (client: npm run check + npm run test:ci · server: dotnet build Baya.sln +
+ dotnet test Baya.sln) and the phase's own Definition of Done — including any runtime probe it
+ demands. A phase is not done on a code-only fix if its DoD requires observed behavior.
+4. Tick the finished items in issues.md (add the commit hash), write the phase report, update the
+ docs the phase names, and commit with message "hardening phase N:
".
+5. End your reply with: the phase completed, what's verified, anything re-deferred (and why), and
+ which phase the loop should run next. If ALL items in issues.md are checked, say "HARDENING CHAIN
+ COMPLETE" and summarize the end state instead.
+```
+
+Repeat until it says complete. Phases 0 (frontend) and 1 (backend) may run in two parallel sessions —
+they touch disjoint trees; everything else runs one at a time.
+
+## B. Unattended interval loop (Claude Code /loop)
+
+If you want it self-driving in one long session:
+
+```
+/loop Execute the next incomplete hardening phase per dev/post-phase/hardening/LOOP-PROMPT.md
+section A (steps 1–5). One phase per iteration, commit when green. If issues.md is fully checked,
+say "HARDENING CHAIN COMPLETE" and stop the loop.
+```
+
+Caveats for unattended mode: Phase 0's DoD needs the dev server + API running (keep them up, or let
+the agent start them); Phase 4 involves product decisions (`cancellation_policy_code` set, REQ-027) —
+the agent will decide from `product/` docs, so review that commit; nothing in the chain should push,
+only commit locally.
+
+## Guardrails (both modes)
+
+- **Never skip a DoD runtime check.** H-01 survived nine phases precisely because "the code is
+ correct" was accepted without a live probe.
+- **issues.md is the single progress state.** No side lists; re-deferrals get written there, not
+ dropped.
+- If a phase file contradicts the live code (something already fixed, a file moved), trust the code,
+ note the drift in the report, and keep going — don't restore the file's assumption.
+- One phase = one commit (Phase 4 may commit per REQ group). Don't batch phases into one commit.
diff --git a/dev/post-phase/hardening/README.md b/dev/post-phase/hardening/README.md
new file mode 100644
index 0000000..13bd948
--- /dev/null
+++ b/dev/post-phase/hardening/README.md
@@ -0,0 +1,125 @@
+# Hardening phases — auth gate, role enforcement & the last mocks
+
+**Created:** 2026-07-16 · **Scope:** whole repo (client + server), after the 16+16 build phases and the
+10 refinement phases all completed · **Method:** a 6-dimension multi-agent audit of the live code
+(anonymous access, role guarding, server authorization, remaining mocks, end-to-end journey walk,
+session/error UX), every finding **adversarially re-verified** against the code with file:line evidence,
+plus a live runtime probe of the dev server.
+
+This directory is a **runnable chain of 6 hardening phases**. Run them **in order, one at a time**,
+pointing a fresh agent at one phase file (*"Execute `dev/post-phase/hardening/hardening-phase-0-auth-gate.md`
+end to end"*) — or drive the whole chain with [`LOOP-PROMPT.md`](LOOP-PROMPT.md). The full verified
+finding ledger (with per-item checkboxes) is in [`issues.md`](issues.md).
+
+> **Why this exists after the refinement chain.** The refinement phases made the two projects run as one
+> app. But live probing shows the user-facing symptom *"the app never asks me to log in, and roles feel
+> unenforced"* is real — caused by an auth gate that never executes at runtime, a token-liveness check
+> that can't read the server's encrypted token, an admin RBAC policy that is structurally dead (every
+> admin endpoint 403s), and two client money-path mocks left dangling by the phase-4 de-mock.
+
+---
+
+## The headline: why the app never asks for login
+
+Three defects **mask each other**, which is why this survived nine refinement phases:
+
+1. **`client/middleware.ts` never executes in the running app** (verified live: a cookie-less
+ `GET /fa/admin` returns 200 with page HTML; a bare `GET /` returns 404 instead of next-intl's locale
+ redirect — under both Turbopack *and* webpack). The gate code itself is correct. Root cause on this
+ machine: a stray `C:\Users\Lenovo\pnpm-lock.yaml` (home directory, not in the repo) makes Next.js
+ infer the **workspace root as the home directory**, so the middleware file is never bound. The repo
+ must defend against this (pin the root in `next.config.mjs`) and prove the gate runs (runtime DoD in
+ Phase 0). Next 16 has also deprecated `middleware.ts` in favor of `proxy.ts`.
+2. **`isTokenAlive` can never return `true` for a real token.** The server's access token is an
+ **encrypted JWE** (`JwtService.cs:116-128`, `EncryptingCredentials`, A128KW/A128CBC-HS256); the client
+ helper (`client/src/lib/auth/token.ts:24-38`) base64-decodes segment 1 and JSON-parses it — impossible
+ for a JWE. So had the middleware ever run, it would have **redirect-looped logged-in users to /login**.
+ Same helper seeds server-side auth state (`getServerAuthState`), so `isAuthenticated` seeds `false` on
+ every hard reload. The wire already returns `accessExpiresAt`/`refreshExpiresAt`
+ (`client/src/services/auth/types.ts:65-66`) — the fix is a readable companion expiry cookie.
+3. **The client fallback can't rescue an anonymous visitor.** `useMe()` is enabled only when
+ `isAuthenticated`; for a cookie-less request that's `false` forever, so `useRoleHydration()` stays
+ `loading` and every private shell renders an **infinite branded splash** instead of redirecting to
+ login. RoleGuard needs an explicit unauthenticated → redirect-to-login branch (defense in depth).
+
+**One manual step no phase can do for you:** delete or move the stray `C:\Users\Lenovo\pnpm-lock.yaml`
+from your home directory (it is unrelated to this repo). Phase 0 pins the workspace root so the app no
+longer *depends* on that cleanup, but the stray file will keep confusing other tools too.
+
+---
+
+## What's actually fine (don't re-fix)
+
+- The middleware/auth-gate **logic** and `PUBLIC_PATHS` are correct as written — the problem is execution
+ + the JWE check, not the design.
+- The 4 private shells genuinely all wrap `RoleGuard`; admin **mutations** are consistently gated behind
+ `useAdminCapabilities()`.
+- Server **tenancy** is enforced correctly in every spot-checked handler (bookings, tickets, patients,
+ care records, bank accounts, centers) — owner-or-staff checks with 404-not-403.
+- Public endpoints (catalog/geo/search/nurses, webhooks, dev) are intentionally anonymous;
+ `dev/last_otp` correctly 404s outside Development.
+- The demo seed gives Journey A a real searchable Tehran nurse; base route names match 1:1
+ client↔server; the silent-refresh mechanism (single-flight, retry-once) is sound.
+
+## The verified problem inventory (17 findings + root cause)
+
+Severity-ordered; the full ledger with evidence is [`issues.md`](issues.md).
+
+| # | Severity | Problem | Phase |
+| --- | --- | --- | --- |
+| H-01 | blocker | Auth gate never executes at runtime (workspace-root misdetection; middleware deprecated) | 0 |
+| H-02 | blocker | `isTokenAlive` can't read the JWE token → would redirect-loop; seeds `isAuthenticated=false` on reload | 0 |
+| H-03 | high | Anonymous visitor to a private shell gets an infinite splash, never a login prompt | 0 |
+| H-04 | blocker | `DynamicPermission` RBAC is dead: no RoleClaim ever seeded/grantable → every admin endpoint 403s for the seeded `super_admin`/`finance` personas | 1 |
+| H-05 | high | `BookingRoles.Admin` bundles Support/Moderation into clinical-notes + nurse-balance + forced-transition access | 1 |
+| H-06 | blocker | Refunds mock cross-imports the retired bookings-mock store → real cancellations 404 | 2 |
+| H-07 | blocker | BNPL mock cross-imports retired mock stores → installment checkout 404s or fabricates a fake success while the real request expires unpaid | 2 |
+| H-08 | blocker | Verification 100% mocked while catalog/search are real → a real nurse "publishes" services that can never appear in search, no feedback | 2 |
+| H-09 | high | Nurse earnings screen fabricated although the REQ-025 endpoints are live (flag held hostage by the admin half of the seam) | 2 |
+| H-10 | medium | Payment outcome hard-codes `bookingId: null` though REQ-017 is delivered → confirmation deep links lost | 2 |
+| H-11 | high | Logout/login never clear the React Query cache → previous user's data leaks to the next login on the same device | 3 |
+| H-12 | high | The only error boundary dumps a raw English stack trace, no retry; no `error.tsx`/`global-error.tsx` | 3 |
+| H-13 | high | All 401/403/5xx/network toasts are hardcoded English on a Persian-default app | 3 |
+| H-14 | medium | Admin read-only consoles (audit/verification/tickets/roles) render without a capability check — only the nav hides them | 3 |
+| H-15 | medium | Tier B/C contract REQs still open (refunds 019-021, BNPL 022-024, admin 029-031, partner 032/033/038, verification admin 034, refund admin 035, payout admin 036) | 4 |
+| H-16 | medium | Partner portal unreachable from login (no `/me` signal), fully mocked, no tenancy gate on its pages | 4+5 |
+| H-17 | medium | patientRecords family record: client `string` ids vs wire `long` ids → PUT is write-unsafe; edits don't survive reload | 5 |
+
+## The 6 hardening phases
+
+| # | Phase | Track | Fixes | Depends on |
+| --- | --- | --- | --- | --- |
+| **0** | [Auth gate & session liveness](hardening-phase-0-auth-gate.md) | frontend | H-01 H-02 H-03 | — |
+| **1** | [Admin RBAC & staff role scopes](hardening-phase-1-admin-rbac.md) | backend | H-04 H-05 (+delivers REQ-031) | — |
+| **2** | [Money-path mock integrity](hardening-phase-2-mock-integrity.md) | frontend | H-06 H-07 H-08 H-09 H-10 | 0 |
+| **3** | [Session & error-surface hardening](hardening-phase-3-session-error-ux.md) | frontend | H-11 H-12 H-13 H-14 | 0 |
+| **4** | [Contract completion batch (Tier B/C)](hardening-phase-4-contract-completion.md) | backend | H-15 H-16(server half) | 1 |
+| **5** | [Final de-mock & partner reachability](hardening-phase-5-final-demock.md) | frontend | H-16(client half) H-17 + flip the last flags | 2, 4 |
+
+```
+frontend: 0 auth gate ──► 2 mock integrity ──► 5 final de-mock
+ └───────► 3 session/error UX ▲
+backend: 1 admin RBAC ──► 4 contract batch ──────┘
+```
+
+Phases 0 and 1 are independent — a frontend and a backend agent can run them in parallel
+(the [shared-working-context protocol](../../shared-working-context/README.md) applies).
+**Minimum path to "the app asks for login and roles hold":** 0 → 1. **Minimum path to "the money
+path works end-to-end on real data":** 0 → 2. Everything real, no mocks: all six.
+
+## How the phase files are written
+
+Same skeleton as the rest of the repo (the [phase template](../../phases/_shared/phase-template.md)):
+mission, context, required reading, enumerated scope **with the audit's file:line evidence inlined**
+(so the executing agent doesn't re-audit), invariants, Definition of Done, how to test, close-out.
+Before executing any phase, read [_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).
+
+## Related documents
+
+- [issues.md](issues.md) — the verified finding ledger (evidence + checkboxes; the loop's progress state).
+- [LOOP-PROMPT.md](LOOP-PROMPT.md) — the reusable prompt that drives this chain phase by phase.
+- [../refinement/README.md](../refinement/README.md) — the prior chain this one follows.
+- [../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
+ — the REQ ledger Phase 4 closes out.
+- [../../shared-working-context/reports/mocks-registry.md](../../shared-working-context/reports/mocks-registry.md)
+ — the mock registry Phases 2 & 5 update.
diff --git a/dev/post-phase/hardening/hardening-phase-0-auth-gate.md b/dev/post-phase/hardening/hardening-phase-0-auth-gate.md
new file mode 100644
index 0000000..3b72fa2
--- /dev/null
+++ b/dev/post-phase/hardening/hardening-phase-0-auth-gate.md
@@ -0,0 +1,106 @@
+# Hardening Phase 0 — Auth gate & session liveness (make the app ask for login)
+
+> Make the login gate actually execute at runtime, make token liveness readable despite the JWE access
+> token, and give anonymous visitors a login redirect instead of an infinite splash. This is the direct
+> fix for the reported symptom *"there is no ask for logging in."*
+> **Track:** frontend · **Depends on:** — · **Unlocks:** Phases 2, 3
+> **Before you start, read [_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).**
+
+## 1. Context — where this sits
+
+Fixes **H-01, H-02, H-03** from [issues.md](issues.md). Three defects mask each other: the middleware
+never runs (so nobody is asked to log in), its token check could never pass anyway (the token is an
+encrypted JWE), and the client fallback shows an infinite splash to anonymous visitors. The gate
+*design* (middleware + `PUBLIC_PATHS` + RoleGuard-as-chrome) is correct and stays.
+
+**What already exists (do not rebuild):** `client/middleware.ts` (correct logic), `PUBLIC_PATHS`
+(`src/constants/routes.ts:169`), `RoleGuard`/`useRoleHydration`/`AuthAccountError`
+(refinement-phase-2), `persistAuthTokens`/`clearAuthTokens` (`src/lib/auth/session.ts`), the
+single-flight silent refresh (`src/lib/api/refresh.ts`), and the wire's
+`accessExpiresAt`/`refreshExpiresAt` (`src/services/auth/types.ts:62-66`).
+
+## 2. Required reading (do this first)
+
+- [issues.md](issues.md) H-01/H-02/H-03 — the verified evidence; don't re-audit.
+- `client/middleware.ts`, `client/src/lib/auth/token.ts`, `client/src/lib/auth/session.ts`,
+ `client/src/lib/auth/server.ts`, `client/src/components/auth/RoleGuard.tsx`,
+ `client/src/services/auth/hooks/useRoleHydration.ts`, `client/src/services/auth/routing.ts`.
+- `client/CLAUDE.md` → "Auth Cookies & session state" (the documented design you are repairing) and
+ "Golden rules".
+- Next.js 16 `proxy.ts` file convention (middleware is deprecated/renamed; file must sit at the same
+ level as `app` — for this repo that's `client/src/`).
+
+## 3. Scope — build this
+
+1. **Make the gate execute — and prove it.**
+ - Pin the workspace root in `client/next.config.mjs`: `turbopack.root` (and
+ `outputFileTracingRoot`) → the `client/` directory, so a stray lockfile in an ancestor directory
+ (the live failure: `C:\Users\Lenovo\pnpm-lock.yaml`) can never re-point root detection.
+ - Migrate `client/middleware.ts` → `client/src/proxy.ts` per the Next 16 convention (export
+ `proxy`; keep the exact logic + matcher). Update the `client/CLAUDE.md` references.
+ - **Runtime proof is part of the deliverable:** with the dev server running and no cookies,
+ `GET /` must 307 to `/fa`, and `GET /fa`, `/fa/bookings`, `/fa/nurse`, `/fa/admin` must each
+ redirect to `/fa/login`. `GET /fa/login` must 200. If the probe fails, keep working — do not
+ declare done on a code-only fix.
+2. **JWE-compatible liveness.** The access token can't be decoded client-side, but verify_otp/refresh
+ already return `accessExpiresAt`/`refreshExpiresAt`:
+ - `persistAuthTokens` additionally writes a **non-sensitive companion cookie** (e.g.
+ `access_expires_at`, name in `COOKIE_NAMES`) holding the ISO/epoch expiry; cookie `maxAge`s for
+ both tokens derive from the served expiries (this also fixes the documented 7d-vs-server drift
+ for the refresh cookie). `clearAuthTokens` deletes it.
+ - Replace `isTokenAlive(token)` call sites (proxy + `getServerAuthState`) with a check that reads
+ the expiry cookie when the token doesn't decode as a plain JWT (keep the JWT path as fallback so
+ a future JWS still works). Token **presence** without a readable, live expiry = not alive.
+ - Result: `getServerAuthState` seeds `isAuthenticated=true` after a hard reload of a logged-in
+ session — verify `useMe`/`useSessionRoleSync` then hydrate roles normally.
+3. **Unauthenticated branch in the client fallback (defense in depth).**
+ - `useRoleHydration` gains an explicit `unauthenticated` state (auth context says logged out);
+ `RoleGuard` redirects it to `/${locale}/login?next=` instead of splashing forever.
+ - The login flow honors `next`: after verify (+ role routing), `RoleRouter` prefers a safe,
+ same-origin relative `next` path over `resolveRoleDestination` when present. Never redirect to
+ an absolute/external URL.
+ - `clientFetch`'s unrecoverable-401 redirect should also carry `next`.
+
+## 4. Mocks & seams in this phase
+
+None introduced. Do not touch `USE_*_MOCK` flags here (Phase 2 owns them).
+
+## 5. Critical rules you must not get wrong
+
+- **The middleware/proxy stays UX-only** — no signature verification client-side; the API remains the
+ authority. Don't try to decrypt the JWE in the client.
+- The expiry cookie is a **liveness hint, not a credential** — never gate real authorization on it.
+- `PUBLIC_PATHS` uses `startsWith` — never add `'/'` or another prefix-of-everything entry.
+- Don't break the locale flow: the i18n redirect handling and locale header in the current middleware
+ must survive the migration exactly.
+- Keep RoleGuard's existing loading/error/mismatch semantics (refinement-phase-2) — you're adding a
+ fourth branch, not rewriting the guard.
+- Golden rules: no hardcoded strings (new copy → both `messages/*.json`), cookie access only via the
+ cookie manager, constants for names/params.
+
+## 6. Definition of Done
+
+- The runtime probe in §3.1 passes (paste the curl/status output into your report).
+- Logged-in user: hard reload on `/fa/bookings` stays there (no splash-hang, no bounce to login).
+- Logged-out user: any private deep link → login → completes OTP → lands back on the deep link.
+- Logout → immediately bounced to login on the next private navigation.
+- `npm run check` green; `npm run test:ci` green; RoleGuard + routing tests extended for the
+ `unauthenticated` branch and the `next` param.
+
+## 7. How to test
+
+1. `cd client && npm run dev` (server up per `dev/post-phase/refinement/RUNBOOK.md`).
+2. No cookies: `curl -I http://localhost:3000/fa/bookings` → 307/308 `Location: /fa/login?next=…`.
+3. Log in (dev OTP via `GET /api/v1/dev/last_otp/{phone}`), F5 on the home shell → stays, roles load.
+4. Delete only the `access_token` cookie, navigate → silent refresh recovers; delete both → login ask.
+5. Visit `/fa/nurse` as a customer-only session → RoleGuard redirect toast (unchanged behavior).
+
+## 8. Hand off & document
+
+- Update `client/CLAUDE.md` (middleware→proxy path, expiry-cookie lifecycle, the JWE note) — its
+ "middleware.ts" references and cookie table must match reality.
+- Tick H-01/H-02/H-03 in [issues.md](issues.md) with the commit hash.
+- Write `dev/shared-working-context/reports/hardening-phase-0-report.md` (what was verified at
+ runtime, any surprises).
+- **Note to the human:** the stray `C:\Users\Lenovo\pnpm-lock.yaml` should still be deleted manually;
+ the root pin makes the app immune, but other tools may not be.
diff --git a/dev/post-phase/hardening/hardening-phase-1-admin-rbac.md b/dev/post-phase/hardening/hardening-phase-1-admin-rbac.md
new file mode 100644
index 0000000..017ec65
--- /dev/null
+++ b/dev/post-phase/hardening/hardening-phase-1-admin-rbac.md
@@ -0,0 +1,112 @@
+# Hardening Phase 1 — Admin RBAC & staff role scopes (make role checks real)
+
+> Resurrect the dead `DynamicPermission` policy so the seeded admin personas can actually operate the
+> backoffice, expose the already-built role-management handlers over HTTP (delivering REQ-031), and
+> narrow the over-broad `BookingRoles.Admin` bucket that hands Support/Moderation staff clinical and
+> financial access. This is the server half of *"roles aren't checked."*
+> **Track:** backend · **Depends on:** — (parallel-safe with Phase 0) · **Unlocks:** Phase 4
+> **Before you start, read [_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).**
+
+## 1. Context — where this sits
+
+Fixes **H-04, H-05** from [issues.md](issues.md). Every admin endpoint is gated by
+`[Authorize(ConstantPolicies.DynamicPermission)]`, whose handler passes only for the literal role
+`"admin"` or a matching RoleClaim — but no user holds `"admin"`, no RoleClaim is ever seeded, and the
+only claim-writing service has no controller. Net effect: the seeded `super_admin` (09120000020) and
+`finance` (09120000021) personas — which RUNBOOK.md documents as the sanctioned admin path — get 403 on
+**every** admin console action. Separately, handler-level role sets are the entire authz boundary for
+bookings (controllers carry blanket `[Authorize]`), and `BookingRoles.Admin` bundles five staff roles
+into clinical-notes/nurse-balance/forced-transition access, violating the codebase's own
+"narrowest fitting scope" convention.
+
+**What already exists (do not rebuild):** the `DynamicPermissionHandler`/`DynamicPermissionService`
+wiring (`ServiceCollectionExtension.cs:49-50,100-103`), `RoleManagerService.ChangeRolePermissionsAsync`
+(`RoleManagerService.cs:145-208` — the claim writer), the Application-layer
+`Features/{Role,Admin}` commands/queries (handlers exist, no HTTP surface), the demo seeder
+(`DemoWorldSeeder.cs:279-308`), tenancy checks in handlers (correct — leave alone), and
+`Baya.Application/Common/StaffRoles.cs` + `Features/PatientCareRecords/PatientAccess.cs:17` (the
+narrow-scope pattern to mirror).
+
+## 2. Required reading (do this first)
+
+- [issues.md](issues.md) H-04/H-05 — evidence with file:line; don't re-audit.
+- `DynamicPermissionService.cs`, `RoleNames.cs`, `RoleManagerService.cs`,
+ `DemoWorldDefinitions.cs`/`DemoWorldSeeder.cs`, `Features/Bookings/BookingRoles.cs` and the three
+ handlers listed in H-05, `Common/StaffRoles.cs`, `PatientAccess.cs`.
+- `dev/post-phase/refinement/RUNBOOK.md:115-130` and
+ `dev/shared-working-context/reports/refinement-phase-2-report.md` — the docs that currently
+ overstate what works (you will correct them).
+- `server/CLAUDE.md` + `server/CONVENTIONS.md` (patterns, zero-new-warnings gate).
+- REQ-031 in `dev/shared-working-context/frontend/requests/for-backend.md` (the RBAC endpoints the
+ frontend's `/admin/roles` grid expects).
+
+## 3. Scope — build this
+
+1. **Make DynamicPermission passable.**
+ - `DynamicPermissionService.CanAccess`: treat `RoleNames.SuperAdmin` (and `RoleNames.Admin`) as a
+ full bypass — mirroring how the rest of the codebase treats super_admin — instead of the single
+ literal `"admin"`.
+ - **Seed RoleClaims** for the fine-grained roles (`finance`, `support`, `moderation`) mapping each
+ to the controller/action set it should reach (finance → payouts/refunds/invoices/BNPL admin;
+ support → tickets/alerts; moderation → reviews queue). Seed via the same idempotent mechanism the
+ demo seeder uses (or a migration `HasData` if that's the house style — check how roles themselves
+ are seeded). A fresh clone must have a working admin console out of the box.
+2. **Expose role management over HTTP (REQ-031).** Add a `RolesController` (or `AdminRolesController`)
+ under `Controllers/V1` wiring the existing `Features/Role` handlers: list roles (+ their claims),
+ grant/revoke a user role, update role permissions. Gate it to `super_admin` only. Follow the house
+ controller conventions (snake_case routes, ApiResult envelope, versioning). Mark REQ-031 delivered
+ in `for-backend.md`.
+3. **Split `BookingRoles.Admin`.** Introduce purpose-specific sets (suggested:
+ `BookingRoles.ClinicalAccess = [Admin, SuperAdmin]`,
+ `BookingRoles.Financial = [Admin, SuperAdmin, Finance]`, keep a `Staff` set where genuinely all
+ staff belong, e.g. read-only booking lookups for ticket context). Update the users:
+ `GetCareInstructionsQuery` (clinical), `TransitionBookingStatusCommand` (financial — it arms the
+ payout trigger), `GetNursePayableBalanceQuery` (financial), and review the remaining
+ `BookingRoles.Admin` consumers (`CancelSessionCommand`, `CancelBookingCommand`,
+ `SubmitCareInstructionsCommand`, `GetVisitVerificationQuery`, `ListBookingsQuery`,
+ `GetBookingDetailQuery`, `GetBookingRequestQuery`) — assign each the narrowest set that matches
+ what it exposes. Justify each choice in the report.
+4. **Docs honesty.** Correct `RUNBOOK.md` + the refinement-phase-2 report claims about the admin
+ personas; document the finance/support/moderation scopes somewhere durable (server/CLAUDE.md or a
+ product note).
+
+## 4. Mocks & seams in this phase
+
+None. This is authorization wiring only — no vendor seams, no schema beyond possible RoleClaim seed
+rows.
+
+## 5. Critical rules you must not get wrong
+
+- **Do not weaken anything:** endpoints currently gated by DynamicPermission must stay gated; you are
+ making the policy *satisfiable*, not optional. No `[AllowAnonymous]` anywhere in this phase.
+- Tenancy behavior (404-not-403 on cross-tenant) is correct today — don't touch those handlers except
+ for the role-set swap.
+- The role-claim seed must be **idempotent** (the seeder runs on every Dev boot) and must not touch
+ production posture (respect the refinement-phase-5 config guard patterns).
+- Support/Moderation must lose clinical + financial access, but must NOT lose what tickets genuinely
+ need (booking summary context for a thread). Check the ticket flows before narrowing a shared query.
+- Zero new build warnings; enum/role names are stable wire contracts — don't rename `super_admin` etc.
+
+## 6. Definition of Done
+
+- Integration tests (Testing env, house `WebApplicationFactory` pattern): super_admin 200s on a
+ representative endpoint of every admin console family; finance 200s on payouts + 403s on
+ verification decide; support 403s on `GET bookings/{id}/care_instructions` and on
+ `TransitionBookingStatus`; moderation 200s on the review queue only. Grant/revoke via the new
+ controller round-trips.
+- `dotnet build Baya.sln` 0 new warnings · `dotnet test Baya.sln` green (all prior tests + new).
+- REQ-031 marked delivered; RUNBOOK/report corrections committed.
+
+## 7. How to test (human)
+
+1. Boot per RUNBOOK; log in as 09120000020 (super_admin) via dev OTP.
+2. Call `GET api/v1/platform_configs`, the payout batches list, the moderation queue — all 200.
+3. Log in as 09120000021 (finance): payouts 200; verification decide → 403.
+4. Grant `moderation` to a fresh user via the new RolesController; verify the queue opens for them.
+
+## 8. Hand off & document
+
+- Tick H-04/H-05 in [issues.md](issues.md) with the commit hash.
+- Update `for-backend.md` (REQ-031), `server/CLAUDE.md` (authz model paragraph), RUNBOOK.
+- Write `dev/shared-working-context/reports/hardening-phase-1-report.md` + a backend handoff note if
+ Phase 4 runs as a separate agent.
diff --git a/dev/post-phase/hardening/hardening-phase-2-mock-integrity.md b/dev/post-phase/hardening/hardening-phase-2-mock-integrity.md
new file mode 100644
index 0000000..5078e33
--- /dev/null
+++ b/dev/post-phase/hardening/hardening-phase-2-mock-integrity.md
@@ -0,0 +1,114 @@
+# Hardening Phase 2 — Money-path mock integrity (no fake success on real data)
+
+> Sever the dangling cross-imports the phase-4 de-mock left behind — the refunds and BNPL mocks still
+> read retired in-memory stores keyed by REAL server ids — split the seams whose real halves are
+> already live (verification nurse flow, nurse earnings), and stop discarding the delivered
+> `bookingId`. After this phase, nothing in the money path can show a fake success against real data.
+> **Track:** frontend · **Depends on:** Phase 0 · **Unlocks:** Phase 5
+> **Before you start, read [_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).**
+
+## 1. Context — where this sits
+
+Fixes **H-06 … H-10** from [issues.md](issues.md). Refinement-phase-4 flipped 14 domains to real but
+left 7 mocked; two of the mocked ones (`refunds`, `bnpl`) still hard-import the **mock modules** of
+now-real sibling domains (`bookings`, `bookingRequests`), whose fixture stores no longer correspond to
+anything — so a real customer's cancel 404s and the BNPL wizard either 404s or fabricates a client-side
+"booking" while the real request expires unpaid. Meanwhile two seams hold real, shipped backend halves
+hostage to a single flag (`verification` nurse flow — which silently breaks nurse discoverability
+because the real search gate needs the real `is_verified`; `payouts` nurse reads — REQ-025 is live).
+
+**What already exists (do not rebuild):** the per-domain seam pattern (`apis/index.ts` selects
+mock/real), all the real `clientApi.ts` implementations named below, the b6 verification endpoints, the
+REQ-025 nurse payout endpoints (`NursePayoutsController.cs:33-48`), and the REQ-017 `bookingId` on
+`BookingRequestDto`.
+
+## 2. Required reading (do this first)
+
+- [issues.md](issues.md) H-06/H-07/H-08/H-09/H-10 — full evidence; don't re-audit.
+- `client/CLAUDE.md` → "The services/{domain} reference pattern" + the de-mock status block.
+- The files cited per finding (refunds/bnpl/bookings/bookingRequests mockApis + constants, checkout
+ pages, `PublishGate.tsx`, `MyServicesList.tsx`, payment `clientApi.ts`/`invalidations.ts`,
+ payouts/verification services).
+- `dev/shared-working-context/reports/mocks-registry.md` — you will update the affected rows.
+
+## 3. Scope — build this
+
+1. **H-06 — refunds mock must see real bookings.** While `USE_REFUNDS_MOCK=true` (its REQs land in
+ Phase 4), rewire `services/refunds/apis/mockApi.ts` to resolve the booking through the **selected**
+ `bookingsApi` (the seam, real today) instead of importing `mockGetBookingForRefund` from the
+ bookings mock module. The mock computes the policy preview locally off the real booking's
+ status/sessions/amounts and simulates the cancel/refund state machine on top. A real booking id
+ must produce a coherent preview + cancel flow (mock-side state), never a 404.
+2. **H-07 — BNPL must not fake success.** Two parts:
+ - Derive the checkout BNPL CTA visibility instead of the bare `BNPL_ENABLED=true`
+ (`services/payment/constants.ts:23`, used at `checkout/page.tsx:214`): hide the branch whenever
+ `USE_BNPL_MOCK` is true while `bookingRequests`/`payment` are real (mixed state = the dangerous
+ combination). An honest absent button beats a fabricated booking.
+ - For the dev/demo path that remains reachable (both mocked, e.g. Testing), rewire
+ `services/bnpl/apis/mockApi.ts:5-8` to go through the **selected** `bookingRequestsApi`/
+ `bookingsApi` seams, never the raw mock modules.
+3. **H-08 — split the verification seam; make the nurse trust flow real.**
+ - Split `VerificationApi` selection per half: nurse-facing methods (status, start, identity,
+ Shahkar, bank, document upload, credentials, trust badge) go **real** now — the clientApi is
+ already 1:1 per the mocks registry; admin-facing methods (queue, case, decideStep, signed doc
+ URL) stay mocked until REQ-034 (Phase 4). Follow whatever per-method selection shape is cleanest
+ under the existing seam pattern (two flags, or a composed api object) and record it in the
+ registry.
+ - Wire `PublishGate.tsx` to the real status (its CTA currently calls nothing — `:59-68`), and gate
+ the variant-activation affordance in `MyServicesList.tsx` on real verification status with an
+ honest explainer, so a nurse can no longer "publish" into invisibility. Remove/dev-gate the
+ `__mockApproveAll` simulator button accordingly.
+4. **H-09 — split the payouts seam.** Nurse reads (`earnings_balance`, `earnings`, `{id}` detail,
+ history) go real (`payouts/apis/clientApi.ts:163-187` already implements them); admin batch methods
+ stay mocked until REQ-036. Same split mechanics as verification.
+5. **H-10 — stop nulling `bookingId`.** `services/payment/apis/clientApi.ts:55-75`: read `bookingId`
+ off the wire (drop the `Omit`/`bookingId: null` and the stale REQ-017-pending comments in
+ `clientApi.ts` and `types.ts:98-110`); confirm `invalidations.ts` now receives it and the
+ confirmation page renders the booking + invoice deep links.
+
+## 4. Mocks & seams in this phase
+
+Touches the `refunds`, `bnpl`, `verification`, `payouts` seams (splits/rewires; flags flipped only for
+the halves whose backend is live). Update each row in
+`dev/shared-working-context/reports/mocks-registry.md` — including correcting the stale "reads the
+shared f7 store" descriptions.
+
+## 5. Critical rules you must not get wrong
+
+- **A mock may only reach sibling data through the selected seam** (`services/{domain}/apis/index.ts`),
+ never by importing a sibling's `mockApi.ts` — that's the root cause you're eradicating. Grep for
+ remaining cross-mock imports before finishing.
+- Money is served IRR digit-strings, BigInt-safe — the refunds mock's local preview must reuse the
+ existing money utils and reconcile (refund + fee = captured), never float math.
+- Verified-only search, two-stage disclosure, and "client never computes what the server owns"
+ invariants all still hold; the verification split must not let the client write `is_verified`.
+- A 409 on the money path is benign convergence, never a toast (existing rule).
+- Keep i18n complete for any new copy (both message files); `npm run check` owns the gate.
+
+## 6. Definition of Done
+
+- Real journey (seeded world, real server): create booking request → accept → pay → **cancel** →
+ policy preview renders with reconciling numbers (no 404).
+- BNPL CTA absent on real checkout while the domain is mocked; the D1-D5 wizard is unreachable with a
+ real request id (and works fully in the all-mock dev mode).
+- Fresh nurse: real verification wizard drives the real b6 endpoints; variant activation is gated
+ until the real status approves; after approval + activation the nurse appears in real search.
+- Nurse earnings screen shows real amounts from the live endpoints.
+- Payment confirmation shows working "view booking" + "download invoice" deep links after a real
+ capture.
+- `npm run check` + `npm run test:ci` green; no `services/*/apis/mockApi` imports across domain
+ boundaries (grep-proof in the report).
+
+## 7. How to test (human)
+
+1. Full customer journey per §6 against the RUNBOOK setup; screenshot the cancel preview.
+2. `grep -r "apis/mockApi" client/src/services --include=*.ts | grep -v "own domain"` → empty.
+3. Nurse persona 09120000003 (unverified): services page shows the gated state; run the real
+ verification flow; confirm search visibility flips after approval.
+
+## 8. Hand off & document
+
+- Tick H-06…H-10 in [issues.md](issues.md) with commit hashes.
+- Update `client/CLAUDE.md` de-mock status block + `mocks-registry.md` rows.
+- Write `dev/shared-working-context/reports/hardening-phase-2-report.md`; file new REQs if a split
+ exposed a missing server read (append to `for-backend.md`, Phase 4 picks them up).
diff --git a/dev/post-phase/hardening/hardening-phase-3-session-error-ux.md b/dev/post-phase/hardening/hardening-phase-3-session-error-ux.md
new file mode 100644
index 0000000..26f5234
--- /dev/null
+++ b/dev/post-phase/hardening/hardening-phase-3-session-error-ux.md
@@ -0,0 +1,99 @@
+# Hardening Phase 3 — Session & error-surface hardening
+
+> Close the shared-device data leak (logout leaves every domain's cache warm for the next login), turn
+> the raw stack-trace error boundary into a branded recovery surface with route-level error pages, and
+> localize the four hardcoded-English failure toasts. Plus: client-side capability gating for the four
+> admin consoles that render without a check.
+> **Track:** frontend · **Depends on:** Phase 0 · **Unlocks:** — (independent polish, safe anytime after 0)
+> **Before you start, read [_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).**
+
+## 1. Context — where this sits
+
+Fixes **H-11 … H-14** from [issues.md](issues.md). All four are verified, none is touched by any other
+phase, and none needs backend work.
+
+**What already exists (do not rebuild):** `useLogout` as the single logout path, the
+`AuthAccountError` recovery-card pattern (mirror it for the boundary), `dispatchToast`/`ToastBridge`
+(the non-React toast bridge — keep it, localize what flows through it), `useAdminCapabilities()`
+(`src/hooks/capabilities.ts`) and the non-leaking access-denied patterns used elsewhere in the admin
+screens.
+
+## 2. Required reading (do this first)
+
+- [issues.md](issues.md) H-11/H-12/H-13/H-14 — evidence; don't re-audit.
+- `client/src/services/auth/hooks/{useLogout,useVerifyOtp,useSelectRole}.ts`,
+ `client/src/lib/query/{queryClient.ts,QueryProvider.tsx}`,
+ `client/src/components/common/ErrorBoundary.tsx` + its two consumers,
+ `client/src/lib/api/client.ts` + `client/src/lib/toast/*`,
+ the four admin pages (`admin/{audit,verification,tickets,roles}/page.tsx`) + `AdminLayout.tsx`.
+- `client/CLAUDE.md` → error contract for `clientFetch`, i18n rules, unit-testing rule.
+- Next.js `error.tsx` / `global-error.tsx` file conventions.
+
+## 3. Scope — build this
+
+1. **H-11 — cache isolation across sessions.** `useLogout.onSettled`: `queryClient.clear()` (replace
+ the auth-only `removeQueries`). Symmetrically clear/reset before seeding the new session in
+ `useVerifyOtp.onSuccess` and `useSelectRole.onSuccess`, so User B never sees User A's cached
+ patients/bookings/addresses/tickets/notifications on a shared device. Check nothing depends on
+ surviving cache entries across logout (geo/catalog reference data may simply refetch — correct).
+2. **H-12 — real error surfaces.**
+ - Rebuild `ErrorBoundary`'s fallback as a branded, i18n'd recovery card (mirror
+ `AuthAccountError`): translated generic message + retry (re-render children / reload), raw
+ `error.toString()` + component stack rendered **only** in development. Keep the class component;
+ add the error-reporting hook point (a single function you can later wire to a service — no TODO
+ comment left behind).
+ - Add `src/app/[locale]/error.tsx` and `global-error.tsx` so errors thrown above the shells
+ (RoleGuard, providers, layouts) get the same branded treatment instead of Next's default. Respect
+ the root-layout constraint (`global-error` must render its own ``; keep locale/dir sane).
+3. **H-13 — localize the failure toasts.** `lib/api/client.ts:57,80,86,91`: replace the four English
+ literals with a small locale-keyed dictionary module (the fetch layer already knows the locale;
+ it can't call `useTranslations`). Add keys (suggest an `errors` namespace) to **both**
+ `messages/en.json` and `messages/fa.json`. Keep the error contract exactly (which statuses toast,
+ which throw) — only the copy source changes.
+4. **H-14 — capability-gate the four admin consoles.** Add a shared `CapabilityGuard`
+ (`need: keyof AdminCapabilities`) that early-returns the existing non-leaking access-denied state,
+ and wrap the page bodies of `admin/audit` (canViewAudit), `admin/verification` (canVerify),
+ `admin/tickets` (canManageTickets), `admin/roles` (canManageRoles) — before their data hooks fire.
+ Display convenience, not security (the server enforces after Phase 1) — but a scoped admin
+ deep-linking must see the denied card, not the data.
+
+## 4. Mocks & seams in this phase
+
+None.
+
+## 5. Critical rules you must not get wrong
+
+- `queryClient.clear()` on logout must run **after** the server revoke call is issued (keep the
+ existing revoke → clear-tokens → dispatch → redirect ordering; the cache clear joins it, doesn't
+ reorder it).
+- Don't toast inside hooks for 401/403/5xx (already toasted by `clientFetch`) — unchanged rule.
+- `global-error.tsx` replaces the root layout when it triggers — it must be self-contained (no
+ providers assumed), bilingual-safe, and never crash itself.
+- Every new user-visible string in both message files; shared components (`CapabilityGuard`, the new
+ boundary fallback if extracted) get co-located tests per the client testing rule.
+- Do not turn the boundary into a swallow-everything — rethrow/log in dev so DX doesn't regress.
+
+## 6. Definition of Done
+
+- Shared-device test: login A (customer with patients) → logout → login B → B never sees A's data
+ (verify React Query devtools cache is empty right after login).
+- Throw a test error inside a page in dev → branded Persian card with retry; stack visible in dev
+ build only. Route-level error files exist and render.
+- On `fa`, kill the API and trigger a fetch → Persian network-error toast; expire the session → Persian
+ session-expired toast + login redirect (Phase 0 behavior).
+- Finance-scoped admin deep-links `/admin/audit` → access-denied card, no data fetch fired.
+- `npm run check` + `npm run test:ci` green (new tests for CapabilityGuard + boundary fallback).
+
+## 7. How to test (human)
+
+1. Two seeded accounts on one browser: full logout/login swap watching the patients screen.
+2. Temporarily `throw new Error('boom')` in a page render; check both dev and `npm run build && start`.
+3. Stop the API mid-session on `/fa`; watch toast language; restart, expire tokens, watch the redirect.
+4. Login as 09120000021 (finance), deep-link the four consoles.
+
+## 8. Hand off & document
+
+- Tick H-11…H-14 in [issues.md](issues.md) with commit hashes.
+- Update `client/CLAUDE.md` (error contract copy source, error.tsx files in the structure tree,
+ logout lifecycle).
+- Write `dev/shared-working-context/reports/hardening-phase-3-report.md`.
diff --git a/dev/post-phase/hardening/hardening-phase-4-contract-completion.md b/dev/post-phase/hardening/hardening-phase-4-contract-completion.md
new file mode 100644
index 0000000..b57f416
--- /dev/null
+++ b/dev/post-phase/hardening/hardening-phase-4-contract-completion.md
@@ -0,0 +1,107 @@
+# Hardening Phase 4 — Contract completion batch (close the Tier B/C REQs)
+
+> Deliver the remaining open contract requests so the last seven mocked client domains have a real
+> backend to flip to: customer refunds, BNPL reads, review eligibility, family-record access, admin
+> console deltas, the admin halves of verification/refunds/payouts, and the partner portal's identity
+> signal + self-scoped reads. The sequel to refinement-phase-3 (which delivered Tier A).
+> **Track:** backend · **Depends on:** Phase 1 (RBAC — the admin endpoints here must be gated by the
+> now-working policy) · **Unlocks:** Phase 5
+> **Before you start, read [_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).**
+
+## 1. Context — where this sits
+
+Fixes **H-15, H-16a** from [issues.md](issues.md). The canonical spec for every item is the REQ entry
+in [for-backend.md](../../shared-working-context/frontend/requests/for-backend.md) plus the Tier B/C
+sections of [refinement-phase-3](../refinement/refinement-phase-3-contract-batch.md) — this file
+sequences and annotates; it does not restate shapes. **Re-check each REQ's `Status:` line before
+building — some were partially delivered** (REQ-025 nurse reads are live; REQ-027's GET/PUT
+care_record exists; REQ-017/my_review landed earlier; Phase 1 may have delivered REQ-031; Phase 2 may
+have appended new REQs — pick those up too).
+
+**What already exists (do not rebuild):** the b11 refund engine (fee-leg decomposition, clawback
+fork), the b12 BNPL machine, the b14 review/moderation + care-record entities, the b13 payout engine +
+`nurse_payouts` reads, the b6 verification pipeline + signed-URL storage seam, partner centers with
+kebab-case admin routes, and the refinement-phase-3 house pattern for contract deltas
+(migration + swagger regen + tests per REQ).
+
+## 2. Required reading (do this first)
+
+- `for-backend.md` — every REQ still `open`; the proposed shapes there are the contract.
+- [refinement-phase-3-contract-batch.md](../refinement/refinement-phase-3-contract-batch.md) §Tier B/C
+ + its "how to test" — mirror its delivery pattern.
+- `server/CLAUDE.md`, `server/CONVENTIONS.md`; the relevant `product/` docs per area (refund policy,
+ BNPL, verification, partners) — business rules are decisions, not guesses.
+- Phase 2's report + any REQs it filed.
+
+## 3. Scope — build this (one commit-sized slice per bullet, in this order)
+
+1. **REQ-019/020/021 — customer refunds** (unblocks `refunds`): `POST bookings/{id}/cancel`
+ (customer-initiated, tenancy-checked), `GET bookings/{id}/cancellation_policy` preview — and
+ **define the canonical `cancellation_policy_code` set** (the client invented
+ `free_24h`/`partial_under_24h`/`customer_no_show`; decide with `product/` and publish the enum),
+ `GET refunds/by_booking/{id}` with fee-leg decomposition. Reuse the b11 engine; a customer cancel
+ is never a self-issued refund — it routes through the existing policy/approval model.
+2. **REQ-022/023/024 — BNPL reads** (unblocks `bnpl`): `checkout_bnpl/options/{requestId}`,
+ `checkout_bnpl/schedule`, eligibility accepting the D3 KYC fields, wallet installments
+ (provider-reported — D5 is NOT a Balinyaar ledger), customer-facing `bookingId` on settle.
+3. **REQ-026 — reviews**: `bookings/{id}/review_eligibility` + confirm/finish `my_review` (verify
+ what already shipped) + the masked-author decision.
+4. **REQ-027 leftovers — family care record**: the `record_access` check endpoint; confirm the
+ product decision recorded for the family-owned record (the GET/PUT already exist with `long` ids —
+ the id-type reconciliation is client-side, Phase 5). Structured `taskResults` on visit notes if the
+ REQ still asks.
+5. **REQ-029/030 — admin console deltas**: config `updatedAt`/`updatedBy`; audit filters
+ (`actor_id`/`action`/`from`/`to`). (REQ-031 should be done by Phase 1 — verify, don't duplicate.)
+6. **REQ-034/035/036 — the admin halves** (unblock `verification`/`refunds`/`payouts` admin UIs):
+ verification queue + case + per-step decide + on-demand signed doc URL; refund preview/initiate/
+ approve/reject (ticket-linked, reusing b11); payout batch preview/run (idempotency-keyed)/retry/
+ record-transfer-reference (reusing b13). All gated with the Phase-1 policy + fine-grained roles
+ (finance vs moderation vs support scopes as seeded).
+7. **REQ-032/033/038 — partner portal**: `administersPartnerCenterId` on `MeResult` + a seeded demo
+ partner admin; `centers/me`, `centers/me/nurses`, `centers/me/bookings`, `centers/me/settlement`
+ (identity-derived tenancy — resolve the center from the caller, never from a client-sent id);
+ invoice `totalIrr`; activate/suspend toggle if still open.
+
+## 4. Mocks & seams in this phase
+
+No new vendor seams. Everything reuses existing engines behind existing seams. Update
+`mocks-registry.md` rows only where a "Make it real →" step is now shorter.
+
+## 5. Critical rules you must not get wrong
+
+- **Additive only** — no breaking DTO/route changes; the 14 already-real client domains must keep
+ working untouched.
+- Money invariants: refund Σ ≤ captured (409 otherwise), fee-leg decomposition, pre/post-payout fork
+ via `INursePayoutStatus`, BNPL net-of-fee model, payout invariant-to-method. All exist — wire, don't
+ re-derive.
+- Tenancy on every new customer/nurse/partner read (owner-or-staff, 404-not-403); partner reads are
+ identity-derived (`/me`-style), never id-parameterized.
+- Admin endpoints: `[Authorize(ConstantPolicies.DynamicPermission)]` + correct fine-grained scope —
+ Phase 1's tests must still pass.
+- Signed URLs short-lived, fetched on demand (the b6/f15 contract). Sequential invoice numbering and
+ Moadian states untouched.
+- Per-REQ: migration if schema changes, swagger regenerated into `dev/contracts/openapi/`, tests per
+ the refinement-3 pattern, `Status:` updated in `for-backend.md` in the same change.
+
+## 6. Definition of Done
+
+- Every REQ listed above is either delivered (status updated, tested) or explicitly re-deferred with
+ a reason recorded in `for-backend.md` — no silent skips.
+- `dotnet build Baya.sln` 0 new warnings · `dotnet test Baya.sln` green.
+- Swagger regenerated; the refinement-3 §7 smoke calls for Tier B/C pass against the seeded world
+ (customer cancel creates a refund; `centers/me` resolves the caller's center; admin
+ queues/previews return their shapes under the right roles).
+
+## 7. How to test (human)
+
+Per tier, via Swagger against the demo world: cancel a paid booking as its customer (policy preview →
+cancel → refund visible via by_booking); fetch BNPL options for an accepted request; open the
+verification queue as super_admin and decide a step; preview + run a payout batch as finance; log in
+as the seeded partner admin → `centers/me` chain returns their center only.
+
+## 8. Hand off & document
+
+- Tick H-15/H-16a in [issues.md](issues.md) (commit hashes per REQ group is fine).
+- Update `for-backend.md` statuses, `mocks-registry.md`, `server/CLAUDE.md` if the project map moved.
+- Write `dev/shared-working-context/reports/hardening-phase-4-report.md` + a handoff note listing
+ exactly which flags Phase 5 may now flip.
diff --git a/dev/post-phase/hardening/hardening-phase-5-final-demock.md b/dev/post-phase/hardening/hardening-phase-5-final-demock.md
new file mode 100644
index 0000000..24f2f8b
--- /dev/null
+++ b/dev/post-phase/hardening/hardening-phase-5-final-demock.md
@@ -0,0 +1,96 @@
+# Hardening Phase 5 — Final de-mock & partner reachability (zero mocks left)
+
+> Flip the last mocked client domains to the Phase-4 backend, reconcile the patientRecords id-type
+> mismatch that makes its PUT write-unsafe, and make the partner portal reachable from login and
+> actually gated. Exit criteria for the whole chain: **no `USE_*_MOCK = true` remains anywhere.**
+> **Track:** frontend · **Depends on:** Phases 2 & 4 · **Unlocks:** — (chain complete)
+> **Before you start, read [_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).**
+
+## 1. Context — where this sits
+
+Fixes **H-16b, H-17** and retires the remaining flags from [issues.md](issues.md). After Phase 2 the
+mocked surface is: `refunds`, `bnpl`, `admin`, `partnerCenter`, `patientRecords`, plus the admin
+halves of `verification`/`payouts`. Phase 4 delivered (or explicitly re-deferred) their REQs — its
+handoff note says exactly which flags may flip. This mirrors refinement-phase-4: **a flip is never
+assumed to be a pure flag flip** — check each `clientApi.ts` mapper against the delivered DTOs first
+(that phase's lesson: delivered fields were being null-overridden by stale mappers).
+
+**What already exists (do not rebuild):** every domain's real `clientApi.ts`, the seam-split
+mechanics from Phase 2, the partner portal screens, `useMyPartnerCenter`'s access-denied pattern, the
+Phase-0 auth gate.
+
+## 2. Required reading (do this first)
+
+- Phase 4's report + handoff note (which REQs landed; which were re-deferred — a re-deferred REQ means
+ its domain **stays mocked** and you record why, you don't force it).
+- [issues.md](issues.md) H-16b/H-17; the refinement-phase-4 file + report (the mapper-fix lesson).
+- `client/CLAUDE.md` de-mock status block; `mocks-registry.md` frontend section.
+- The regenerated swagger in `dev/contracts/openapi/` for every DTO you map.
+
+## 3. Scope — build this
+
+1. **H-17 — patientRecords id reconciliation, then flip.** Client `Medication`/`RoutineItem`/
+ `CareTask.id` are `string` ('m1'/'r1'/'t1' seeds) vs the wire's `long`
+ (`CarePlanDtos.cs:11-15`). Reconcile before flipping: existing items carry the server's numeric id
+ (stringified is fine if the type stays string — but the PUT must send what the server accepts);
+ **new items omit the id** and let the server assign. Update the mapper + `useUpdateCareRecord`'s
+ optimistic path; then `USE_PATIENT_RECORDS_MOCK=false`. The nurse visit-note half is already
+ contract-real — don't disturb it.
+2. **H-16b — partner reachability + gate.**
+ - `Me` gains `administersPartnerCenterId` (REQ-038); `resolveRoleDestination` gains the partner
+ branch (a partner admin lands on `/partner` after login; add the routing unit test).
+ - Gate the portal at the **layout** level: a `PartnerAccessGuard` in `partner/layout.tsx` resolves
+ `useMyPartnerCenter()` once and renders the non-leaking access-denied state on 403/404 before any
+ child page mounts; `nurses`/`bookings`/`settlement` pages stop fetching before the gate resolves.
+ - Flip `USE_PARTNER_MOCK=false` onto the `centers/me*` reads (REQ-032/033).
+3. **Flip the rest, mapper-checked, in dependency order:** `refunds` (REQ-019/020/021 — retire the
+ Phase-2 mock-side preview), `bnpl` (REQ-022/023/024 — restore the checkout CTA derivation to
+ enabled-when-real; delete the dev gateway-harness page if the real provider redirect replaces it),
+ `verification` admin half (REQ-034 — admin queue/case/decide/signed-URL onto real), `payouts` admin
+ half (REQ-036 — batch preview/run/retry/reference onto real), `admin` (REQ-029/030/031 — config/
+ audit/alerts/holidays/RBAC grid real; the RBAC grid consumes Phase 1's RolesController).
+4. **Cleanup + honesty pass.** Delete now-unused mockApi modules and cross-mock helpers
+ (`mockGetBookingForRefund`, `mockInsertConvertedBooking`, `__mockApproveAll`, …) unless a Testing
+ path genuinely uses them (then say so in the registry). No dead code. Update the
+ `client/CLAUDE.md` de-mock block (should read "22/22 real"), `mocks-registry.md` (frontend rows →
+ 🟢), and the project-structure tree if files moved/died.
+
+## 4. Mocks & seams in this phase
+
+This phase **retires** the frontend mock seams. The seam pattern itself stays (it's the test/dev
+affordance); only the defaults flip. Any domain that must stay mocked (re-deferred REQ) keeps a
+registry row with the reason + pull-trigger.
+
+## 5. Critical rules you must not get wrong
+
+- **Mapper-check every flip** against the real swagger — the refinement-4 lesson. A delivered field a
+ stale mapper drops is a silent regression.
+- Money stays served IRR digit-strings; D5 wallet is provider-reported, never a Balinyaar ledger; the
+ BNPL confirmation reuses the card confirmation (`?method=bnpl`).
+- Partner tenancy is server truth — the client guard is chrome; never pass a center id from the
+ client to the `me`-scoped routes.
+- Published-only reviews, two-stage disclosure, nurse append-only records — unchanged invariants.
+- Per flip: run the affected screen against the live server before moving to the next flag.
+
+## 6. Definition of Done
+
+- `grep -rn "USE_.*_MOCK = true" client/src/services` → empty (or each survivor justified in the
+ registry + issues.md).
+- Journey A end-to-end on real data: login → search → request → accept → pay (card AND BNPL) →
+ booking → cancel/refund path visible → review. Journey B: verification → publish → inbox → accept →
+ EVV → earnings. Admin: verification decide, refund approve, payout run. Partner: login →
+ auto-routed → own center only.
+- `npm run check` + `npm run test:ci` green; no dead mock code.
+
+## 7. How to test (human)
+
+Run the two journeys + the admin/partner spot-checks above against the RUNBOOK setup. The demo world
+personas: customers 0912000000x, nurses (incl. unverified 09120000003), admins 0912000002x, plus the
+Phase-4 partner admin seed.
+
+## 8. Hand off & document
+
+- Tick H-16b/H-17 (and the flip checklist) in [issues.md](issues.md) with commit hashes.
+- Final updates: `client/CLAUDE.md`, `mocks-registry.md`, `for-backend.md` (all REQs terminal).
+- Write `dev/shared-working-context/reports/hardening-phase-5-report.md` declaring the chain complete
+ (or listing exactly what remains and why).
diff --git a/dev/post-phase/hardening/issues.md b/dev/post-phase/hardening/issues.md
new file mode 100644
index 0000000..86f6438
--- /dev/null
+++ b/dev/post-phase/hardening/issues.md
@@ -0,0 +1,163 @@
+# Hardening issue ledger (verified findings)
+
+Every item below was found by one audit agent and **re-confirmed by an independent adversarial
+verifier** reading the code (and, for H-01, probing the running dev server). Evidence is file:line at
+the time of the audit (2026-07-16) — re-locate if lines drifted, but do not re-litigate the finding.
+
+**This file is the loop's progress state.** When a phase fixes an item, tick it `[x]` and append
+`— fixed in ` on the same line. `LOOP-PROMPT.md` reads this file to decide what's left.
+
+---
+
+## Phase 0 — auth gate & session liveness
+
+- [ ] **H-01 (blocker) — the auth gate never executes at runtime.**
+ `client/middleware.ts` is correct as written (PUBLIC_PATHS = `['/login']`, redirect on dead token)
+ but never runs: live cookie-less `GET /fa/admin` → 200 full HTML, bare `GET /` → 404 (no next-intl
+ locale redirect either) — reproduced under Turbopack **and** `--webpack`. Cause: Next.js infers the
+ workspace root from a stray `C:\Users\Lenovo\pnpm-lock.yaml` (startup warning names it), so the
+ middleware file is never bound. Also: Next 16 deprecates `middleware.ts` for `proxy.ts`, and the
+ docs place the file "at the same level as `pages` or `app`" — for this repo that's `client/src/`.
+ Fix: pin `turbopack.root` (+ `outputFileTracingRoot`) in `client/next.config.mjs`; migrate the file
+ to `src/proxy.ts`; **prove the gate runs with a runtime probe** (curl DoD).
+
+- [ ] **H-02 (blocker) — `isTokenAlive` can never read the real token (JWE).**
+ Server: `server/src/Infrastructure/Baya.Infrastructure.Identity/Jwt/JwtService.cs:116-128` —
+ `EncryptingCredentials(Aes128KW, Aes128CbcHmacSha256)` ⇒ the access token is a 5-segment JWE.
+ Client: `client/src/lib/auth/token.ts:24-38` JSON-parses segment 1 ⇒ always `null` ⇒
+ `isTokenAlive` always `false` for real tokens. Consequences: (a) once H-01 is fixed the middleware
+ would redirect-loop **logged-in** users; (b) `getServerAuthState` (`client/src/lib/auth/server.ts`)
+ seeds `isAuthenticated=false` on every hard reload. The wire already returns
+ `accessExpiresAt`/`refreshExpiresAt` (`client/src/services/auth/types.ts:62-66`): persist a
+ readable companion expiry cookie in `persistAuthTokens` and check that instead of decoding.
+
+- [ ] **H-03 (high) — anonymous visitor to a private shell = infinite splash, never a login ask.**
+ `useMe()` is enabled only when `isAuthenticated`; for a cookie-less session that's permanently
+ `false`, so `useRoleHydration()` never leaves `loading` and `RoleGuard`
+ (`client/src/components/auth/RoleGuard.tsx:55`) renders `AuthSplash` forever (splash markup
+ confirmed in served HTML). Add an explicit `unauthenticated` state → redirect to `/login?next=…`
+ with return-URL honored post-login.
+
+## Phase 1 — admin RBAC & staff role scopes (server)
+
+- [ ] **H-04 (blocker) — DynamicPermission RBAC is structurally dead; the whole admin API 403s.**
+ `DynamicPermissionService.cs:9` passes only the literal role `"admin"`; the seeded personas hold
+ `super_admin`/`finance` (`DemoWorldDefinitions.cs:144-145`, distinct constants `RoleNames.cs:13,17`).
+ No RoleClaim is ever seeded, and the only claim writer
+ (`RoleManagerService.ChangeRolePermissionsAsync`, `RoleManagerService.cs:145-208`) has **no HTTP
+ surface** (the `Features/{Role,Admin}` handlers exist but no controller exposes them). Every
+ `[Authorize(ConstantPolicies.DynamicPermission)]` endpoint (all Admin* controllers, PlatformConfig,
+ Holidays, Audit, SupportAlerts, Reviews-moderate) 403s for both seeded admins. RUNBOOK.md:115-130 and
+ refinement-phase-2-report falsely document these personas as working — fix the docs too.
+
+- [ ] **H-05 (high) — `BookingRoles.Admin` over-grants: Support/Moderation get clinical + financial access.**
+ `Features/Bookings/BookingRoles.cs:8-9` bundles Admin/SuperAdmin/Support/Finance/Moderation; used by
+ `GetCareInstructionsQuery.Handler.cs:31,37` (encrypted clinical notes),
+ `TransitionBookingStatusCommand.Handler.cs:24-25,51-54` (force `Completed` ⇒ arms the payout
+ dispute-window), `GetNursePayableBalanceQuery.Handler.cs:22-24` (contradicts its own comment).
+ Controllers carry only blanket `[Authorize]` — the handler set IS the boundary. The codebase's own
+ convention is narrower (`PatientAccess.cs:17` = [Admin, SuperAdmin]; `StaffRoles.cs:5-13`
+ "narrowest fitting scope"). Split into `ClinicalAccess` / `Financial` sets and update the handlers.
+
+## Phase 2 — money-path mock integrity (client)
+
+- [ ] **H-06 (blocker) — refunds mock reads the retired bookings-mock store → real cancel 404s.**
+ `services/refunds/apis/mockApi.ts:3,88` imports `mockGetBookingForRefund` from
+ `services/bookings/apis/mockApi.ts` whose fixture array (ids ~5001-5005, `:340-345` throws 404
+ otherwise) is orphaned since `USE_BOOKINGS_MOCK=false`. Cancel CTA renders for every real booking
+ (`bookings/[id]/page.tsx:79-104`) then the preview errors (`bookings/[id]/cancel/page.tsx:50-78`).
+
+- [ ] **H-07 (blocker) — BNPL wizard runs on a disconnected mock store keyed by the real request id.**
+ `checkout/page.tsx:213-224` pushes the **real** `requestId` (payment+bookingRequests are real) into
+ the BNPL branch; `services/bnpl/apis/mockApi.ts:5-8` hard-imports `bookingRequestsMockApi` +
+ `mockInsertConvertedBooking` (raw mock modules, bypassing both domains' seams); store holds only
+ fixture ids 1-2 → 404, or on id collision `settle()` (`:260-317`) fabricates a client-side "booking"
+ while the real request expires unpaid. `BNPL_ENABLED=true` (`services/payment/constants.ts:23`) has
+ no gating. Stopgap: hide/derive the CTA while `USE_BNPL_MOCK && !USE_BOOKING_REQUESTS_MOCK`; any
+ surviving mock must resolve requests through the real `bookingRequestsApi` selector.
+
+- [ ] **H-08 (blocker) — verification is 100% mocked while catalog/search are real → invisible nurses.**
+ `services/verification/constants.ts:8-9` `USE_VERIFICATION_MOCK=true` gates the WHOLE
+ `VerificationApi` (nurse flow + TrustBadge + admin queue, one interface — `types.ts:255-265`).
+ The mock's `__mockApproveAll` never flips the real `nurse_profiles.is_verified`
+ (`NurseProfile.cs:44-53`, private setter, only the real b6 `VerificationAggregator` calls it), but
+ variant activation is real and unconditional (`SetVariantActiveCommand.Handler.cs:15-36`), and the
+ search gate requires `IsVerified` (`SearchIndexMaintainer.cs:34,65,94,152`). `PublishGate.tsx:59-68`
+ CTA makes no API call at all. Fix: split the seam (nurse half real now — clientApi is 1:1 per the
+ registry; admin half stays mocked until REQ-034) and wire/gate the publish UI honestly.
+
+- [ ] **H-09 (high) — nurse earnings fabricated although its endpoints are live.**
+ `services/payouts/constants.ts:13` `USE_PAYOUTS_MOCK=true`, but REQ-025 was **delivered** —
+ `NursePayoutsController.cs:33-48` serves earnings_balance/earnings/{id}, and
+ `payouts/apis/clientApi.ts:163-187` already implements them. The flag is held hostage by the admin
+ batch methods (REQ-036, still open). Split the seam per-method (nurse reads real, admin mock).
+
+- [ ] **H-10 (medium) — payment outcome discards the delivered `bookingId`.**
+ `services/payment/apis/clientApi.ts:55-75` types the response as `Omit<…,'bookingId'>` and sets
+ `bookingId: null` (line 72) although `BookingRequestDto.cs:46-49` carries it (REQ-017 delivered).
+ Kills the confirmation deep links (`checkout/return/page.tsx:84`,
+ `confirmation/page.tsx:94-110`) and the targeted cache invalidation (`invalidations.ts:17-24`).
+ Read it off the wire; drop the stale REQ-017-pending comments.
+
+## Phase 3 — session & error-surface hardening (client)
+
+- [ ] **H-11 (high) — logout/login never clear the query cache → cross-account data leak.**
+ `useLogout.ts:22-30` removes only `authKeys.all` (`['auth']`); `useVerifyOtp.ts:25-32` /
+ `useSelectRole.ts:19-38` touch only auth keys; `queryClient.ts:17-27` is a tab-lifetime singleton;
+ domain keys carry no user id. User B logging in after User A on the same device is served A's cached
+ patients/bookings/addresses/tickets until staleTime lapses. Fix: `queryClient.clear()` on logout +
+ reset on login.
+
+- [ ] **H-12 (high) — raw stack-trace error boundary; no route-level error pages.**
+ `components/common/ErrorBoundary.tsx:35-58` renders hardcoded-English `error.toString()` + full
+ component stack, no retry, TODO-only reporting; used at `CustomerLayout.tsx:78` and
+ `TopBarAndSideBarLayout.tsx:104` — and sits BELOW RoleGuard/providers, while **no `error.tsx` or
+ `global-error.tsx` exists anywhere** under `src/app`. Replace with a branded i18n recovery card
+ (mirror `AuthAccountError`), dev-only stack, and add `[locale]` route-level error files.
+
+- [ ] **H-13 (high) — 401/403/5xx/network toasts are hardcoded English.**
+ `lib/api/client.ts:57,80,86,91` — four `dispatchToast('…English…')` literals pass through
+ `dispatchToast.ts`/`ToastBridge.tsx` verbatim; no keys exist in either messages file. Violates the
+ project's golden rule #3 on the fa-default app. Localize via a locale-keyed dictionary (non-React
+ call site — the fetch layer already computes the locale).
+
+- [ ] **H-14 (medium) — admin read-only consoles unguarded client-side.**
+ `admin/audit/page.tsx:25`, `admin/verification/page.tsx:50`, `admin/tickets/page.tsx:43`,
+ `admin/roles/page.tsx:50,121` fetch/render with no `useAdminCapabilities()` check — only the
+ sidebar (`AdminLayout.tsx:24-33`) hides them. (Server enforcement exists but is the dead
+ DynamicPermission policy — H-04.) Add a `CapabilityGuard` early-return with the non-leaking
+ access-denied pattern.
+
+## Phase 4 — contract completion batch (server; Tier B/C REQs)
+
+- [ ] **H-15 (medium) — the deferred REQ set, per the ledger** (`for-backend.md` — re-check each
+ status before building; REQ-031's HTTP surface may already exist after Phase 1):
+ REQ-019/020/021 customer refunds (cancel command, policy preview + canonical
+ `cancellation_policy_code` set, refund-by-booking + decomposition) · REQ-022/023/024 BNPL
+ (options/schedule/wallet-installments, eligibility KYC fields) · REQ-026 reviews
+ (eligibility/my-review reads — verify what phase 3/4 already delivered) · REQ-027 leftovers
+ (`record_access` check; the GET/PUT care_record endpoints already exist —
+ `PatientCareRecordsController.cs:44-54`) · REQ-029/030 admin config `updatedAt`/`updatedBy` + audit
+ filters · REQ-034 admin verification queue/signed-doc-URL/approve · REQ-035 admin refund
+ preview/initiate/approve/reject · REQ-036 admin payout preview/run/retry/record-reference.
+
+- [ ] **H-16a (medium, server half) — partner portal has no identity signal or self-scoped reads.**
+ REQ-038: `administersPartnerCenterId` on `MeResult` + a seeded demo partner admin; REQ-032/033:
+ `centers/me[/nurses|/bookings|/settlement]` split reads + invoice `totalIrr`. `Me` interface
+ (`services/auth/types.ts:79-91`) has no partner field; `resolveRoleDestination`
+ (`services/auth/routing.ts:32-47`) has no partner branch.
+
+## Phase 5 — final de-mock & partner reachability (client)
+
+- [ ] **H-16b (medium, client half) — partner routing + portal gate + flip `USE_PARTNER_MOCK`.**
+ Add the partner branch to `resolveRoleDestination` off the new `/me` signal; gate the portal at the
+ layout level (today `partner/layout.tsx:15` RoleGuard has no `expected`, the mock resolves a
+ hardcoded center for ANY caller — `partnerCenter/apis/mockApi.ts:237-243` — and `nurses`/`bookings`
+ pages don't even await `useMyPartnerCenter`).
+
+- [ ] **H-17 (medium) — patientRecords id-type mismatch; flip the remaining flags.**
+ Client `Medication/RoutineItem/CareTask.id: string` (`patientRecords/types.ts:36,45,53`, mock seeds
+ 'm1'/'r1'/'t1') vs wire `long Id` (`CarePlanDtos.cs:11-15`) — a naive flip 400s every family-record
+ save; new items should omit id (server assigns). Then flip every remaining `USE_*_MOCK` (refunds,
+ bnpl, payouts-admin, admin, partnerCenter, verification-admin, patientRecords) as its REQs land;
+ delete the orphaned cross-mock helpers; update `mocks-registry.md` + `client/CLAUDE.md`.
diff --git a/dev/post-phase/ui/README.md b/dev/post-phase/ui/README.md
new file mode 100644
index 0000000..d43f73c
--- /dev/null
+++ b/dev/post-phase/ui/README.md
@@ -0,0 +1,196 @@
+# UI phases — giving Balinyaar the interface its product deserves
+
+**Created:** 2026-07-16 · **Scope:** `client/` only (frontend track) ·
+**Method:** a 15-agent parallel design audit over every UI surface of the client (theme/tokens, shells,
+component primitives, feature widgets, auth, storefront, booking, checkout/money, account, nurse workspace,
+nurse trust/ops, admin/partner, messaging/notifications, cross-cutting UX patterns, fa/en microcopy).
+The full per-area findings — with file/line evidence — live in [`audit/`](audit/).
+
+This directory is a **runnable chain of 14 UI phases** (0–13) that takes the client from *"feature-complete
+but visually a default-MUI starter"* to *"a designed, branded, Persian-native product."* The functionality is
+already there — this chain is about **look, feel, hierarchy, trust presentation, mobile/RTL/Persian
+nativeness, and the UX defects the audit surfaced along the way.** Run the phases **in order, one at a time**,
+pointing a fresh agent at one phase file (*"Execute `dev/post-phase/ui/ui-phase-0-design-language.md` end to
+end"*). After phases 0–2, most later phases touch disjoint route trees and can be parallelized if you accept
+some risk; the recommended path is sequential.
+
+> **Relation to the other chains.** The [refinement chain](../refinement/README.md) made the app *work* as one
+> integrated system (it is complete). This chain makes it *feel* like a product. It deliberately does **not**
+> touch `server/` — where a UI improvement needs a backend change, the phase files a REQ in
+> [`for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md) and builds mock-tolerant
+> UI behind the existing `services/{domain}` seams (REQ-001…038 are taken; number onward from there).
+
+---
+
+## What is already good (do not regress it)
+
+The audit found the feature layer unusually disciplined. Every phase must preserve:
+
+1. **Token discipline** — 331 `var(--bal-*)` usages across 103 files, effectively **zero hard-coded hexes**
+ in feature code. The two-layer token system (`tokens.css` ↔ `colors.ts`) works; extend it, never bypass it.
+2. **The four-state data pattern** — skeleton → error-with-retry → empty → data is genuinely implemented on
+ most feature pages (search results, earnings, verification, tickets…). The gaps are enumerated per phase.
+3. **RTL as a habit** — logical properties (`borderInlineStart`, `textAlign: 'start'`, `marginInline*`),
+ deliberate `dir="ltr"` islands for phone numbers/IBANs/clocks, RTL-mirrored message bubbles.
+4. **Persian correctness plumbing** — `utils/money.ts` (BigInt IRR, Toman-at-the-boundary, fa digit
+ grouping), `utils/date.ts` (Shamsi via `Intl` `fa-IR-u-ca-persian`), locale digits in counts and pagers.
+5. **Product-honesty components** — `EscrowNotice` (product-mandated verbatim copy), `RefundEtaBanner`'s
+ honest BNPL 7–10-day window, `TrustBadge`'s three honest states, EVV's advisory-never-blocking semantics,
+ the two-stage clinical/address disclosure gates, negative-balance "owed back" rendering.
+6. **The architecture seams design work needs** — the `AppIcon` string registry (one-file icon swap), the
+ per-actor shell split (`CustomerLayout`/`NurseLayout`/`AdminLayout`/`PartnerLayout`), component defaults in
+ `components/config.ts`, the `services/{domain}` mock seams, the admin composite layer
+ (`AdminDataTable`/`ConfirmDialog`/…), optimistic ticket send with `clientMessageId` reconciliation.
+7. **Copy that is already right** — the escrow/payout/refund explainers, the culturally-tuned gender copy,
+ the formal-شما register, specific actionable verification-failure reasons.
+
+---
+
+## What is actually wrong (the problem inventory)
+
+Every item is verified in code; file/line evidence is in [`audit/`](audit/) and repeated in the phase files.
+
+### A. The starter is still the face of the app
+- `createTheme` has **zero `components` overrides** (`theme/theme.ts`) — every Button/Card/TextField/AppBar/
+ Chip/Dialog/Stepper renders stock Material with recolored primaries. This alone is most of the
+ "old MUI beginner example" feel.
+- The **brand logo is the starter's Twemoji cartoon pencil** (`AppIcon/icons/PencilIcon.tsx`), rendered at
+ 56px on the auth screens and in every top bar. The seed-deck mark (deep-teal square, cream glyph,
+ terracotta dot) was never built.
+- The nurse/admin/partner chrome is the untouched starter shell: solid-primary fixed `TopBar` with a static
+ centered label, flat 10-item sidebar whose **active highlight never fires** (locale-prefix bug in
+ `SideBarNavItem`), a permanent `UserInfo` placeholder ("Current User" / "Loading..." in English), physical
+ `paddingLeft/Right` RTL hazards, and a desktop SSR mobile-first flash.
+- The login page — the only front door — is wrapped in starter dashboard chrome titled **"Unauthorized -
+ Balinyaar"** in English; `ErrorBoundary` is unstyled English with a raw stack dump; `globals.css` is the
+ starter reset; `light.ts`/`dark.ts` are dead starter themes still exported.
+
+### B. The design system is missing a layer
+- MUI palette has **no success/error/warning/info**, so inline alerts show stock MUI green/red while toasts
+ show brand `--bal-*` colors — two feedback systems for one semantic state.
+- **Persian typography is Roboto metrics**: non-zero letter-spacing on a joined script, tight heading
+ line-heights that clip Persian ascenders, requested weight 600 that Mikhak doesn't load (renders 700), an
+ EN brand font declared but never wired.
+- Tokens cover **color only** — no elevation/shadow (grey MUI shadows on warm cream), radius scale, motion,
+ or focus-ring tokens; exactly **one** `:focus-visible` style exists in the whole app.
+- The icon registry mixes filled/outlined generations, has **no back/chevron icon at all**, and `AppIcon`'s
+ `size` prop is silently broken for MUI icons. `AppButton` ships a starter `margin: 1` default that 213
+ call-sites in 82 files neutralize with `sx={{ m: 0 }}`.
+- Missing primitives pages keep hand-rolling: EmptyState/ErrorState (29 dashed-Paper copies in 23 files),
+ PageHeader, Money display, card anatomy (~12 hand-rolled Paper recipes), skeleton twins, relative time,
+ a `formatNumber` helper (the `fa-IR` ternary is copy-pasted 25+ times) — and **no Jalali date picker**,
+ so every date input in a Shamsi-displaying product is a native Gregorian `type="date"`.
+- No route-level `loading.tsx`/`error.tsx`/`not-found.tsx` anywhere; one static `` for ~60 routes.
+
+### C. Trust surfaces undersell the product
+Trust *is* the product, and the UI treats it generically: a bare login card with no trust presence; a static
+✓ chip that never explains **what** was verified; a checkout whose total is a `subtitle2` row; a payment
+confirmation with **no reference code**; a nurse verification journey with two competing progress metaphors;
+a nurse public profile that doesn't answer "would I let this person into my mother's home?".
+
+### D. Real UX defects found along the way (fixed by their area's phase)
+The audit found genuine bugs beyond styling — the worst: a cancel-request dialog whose **dismiss button is
+labeled with the destructive action**; the error→false-empty pattern (a failed query renders "you have no
+patients/services/requests"); **no route to a customer's pending requests** (`useCustomerRequests` is wired
+to nothing); **no sign-out anywhere in the customer shell**; `PublishGate`'s **fake success snackbar**;
+bookings list unpaginated (booking #21 unreachable); the nurse day-of flow has **no address, no contact, no
+navigation affordance**; `DocumentUpload`'s rejected-state re-upload shows no progress; partner booking
+statuses render as raw English `snake_case`; admin actions target users by hand-typed numeric ID.
+
+### E. Mobile-native and Persian-native gaps
+No safe-area handling under the customer/nurse bottom navs; OTP inputs without `autocomplete="one-time-code"`
+/ WebOTP in an OTP-first market; Gregorian date pickers everywhere; Latin digits in timers; fa catalog missing
+ICU plural/zero forms («مشاهده ۰ پرستار»); desktop rendered as an 800px phone column with a mobile tab bar.
+
+### F. Copy defects on trust-critical strings
+The brand name is spelled two ways («بالین یار» vs «بالینیار»); تأیید appears with and without hamza (63
+occurrences, both forms); grammar bugs sit on nurse-facing EVV errors and customer address hints; BNPL copy
+uses banker's jargon (نکول) and one sentence states the **inverse** of the intended risk allocation;
+policy numbers (72h dispute window, cancellation tiers) are hard-coded into copy the admin config can change.
+
+---
+
+## The 14 UI phases
+
+Phases 0–2 build the foundation everything else composes from. Phases 3–11 are area redesigns over disjoint
+route trees. Phase 12 is the closing sweep; phase 13 is optional and product-gated.
+
+| # | Phase | Delivers | Depends on |
+| --- | --- | --- | --- |
+| **0** | [Design language & theme foundation](ui-phase-0-design-language.md) | brand mark, `theme.components` pass, semantic palette, Persian type scale, one icon family, token extension (elevation/motion/focus/rating/trust/money), starter purge | — |
+| **1** | [Shared primitives & app-wide states](ui-phase-1-primitives-and-states.md) | EmptyState/ErrorState kit (+ error→false-empty fixes), PageHeader, card kit, ``, Jalali date picker, StatusChip v2, StatusTimeline, CountdownTimer v2, RatingInput v2, skeleton twins, route-level loading/error/404, per-route metadata, formatting utils | 0 |
+| **2** | [Shells & navigation](ui-phase-2-shells-and-navigation.md) | locale-aware nav (`createNavigation`), contextual customer header + safe-area bottom bar, grouped nurse sidebar + nurse bottom nav + identity card, dense admin chrome, de-startered public shell, sign-out / role / locale switchers | 0, 1 |
+| **3** | [Auth & first-run](ui-phase-3-auth-and-first-run.md) | trust-forward login hero, OTP autofill (WebOTP), terms/privacy consent + pages, illustrated select-role, focused onboarding wizard, returnUrl | 0–2 |
+| **4** | [Customer storefront](ui-phase-4-customer-storefront.md) | home that sells, Shamsi date filter, sticky search CTA, honest results header + NurseResultCard v2, nurse profile as trust dossier + verification explainer | 0–2 |
+| **5** | [Booking lifecycle](ui-phase-5-booking-lifecycle.md) | C4 trust-anchored request form, C5 countdown ring + recovery, customer requests tabs on /bookings, booking-detail hero + vertical timeline, cancel-flow fixes, review context | 0–2 (4 recommended) |
+| **6** | [Checkout & money](ui-phase-6-checkout-and-money.md) | checkout hierarchy + sticky pay bar, receipt-grade confirmation, wallet as money hub, honest BNPL comparison, fiscal-grade invoice, designed wait states | 0–2 (5 recommended) |
+| **7** | [Nurse daily ops](ui-phase-7-nurse-daily-ops.md) | the real nurse dashboard, visit workspace (address/contact/EVV-first), decision-first request inbox + urgency system, earnings clarity | 0–2 |
+| **8** | [Nurse business & verification](ui-phase-8-nurse-business-and-verification.md) | activation checklist (honest PublishGate), unified verification journey, DocumentUpload fixes, variant-builder preview, coverage viz, bank flows, public-profile preview | 0–2 (4 for trust components) |
+| **9** | [Customer account & care circle](ui-phase-9-customer-account-and-care-circle.md) | profile as account hub (+ sign-out entry), care-circle reframe with avatars, per-item care-record editing, real map picker, mobile full-screen forms | 0–2 |
+| **10** | [Messaging & notifications](ui-phase-10-messaging-and-notifications.md) | chat-grade ticket threads (live, scrolled, grouped), inbox pagination/filters, right-sized emergency affordance, notification center grouping + bell popover | 0–2 |
+| **11** | [Admin & partner console](ui-phase-11-admin-and-partner-console.md) | URL-synced list state, user/nurse pickers (no raw IDs), verification desk, ticket console lifecycle, AdminDataTable v2, Jalali inputs, partner localization | 0–2 |
+| **12** | [Copy, motion & final polish](ui-phase-12-copy-motion-and-polish.md) | Persian style guide + catalog sweep, ICU plurals, trust-moments copy, config-served policy numbers (REQ), motion pass, a11y sweep, 4-axes QA | all prior |
+| **13** | [Public front door](ui-phase-13-public-front-door.md) *(optional)* | public landing, how-it-works, SEO/metadata/OG, guest-browse decision (+ public-endpoint REQs) | 0–2 · product decision |
+
+### Dependency & sequencing
+
+```
+Foundation (strictly in order):
+ 0 design language ──► 1 primitives & states ──► 2 shells & navigation
+
+Area redesigns (after 0–2; sequential recommended, parallel possible across disjoint trees):
+ 3 auth/first-run
+ 4 storefront ──► 5 booking ──► 6 checkout/money (the customer funnel, in funnel order)
+ 4 ──────────────► 8 nurse business & verification (reuses phase 4's trust components)
+ 7 nurse daily ops
+ 9 customer account · 10 messaging/notifications · 11 admin/partner
+
+Closing:
+ 12 copy, motion & polish (last — sweeps everything the earlier phases touched)
+ 13 public front door (any time after 0–2; needs a product decision on guest browse)
+```
+
+**Ownership rules that keep parallel runs safe:** phase 0 owns `theme/`, `AppIcon`, `AppButton`, the brand
+mark; phase 1 owns every *shared* primitive (`StatusChip`, `CountdownTimer`, `RatingInput`, `StepperHeader`
+usage, state views, ``, the Jalali picker); phase 2 owns `layout/`. Later phases **consume** these and
+own only their route tree plus the feature components that belong to it (e.g. `NurseResultCard` → 4,
+`BookingRequestSummaryCard` → 5, messaging composites → 10). If a later phase finds a foundation gap, it
+extends the foundation file *minimally* and notes it in its report — it does not fork a local variant.
+
+**Minimum path to "the app suddenly looks designed":** phases **0 → 1 → 2**. Those three de-starter every
+screen at once (theme pass + primitives + chrome); phases 3–11 are then per-area redesigns on a system that
+already looks right.
+
+---
+
+## How the phase files are written
+
+Each file follows the repo's [phase template](../../phases/_shared/phase-template.md): a one-paragraph
+mission, context (what already exists — don't rebuild), required reading (including the relevant
+[`audit/`](audit/) files, which carry the full evidence), enumerated scope with file paths, the mocks/REQ
+posture, the invariants it must not break (including the area's **do-not-regress** list), a Definition of
+Done, concrete how-to-test steps across the four axes (`/fa` + `/en` × light + dark, mobile + desktop), and a
+close-out (docs + report + memory).
+
+Non-negotiables for every phase (they restate them, but for the human reader):
+
+- **Invoke the `frontend-designer` skill before any UI work** — it is the design contract (brand palette,
+ token rules, `App*` wrappers, icon registry, RTL/dark-mode/i18n rules).
+- **`npm run check` green + `npm run test:ci`** when shared components change; new shared components get
+ co-located tests; `en.json`/`fa.json` stay in sync.
+- **Frontend lane only.** Backend gaps become REQ entries in
+ [`for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md), never edits to `server/`.
+- **Verify visually on `/fa` first** — it is the default locale and RTL.
+
+## Related documents
+
+- [`audit/`](audit/) — the 15 per-area audit reports (current state, problems with evidence, opportunities,
+ keep-lists) this chain was synthesized from.
+- [../refinement/README.md](../refinement/README.md) — the integration/production chain that preceded this
+ one (complete).
+- [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md) — how an
+ executing agent works a phase; every phase file links it.
+- [.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md) — the
+ design contract this chain builds on (and extends: phase 0 updates it when tokens/icons change).
+- [../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
+ — the REQ tracker; UI phases append REQ-039+ here.
diff --git a/dev/post-phase/ui/audit/admin-partner.md b/dev/post-phase/ui/audit/admin-partner.md
new file mode 100644
index 0000000..071817d
--- /dev/null
+++ b/dev/post-phase/ui/audit/admin-partner.md
@@ -0,0 +1,76 @@
+# Admin backoffice + partner portal (client/src/app/[locale]/(private-routes)/admin + /partner, client/src/components/admin, client/src/layout)
+
+## Current state
+
+The backoffice is in far better shape than "15 hand-rolled tables": there is a real shared primitive layer in client/src/components/admin — AdminDataTable (typed columns, dense, horizontal-scroll container, align defaults to 'inherit' for RTL), AdminPageHeader, AdminPager (prev/next only), AdminEmptyState, AdminErrorState, and a genuinely good ConfirmDialog (required-reason gating, loading-disables-buttons, destructive color) — and every console actually uses them. Every list page follows the same skeleton→error→empty→table/cards branch, filters live in the page-header actions slot or a bordered filter row (tickets/audit use a draft-vs-applied Apply pattern), and status colors flow through the shared StatusChip whose colors resolve from --bal-* semantic tokens (dark scheme covered in src/theme/tokens.css; I found zero hard-coded hexes anywhere in the admin/partner surface). Specialized composites (RefundPanel, DocumentViewer with on-demand signed-URL re-request, AdminMessageBubble with visually distinct internal notes, SupportAlertCard with a severity borderInlineStart accent, ConfigRow, AuditLogRow with an expandable field diff, PartnerSettlementRow with VAT-on-commission PriceBreakdown) cover the domain-heavy screens. Money renders as Toman via shared utils, dates as Shamsi, IBANs/reference codes are dir="ltr"-wrapped, and PII discipline (write-then-masked IBAN, never-echoed credential numbers, non-leaking partner access-denied state) is visible in the UI code itself.
+
+What drags it down is everything around the pages. The shell is the untouched open-source starter: TopBarAndSideBarLayout renders a default MUI AppBar with a centered nowrap title, a logo IconButton that doubles as the sidebar opener with a hard-coded English 'Open Sidebar' tooltip, physical left/right anchor constants in layout/config.ts, physical paddingLeft/Right compensation, and an 8px page gutter with no content max-width — the classic old-MUI-example look the owner already flagged, and the admin console inherits it wholesale. Worse, sidebar navigation is functionally degraded: SideBarNavItem computes selection via pathname.startsWith(path) against a locale-less ROUTES path while next-intl (localePrefix always, confirmed in client/middleware.ts) prefixes every pathname with /fa or /en — so the active console is never highlighted, and every sidebar click first hits a locale-normalization redirect. Above that, admin workflow affordances are thin: no filter/page state in the URL (back/refresh loses queue position), no sorting or text search on queues (verification filters only by status; tickets has no date/assignee/updated column), the pager indicator says only "صفحه {page}" with no total, users/notifications routes are PlaceholderScreens (notifications is a live sidebar item for every admin), config and holidays fetch page 1 forever with no pager, and people are everywhere referred to as raw numeric IDs — role grants, partner-center admin assignment, and nurse-roster assignment are typed into bare number inputs with no lookup. The partner portal is a small, clean, read-only surface (home/nurses/bookings/settlement on the same primitives) whose one glaring defect is rendering raw English snake_case wire codes as booking statuses to Persian-speaking center staff.
+
+## Problems (20)
+
+- **[high]** `client/src/layout/components/SideBarNavItem.tsx` — Active-item highlighting in the admin/partner sidebar never fires: selection compares a locale-less ROUTES path against the locale-prefixed pathname from next/navigation usePathname (localePrefix is always-on per client/middleware.ts), so startsWith is always false. Side effect: sidebar links navigate to locale-less URLs and eat a 307 locale redirect on every click.
+ - evidence: line 28: `const selected = propSelected || (path && path.length > 1 && pathname.startsWith(path)) || false;` — pathname is '/fa/admin/…', path is '/admin/…'
+- **[high]** `client/src/layout/TopBarAndSideBarLayout.tsx` — The whole backoffice sits in the untouched starter shell: default-blue-shadow MUI AppBar with a centered nowrap title, logo IconButton doubling as the sidebar opener, physical paddingLeft/paddingRight compensation keyed on anchor.includes('left') (works only by grace of stylis-plugin-rtl), and an 8px content gutter with no max-width — a dense worklist page starts 8px from the viewport edge and reads as an old MUI example, not a branded console.
+ - evidence: lines 53-60 physical padding keyed on anchor strings; line 71 hard-coded `'Open Sidebar'` tooltip; line 102 `paddingLeft: 1, paddingRight: 1` main gutter
+- **[high]** `client/src/app/[locale]/(private-routes)/partner/bookings/page.tsx` — Partner-facing booking statuses render as raw English snake_case wire codes ('pending_payment', 'in_progress') in both the filter menu and the table chip — an untranslated, un-StatusChip'd surface shown to external Persian-speaking center staff, breaking the app-wide localized-status-chip convention.
+ - evidence: line 17 comment 'labels are the codes themselves'; line 45 ` `; lines 66-69 `{s} `
+- **[high]** `client/src/app/[locale]/(private-routes)/admin/roles/page.tsx` — High-stakes audited actions target users by hand-typed numeric ID with no lookup or name echo-back: role grants here, partner-center admin assignment and sponsored-nurse assignment in partners pages. One mistyped digit grants super_admin to the wrong account or links the wrong nurse — and there is no Users console to even look an ID up (users page is a placeholder).
+ - evidence: GrantRoleDialog lines 176-182 ``; same pattern in partners/[id]/page.tsx lines 226-245 (assign nurse) and partners/page.tsx lines 238-244 (adminUserId)
+- **[medium]** `client/src/app/[locale]/(private-routes)/admin/alerts/page.tsx` — Assign-to-self silently falls back to user ID 1 when the current user isn't hydrated — an alert could be assigned to whoever user #1 is instead of the acting admin.
+ - evidence: line 36: `const meId = authState.currentUser?.id ?? 1;`
+- **[medium]** `client/src/app/[locale]/(private-routes)/admin/config/page.tsx` — Config list and its history drawer are hard-wired to page 1 with no pager — any platform_configs rows beyond the first page are invisible and uneditable from the UI. Same defect on holidays (useHolidays({}, 1)), where the calendar grows every year and will silently truncate.
+ - evidence: line 63 `usePlatformConfigs(1)`; line 182 `useConfigChangeHistory(configKey, 1, …)`; holidays/page.tsx line 38 `useHolidays({}, 1)`
+- **[medium]** `client/src/app/[locale]/(private-routes)/admin/tickets/page.tsx` — No admin filter/page state is URL-synced anywhere in the backoffice: applied filters and page live in component state, so browser back from a ticket, a refresh, or sharing a link with a colleague loses the queue position — detail pages even hand-roll 'back' buttons that router.push to the bare list. For a worklist tool this is a daily-use tax.
+ - evidence: lines 39-41 `useState` + `useState(1)` with no searchParams; tickets/[id]/page.tsx line 90 `router.push(`/${locale}${ROUTES.ADMIN_TICKETS}`)`
+- **[medium]** `client/src/app/[locale]/(private-routes)/admin/tickets/[id]/page.tsx` — The admin ticket thread has no status controls at all — no close/reopen and no assignee (services/tickets exposes no close mutation), so a resolved case can never leave the 'open' queue from the UI; and the message list has no scroll-to-latest, so long threads open scrolled to the oldest message.
+ - evidence: lines 118-127 render only chips for status; hooks dir has useAdminTicket/usePostAdminMessage but no close/assign hook; line 150 plain `maxHeight: 520, overflowY: 'auto'` Box with no scroll anchoring
+- **[medium]** `client/src/app/[locale]/(private-routes)/admin/tickets/[id]/page.tsx` — The internal-note vs public-reply distinction exists only as a small ToggleButtonGroup above the composer; the composer itself looks identical in both modes and the send button label never changes — an admin can easily post an internal note publicly. The bubbles are well-differentiated after the fact, but the safety cue is needed before send.
+ - evidence: lines 163-190: ToggleButtonGroup + plain TextField + single `{t('ticket_send')}` button; only the placeholder string changes with mode
+- **[medium]** `client/src/app/[locale]/(private-routes)/admin/verification/page.tsx` — The highest-traffic trust queue has no text search (nurse name/phone), no sorting, no queue counts, and no age/SLA signal — only a 3-value status select. The desk cannot prioritize by 'waiting longest' or find a specific applicant; submittedAt renders but is not sortable.
+ - evidence: lines 101-117: the only filter is a status TextField select; AdminDataTable (components/admin/AdminDataTable.tsx) has no sort affordance at all
+- **[medium]** `client/src/app/[locale]/(private-routes)/admin/audit/page.tsx` — Every date input across the backoffice is a native Gregorian type="date" field (audit from/to, payout preview window, holiday date, credential issued/expires) while every displayed date is Shamsi — Iranian ops staff must mentally convert calendars to filter or enter data; there is no Jalali picker anywhere.
+ - evidence: lines 63-78 two `type="date"` fields; payouts/page.tsx lines 235-250; holidays/page.tsx lines 139-146; verification/[nurseId]/page.tsx lines 415-433
+- **[medium]** `client/src/app/[locale]/(private-routes)/admin/payouts/[batchId]/page.tsx` — Detail-page headers follow four different patterns: verification case uses AdminPageHeader + back link, ticket thread has no page header (h6 inside a Paper), payout batch hand-rolls a raw h5, partner-center detail hand-rolls h5 + chips and its back button misuses the 'partners' icon as a back glyph. No shared detail-header/breadcrumb primitive exists.
+ - evidence: lines 52-64 raw `Typography variant="h5"`; partners/[id]/page.tsx lines 80-90 `startIcon="partners"` on the back button; tickets/[id]/page.tsx line 110 h6-in-Paper
+- **[medium]** `client/src/components/admin/AdminPager.tsx` — The pager shows only the current page with no total ('صفحه ۳'), even though every caller computes pageCount; there is no total-results count, page-size control, or jump — weak for an ops tool paging 20-25 rows at a time through large queues.
+ - evidence: client/messages/fa.json line 1194 `"page_indicator": "صفحه {page}"` (the non-admin namespace at line 992 has 'صفحه {page} از {total}' — the admin one lost the total)
+- **[medium]** `client/src/app/[locale]/(private-routes)/admin/notifications/page.tsx` — The notifications sidebar item is shown to every admin (show: true in AdminLayout) but leads to a PlaceholderScreen dead-end; the users route is likewise a placeholder — starter-style stub screens shipped inside a production nav.
+ - evidence: whole file is ` `; AdminLayout.tsx line 34 `{ title: t('notifications'), …, show: true }`; users/page.tsx same placeholder
+- **[medium]** `client/src/components/admin/AuditLogRow.tsx` — Actors and owners render as bare '#42' style numeric IDs across audit rows, alert cards, role grants, and payout previews — investigating 'who did this' requires leaving the tool; there is no name-resolution layer or link-to-user anywhere.
+ - evidence: line 48 `{entry.actorUserId != null ? `#${entry.actorUserId}` : '—'}`; SupportAlertCard.tsx line 74 `#${alert.ownerUserId}`; roles/page.tsx line 60 `render: (g) => `#${g.userId}``
+- **[low]** `client/src/layout/components/SideBar.tsx` — Hard-coded English strings in the fa-default shell chrome: the logout tooltip and the sidebar-open tooltip are untranslated literals.
+ - evidence: line 76 `title="Logout Current User"`; TopBarAndSideBarLayout.tsx line 71 `'Open Sidebar'`
+- **[low]** `client/src/components/admin/AuditLogRow.tsx` — The expandable diff row's chevron is static — it never rotates and the clickable header has no aria-expanded/button semantics, so open/closed state is invisible and the row isn't keyboard-toggleable.
+ - evidence: line 53 ` ` with no rotation transform; lines 38-42 onClick on a plain Stack
+- **[low]** `client/src/app/[locale]/(private-routes)/admin/config/page.tsx` — Dead ternary on the config edit field type — both branches are 'text'.
+ - evidence: line 152 `type={config.dataType === 'int' || config.dataType === 'decimal' ? 'text' : 'text'}`
+- **[low]** `client/src/app/[locale]/(private-routes)/admin/holidays/page.tsx` — Misleading constant/comment: TODAY_ISO is an empty string yet the comment claims it is 'seeded below via state default' — a new holiday's date field simply starts blank.
+ - evidence: line 106 `const TODAY_ISO = ''; // seeded below via state default so no Date at module load`
+- **[low]** `client/src/app/[locale]/(private-routes)/admin/payouts/page.tsx` — The payout window default is computed with toISOString() (UTC), so near local midnight in Tehran the prefilled start/end dates are off by one day from the admin's wall-clock date.
+ - evidence: line 56 `const isoDate = (d: Date): string => d.toISOString().slice(0, 10);`
+
+## Opportunities (11)
+
+- **Purpose-built backoffice shell to replace the starter chrome** (impact: high, effort: large) — One shell change fixes the whole area's first impression and daily ergonomics: slim top bar (breadcrumb trail + global search + bell), a collapsible rail sidebar with working active-state (use next-intl's locale-aware pathname/Link), content area with ~1440px max-width and 24px gutters, cream surface with the deep-teal rail. Fix the locale-less sidebar hrefs and the 'Open Sidebar'/'Logout' literals at the same time. Every one of the 21 pages inherits it for free.
+- **useAdminListState: URL-synced filters + page as a shared hook** (impact: high, effort: medium) — A small hook that reads/writes filters and page to searchParams (the customer search flow already does this pattern) and is adopted by all nine queue pages. Fixes back-button/refresh/share-a-link wholesale, makes detail-page 'back' a real history back, and costs each page a two-line change since filter state is already isolated.
+- **UserPicker/NursePicker autocomplete to kill raw-ID inputs** (impact: high, effort: medium) — One shared async Autocomplete (search by name/phone, renders name + masked phone + id, echoes the resolved name in the confirm dialog) dropped into role grant, partner admin assignment, sponsored-nurse assignment, and alert assignment. Turns the scariest wrong-target failure mode in the backoffice into a non-issue and gives the ConfirmDialog copy a human name instead of '#42'. Needs one small lookup endpoint (or the future users console's list API).
+- **Workbench home: queue counts and aging on the console cards** (impact: high, effort: medium) — The admin overview cards are pure links today. Add per-console live counts (pending verifications, open tickets, unresolved alerts, reviews awaiting moderation, next payout window + eligible total) and an 'oldest waiting' age chip. This converts the landing page from a menu into the morning triage screen — the single highest-leverage screen for an ops team, and the card grid already exists.
+- **Verification desk redesign (the flagship queue)** (impact: high, effort: large) — This is trust — the product — and worth real design: tab counts per status, name/phone search, sortable age column with SLA coloring, and a split-pane case view (queue rail + case detail) with keyboard next/prev so a reviewer never round-trips to the list between cases. The case page already has the right bones (StepCard, DocumentViewer, credential form); it needs the throughput layout around them, plus side-by-side document/identity comparison for the identity cross-check the docstring promises.
+- **Ticket console: status/assignee lifecycle + safer internal mode** (impact: high, effort: medium) — Add close/reopen and assign-to-me on the thread header (needs the small backend mutation), unread/last-activity + assignee columns in the queue, scroll-to-latest on thread open, and make the composer visibly amber (warning-soft background + 'ثبت یادداشت داخلی' send label) whenever internal mode is on. Tickets are where refunds, emergencies, and coordination all converge — second-highest-traffic surface after verification.
+- **Shared Jalali date picker** (impact: medium, effort: medium) — One JalaliDatePicker component (input + calendar in Shamsi, emits ISO Gregorian on the wire) replacing every native type="date" across audit filters, payout windows, holidays, and credential forms. Directly reduces data-entry errors for the finance and trust desks and removes the display/input calendar mismatch.
+- **Payout run: explicit money-movement summary + typed confirmation** (impact: medium, effort: small) — The preview dialog lists eligible nurses but the final ConfirmDialog is generic copy. Show the batch total (sum to move), count, and processing date in the confirm step and require typing the amount or the word تایید for the run — the standard guard for an action that irreversibly moves money. Also localize the skipped-reason strings (currently raw server text rendered dir="ltr").
+- **AdminDataTable v2: sorting, sticky header, total count** (impact: medium, effort: small) — Add optional per-column sort (server param already keyed by filter object), a sticky header for long pages, and a footer line 'نمایش ۱–۲۰ از ۱۲۴' wired to the total every caller already has — plus restore '{page} از {total}' in the admin pager message. Small changes to one component + one i18n line lift all eleven tables at once.
+- **Partner portal polish: localized statuses, booking detail, invoice export** (impact: medium, effort: small) — Map the seven booking codes to the existing StatusChip kinds + fa labels (small, fixes the worst partner-facing defect), link a sponsored-booking row to a scoped read-only detail, and add a CSV/Excel export on settlement for the center's accountant — the actual consumer of that screen. Keeps the portal light-touch while making it feel finished.
+- **Real Users console replacing the placeholder** (impact: medium, effort: large) — A read-first user directory (search by phone/name, role chips, verification state, links into their nurse/customer profile, tickets, bookings, audit trail) that becomes the hub the numeric IDs all over the backoffice can deep-link to. Pairs with the UserPicker endpoint; also gives the roles grid a place to launch from.
+
+## Keep (do not regress)
+
+- The shared composite layer itself — AdminDataTable/AdminPageHeader/AdminPager/AdminEmptyState/AdminErrorState/ConfirmDialog in client/src/components/admin are used by every console; there is one table, one pager, one empty/error/confirm pattern, all unit-tested. Any redesign should restyle these primitives, not fork per-page markup.
+- ConfirmDialog's action-safety contract: every irreversible/audited action (approve/reject verification, moderate review, revoke role, run/retry payout, resolve alert, verify center) goes through it, with required-reason gating for reject/hide/resolve, loading that disables both buttons (double-submit-proof), and error color on destructive confirms.
+- Token discipline and dark-mode-by-construction: zero hard-coded hexes in the entire admin/partner surface; StatusChip and all accents resolve from --bal-* semantic tokens defined for both schemes in src/theme/tokens.css, plus palette-aware 'divider'/'action.hover' everywhere else.
+- RTL correctness in content code: borderInlineStart severity accents (SupportAlertCard, payout failure block), dir="ltr" wrappers on every IBAN/reference-code/latin-reason string, AdminDataTable's align:'inherit' default, and the config-history Drawer anchoring by locale.
+- Trust/PII handling expressed in the UI: DocumentViewer fetches short-lived signed URLs on demand with an expired→re-request affordance; settlement IBAN is write-then-masked (blank field + masked placeholder, never echoed); credential numbers are accepted but never displayed; internal ticket notes are visually unmistakable (dashed warning border + badge) and isolated to admin types.
+- Consistent loading/empty/error triad on every list page — skeleton stacks sized to the content, dashed-border empty states with domain icons, and an inline retry error panel; no page dumps a spinner-only or blank state.
+- The draft-vs-applied filter pattern on tickets and audit (typing never refetches; Apply commits the query key) — the right behavior for server-keyed caches, worth spreading, not replacing.
+- Server-authority posture: capability flags (useAdminCapabilities) only hide controls, money is never recomputed client-side (PriceBreakdown renders server decompositions; payout eligibility/holiday shift come from the server), and the roles console honestly banners its mock-backed status.
+- Shamsi-first display formatting via formatShamsiDate/DateTime and Toman via formatIrrToToman across every admin and partner money/date render.
diff --git a/dev/post-phase/ui/audit/auth-first-run.md b/dev/post-phase/ui/audit/auth-first-run.md
new file mode 100644
index 0000000..913f1a9
--- /dev/null
+++ b/dev/post-phase/ui/audit/auth-first-run.md
@@ -0,0 +1,70 @@
+# Auth & first-run experience (login → OTP → role routing → select-role → customer onboarding)
+
+## Current state
+
+Login lives at client/src/app/[locale]/(public-routes)/login/page.tsx, which renders LoginFlow (client/src/components/auth/LoginFlow.tsx): a two-step phone-OTP machine (PhoneStep → OtpStep) inside AuthCard — a 420px outlined Paper with a BrandMark lockup (icon + wordmark + tagline "مراقبت مطمئن در خانه"). One login stack serves both actors, parameterized by ?role=nurse; after verify, RoleRouter resolves /me behind a branded AuthSplash and routes to the family app, nurse app, admin console, or /select-role for role-less users. The middleware (client/middleware.ts) redirects every unauthenticated hit to /{locale}/login — the login screen is literally the product's front door; there is no public landing page. The whole thing sits inside PublicLayout → TopBarAndSideBarLayout, the untouched starter dashboard shell: a fixed AppBar titled "Unauthorized - Balinyaar", a logo icon-button that opens an empty Drawer, and BottomBar plumbing with zero items.
+
+Mechanically the flow is strong: PhoneNumberField normalizes Persian/Arabic digits, caps at 11, forces LTR entry; OtpInput is a 5-box group with auto-advance, backspace-to-previous, paste distribution, digit normalization, and dir="ltr"; the resend countdown is seeded from the server's resendAvailableInSeconds via a leak-free useCountdown; wrong-code, expired, lockout (with resend as the escape hatch) and 429 states are all explicitly handled. Visually, however, it is a default-MUI form: no illustration, no brand surface, no trust content, and the "logo" (AppIcon icon="logo") resolves to the starter's PencilIcon — a hard-coded multicolor Twemoji pencil whose path fills ignore the passed var(--bal-primary).
+
+select-role (client/src/components/auth/SelectRole.tsx) is a clean radio-card picker (accessible role="radio"/aria-checked) with generic filled MUI icons (nurse = a house icon). Customer onboarding (client/src/app/[locale]/(private-routes)/(customer)/onboarding/page.tsx) is a 2-step wizard (relation → patient form) using a default MUI Stepper (StepperHeader) and RelationSelect radio cards where every option shares the same 'account' icon. It renders inside the full CustomerLayout (5-tab bottom nav, support icon, notification bell), and CustomerHomePage force-redirects any customer with zero patients into it — there is no skip path and no welcome moment. Colors come from a well-built token system (client/src/theme/tokens.css) with a complete dark scheme, and the Mikhak Persian typeface loads only on fa routes.
+
+## Problems (16)
+
+- **[high]** `client/src/components/common/AppIcon/config.ts` — The brand 'logo' used on every auth screen (BrandMark, TopBar logo button) is the starter kit's Twemoji pencil. Its SVG paths carry hard-coded fills (#EA596E red, #FFCC4D yellow, #D99E82, #CCD6DD), so BrandMark's color="var(--bal-primary)" is silently ignored — the first thing a family sees on a healthcare-trust product is a cartoon pencil, identical in light and dark mode.
+ - evidence: config.ts line 115 `logo: PencilIcon`; client/src/components/common/AppIcon/icons/PencilIcon.tsx lines 8-24 hard-coded `fill="#EA596E"` etc.; client/src/components/auth/BrandMark.tsx line 21 passes `color="var(--bal-primary)"` which cannot override path-level fills
+- **[high]** `client/src/layout/PublicLayout.tsx` — The login screen — the product's only front door (middleware redirects all unauthenticated traffic here) — is wrapped in the starter dashboard shell: a fixed AppBar whose title is the hard-coded English string 'Unauthorized - Balinyaar' shown untranslated on the fa default locale, a pencil-logo IconButton that opens a completely empty sidebar Drawer (SIDE_BAR_ITEMS = []), and dead BottomBar plumbing (BOTTOM_BAR_ITEMS = []).
+ - evidence: line 9 `const TITLE_PUBLIC = 'Unauthorized - Balinyaar'`; lines 14/19 empty nav arrays; TopBarAndSideBarLayout.tsx line 71 hard-coded tooltip `'Open Sidebar'`
+- **[high]** `client/src/components/OtpInput/OtpInput.tsx` — No autoComplete="one-time-code" on the inputs and no WebOTP integration, so iOS/Android never offer the SMS code as a keyboard suggestion and Chrome cannot auto-read it. Automatic OTP fill is table-stakes in the Iranian market (Snapp/Digikala/Tapsi all auto-fill); its absence makes every login feel worse than the apps users compare against.
+ - evidence: slotProps.htmlInput (lines 121-128) sets inputMode/maxLength/aria-label but no autoComplete; repo-wide grep for 'one-time-code'/'OTPCredential' finds nothing
+- **[high]** `client/src/components/auth/PhoneStep.tsx` — No terms-of-service / privacy consent anywhere in the login flow. In a phone-OTP market login IS signup — entering a phone number creates an account — yet there is no 'با ورود، شرایط استفاده و حریم خصوصی را میپذیرید' line and no terms/privacy routes exist in the app. Legal exposure and a missing trust cue on a product whose entire pitch is trust.
+ - evidence: PhoneStep renders only title/subtitle/field/CTA/role-switch (lines 56-102); grep for شرایط/حریم/terms/privacy in client/messages/fa.json finds no auth-namespace strings and no terms page exists under (public-routes)
+- **[medium]** `client/src/components/auth/AuthCard.tsx` — The login card is a bare outlined Paper with zero brand or trust presence: no illustration, no teal/cream hero treatment, no mention of nurse verification, licensed-nurse vetting, or escrowed payment. For a trust-first healthcare marketplace with no landing page, the first impression carries no evidence of trustworthiness at all — it reads as a generic admin-template form.
+ - evidence: lines 13-30: Stack + Paper elevation={0} borderColor:'divider' — the entire visual identity of the screen
+- **[medium]** `client/src/components/auth/OtpStep.tsx` — The masked phone echo is interpolated into an RTL Persian sentence with no bidi isolation. '0912•••1234' is two European-number runs separated by neutral bullets; the Unicode bidi algorithm can reorder the segments in an RTL paragraph (the classic digits-around-neutrals reversal), rendering the number scrambled for exactly the string that tells users where their code went.
+ - evidence: line 100 `{t('otp_sent_to', { phone: maskIranMobile(phone) })}` — no , dir="ltr" span, or LRM wrapping; fa.json line 636 embeds {phone} mid-sentence
+- **[medium]** `client/src/components/auth/PhoneStep.tsx` — The 429 rate-limit message renders as helperText while error={invalid} is false, so it appears in low-contrast grey secondary text with no error styling on the field — the one state where the user is blocked and most needs to notice the message is the least visible one.
+ - evidence: line 75-76 `error={invalid} helperText={invalid ? t('phone_invalid') : rateLimited ? t('rate_limited') : ' '}` — rateLimited never sets error
+- **[medium]** `client/src/components/auth/SelectRole.tsx` — The first decision a new user makes is presented with generic filled MUI icons: nurse = a Home (house) icon, customer = AccountCircle. A house for 'I am a nurse' is semantically confusing, and selection feedback is only a border-color change — no background tint, no check indicator, no warmth on what should be a welcoming moment.
+ - evidence: lines 21-24 `{ role: 'customer', icon: 'account' }, { role: 'nurse', icon: 'home' }`; lines 74-83 selected state = borderColor only
+- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/onboarding/page.tsx` — First-run onboarding renders inside the full CustomerLayout app shell — 5-tab bottom nav, support icon, notification bell — so a user who hasn't finished setup can tab away mid-wizard, and there is no focused 'welcome' framing. Combined with the home page force-redirecting zero-patient customers here, families cannot browse or search nurses at all until they register a patient, with no 'skip for now' affordance.
+ - evidence: onboarding sits in the (customer) route group whose layout.tsx wraps children in CustomerLayout; (customer)/page.tsx line 63 `if (isEmpty) router.replace(`/${locale}${ROUTES.ONBOARDING}`)`
+- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/onboarding/page.tsx` — All four relation options (پدر/مادر، همسر، فرزند، خودم) are given the identical 'account' icon, producing four visually indistinguishable cards in the very first product interaction after signup.
+ - evidence: line 31 `RELATION_CODES.map((code) => ({ code, label: t(`relation_${code}`), icon: 'account' }))`
+- **[low]** `client/src/components/auth/OtpStep.tsx` — The resend countdown renders Latin digits ('01:23') inside a Persian sentence; Persian-market apps localize timers to Persian numerals (۰۱:۲۳). formatMmSs uses raw String/padStart with no locale-aware digit formatting.
+ - evidence: lines 24-28 `formatMmSs` + line 140 `{t('resend_in', { time: formatMmSs(countdown.seconds) })}`
+- **[low]** `client/src/components/common/AppButton/AppButton.tsx` — Starter-kit AppButton defaults every button to margin: theme.spacing(1), forcing every auth/onboarding call site to pass sx={{ m: 0 }} to undo it — spacing becomes inconsistent the moment anyone forgets, and the workaround is repeated on at least four auth CTAs.
+ - evidence: lines 9-11 `DEFAULT_SX_VALUES = { margin: 1 }`; countered by sx={{ m: 0 }} in PhoneStep.tsx:88, OtpStep.tsx:132, SelectRole.tsx:106, onboarding/page.tsx:70
+- **[low]** `client/middleware.ts` — The auth redirect discards the original destination — no returnUrl/next param is carried to /login — so any deep link (a shared nurse profile, a booking detail from an SMS) dumps the user on their role home after login instead of where they were going.
+ - evidence: line 29 `NextResponse.redirect(new URL(`/${locale}${ROUTES.LOGIN}`, request.url))` with no query param for the attempted pathname
+- **[low]** `client/src/components/OtpInput/OtpInput.tsx` — Backspace on an empty box only moves focus to the previous box without clearing it, so deleting a mistyped code takes two key presses per digit — minor friction on the highest-frequency correction gesture.
+ - evidence: lines 89-93: `if (event.key === 'Backspace' && !chars[index]) focusBox(index - 1)` — never clears chars[index-1]
+- **[low]** `client/src/layout/TopBarAndSideBarLayout.tsx` — Persistent-sidebar offset uses physical paddingLeft/paddingRight keyed on anchor strings 'left'/'right' rather than logical properties — an RTL hazard pattern in the shell that hosts the public/auth routes (dormant on login because the variant is temporary, but live starter debt in the shared chrome).
+ - evidence: lines 53-60 `paddingLeft: … anchor?.includes('left') ? SIDE_BAR_WIDTH : undefined` (and mirrored paddingRight)
+- **[low]** `client/src/layout/components/SideBar.tsx` — Hard-coded English strings in chrome reachable from the auth shell: the logout tooltip 'Logout Current User' and the empty drawer that opens from the login screen's pencil button containing only a dark-mode switch — untranslated, purposeless starter UI on the fa default locale.
+ - evidence: line 76 `title="Logout Current User"`; PublicLayout passes items=[] so SideBarNavList renders nothing
+
+## Opportunities (10)
+
+- **Rebrand the login as a trust-forward hero screen** (impact: high, effort: medium) — Since login is the product's only front door, redesign it as a branded moment: cream (--bal-bg-default) backdrop, a real Balinyaar logotype, a warm nurse-and-family illustration, and 2-3 trust bullets under the card (پرستاران دارای پروانه نظام پرستاری و احراز هویتشده، پرداخت امن نزد بالینیار تا پایان خدمت، پشتیبانی) — the same escrow/verification facts the product already implements. Drop PublicLayout's dashboard chrome entirely for auth routes (a minimal logo-only header is enough).
+- **WebOTP + one-time-code autofill with auto-submit** (impact: high, effort: small) — Add autoComplete="one-time-code" to the OTP inputs, wire navigator.credentials.get({otp}) (WebOTP) with an AbortController fallback, and format the SMS with the @origin #code convention server-side. Combined with the existing onComplete auto-verify, most users would never type the code — the single biggest perceived-quality jump available for the flow, at Snapp/Digikala parity.
+- **Real logo asset replacing the Twemoji pencil** (impact: high, effort: small) — Commission/derive a simple Balinyaar mark (currentColor SVG so the existing AppIcon color plumbing and dark scheme work), register it as ICONS.logo, and align favicon.ico + site.webmanifest icons with it. One-file swap that fixes the brand mark on login, splash, error, select-role, and the app shells simultaneously.
+- **A focused first-run journey with a welcome moment** (impact: high, effort: medium) — Give onboarding its own chrome-free layout (like AuthCard, no bottom nav), open with a one-screen welcome ('خوش آمدید — برای شروع، بگویید مراقبت برای چه کسی است'), use distinct relation iconography (elderly/family icons already exist in ICONS), add a 'بعداً تکمیل میکنم' skip that lands on a browse-capable home with a persistent complete-your-profile nudge, and close with a small success state before Home. Turns a forced form into a warm ramp and removes the browse-blocking wall.
+- **Terms/privacy consent line + static pages** (impact: high, effort: small) — Add the standard implicit-consent line under the login CTA linking to new /terms and /privacy public routes. Closes the legal gap and doubles as a trust cue; the (public-routes) group already exists to host them.
+- **Public landing page for unauthenticated visitors** (impact: high, effort: large) — Replace the blanket redirect-to-login with a real marketing front door at /: value proposition, how-it-works (search → book → escrow → confirmed care), trust/verification explainer, service categories, and a prominent nurse-recruitment CTA (?role=nurse). This is also the only path to SEO for a marketplace whose customers arrive via search.
+- **Persian-digit localization utility** (impact: medium, effort: small) — A tiny formatNumber/formatDigits helper on Intl.NumberFormat(locale) applied to the resend timer (and reusable for prices/dates app-wide), so fa users see ۰۱:۲۳ instead of 01:23 everywhere a timer or count renders.
+- **Carry a returnUrl through login** (impact: medium, effort: small) — middleware appends ?next=; RoleRouter honors it (validated same-origin, role-permitting) before falling back to resolveRoleDestination. Makes SMS deep links, shared nurse profiles, and session-expiry re-logins land where the user intended.
+- **OTP delivery fallback affordances** (impact: medium, effort: medium) — After a failed resend cycle or lockout, surface an escalation path — 'کد را دریافت نکردید؟' with a voice-call OTP option or a support link. Iranian SMS delivery is flaky enough (promotional-SMS blocking is widespread) that best-in-class local apps all offer a second channel; today the dead end is silent.
+- **Elevate select-role into an illustrated fork** (impact: medium, effort: small) — Two larger illustrated cards (family receiving care vs. nurse professional), a selected-state fill using --bal-primary-soft plus a check glyph, and a reassurance line that the other role can be added later (dual-role sessions are already supported by RoleGuard). This screen is each new user's first branded decision — worth more than two grey bordered rows.
+
+## Keep (do not regress)
+
+- OTP input mechanics are genuinely well-built: auto-advance, paste distribution across boxes, Persian/Arabic→ASCII digit normalization, and dir="ltr" forcing on both the OTP group and the phone field so codes/numbers read correctly inside the RTL layout (client/src/components/OtpInput/OtpInput.tsx, client/src/components/PhoneNumberField/PhoneNumberField.tsx)
+- Server-driven resend cooldown (resendAvailableInSeconds seeds the countdown) with a leak-free one-shot useCountdown, and the deliberate rule that resend stays available during lockout as the recovery path (OtpStep.tsx line 91)
+- Explicit, distinct error states throughout: wrong/expired code, max-attempts lockout via OTP_LOCKED_CODE, and 429 rate-limit handling — plus the ' ' helperText placeholder that prevents layout jump when errors appear (PhoneStep.tsx line 76)
+- Auto-verify on the fifth digit with a disabled CTA until complete, and the masked phone echo (0912•••1234) protecting the number on the OTP screen (fix its bidi wrapping, keep the masking)
+- The routing hardening is excellent UX engineering: RoleRouter + branded AuthSplash so the wrong actor shell never flashes, AuthAccountError as an explicit recovery instead of silently downgrading a nurse to the customer shell, and RoleGuard's redirect-with-toast on role mismatch
+- One login stack parameterized by intendedRole — no forked customer/nurse login trees — with the intent carried through to select-role pre-selection
+- The token system in src/theme/tokens.css: complete, thoughtfully lifted dark scheme, soft tints, and brand-harmonized feedback colors; BrandMark's wordmark correctly uses primary.main so it tracks the scheme
+- Accessible selection semantics on the radio cards (role="radio", aria-checked, tabIndex, Enter/Space handlers) in SelectRole and RelationSelect, and per-box aria-labels on the OTP inputs
+- Mikhak Persian typeface loaded only on fa routes (preload:false + conditional className in the locale layout) — correct i18n-aware font strategy
+- Onboarding's relation choice pre-shapes the patient form and hides the already-answered relation field (no double-asking), and the home-page redirect gate waits for a settled patients list so a fresh create never bounces the user back into onboarding
diff --git a/dev/post-phase/ui/audit/booking-lifecycle.md b/dev/post-phase/ui/audit/booking-lifecycle.md
new file mode 100644
index 0000000..90e60e0
--- /dev/null
+++ b/dev/post-phase/ui/audit/booking-lifecycle.md
@@ -0,0 +1,72 @@
+# Customer booking-request + booking lifecycle screens (C4 request form → C5 pending tracker → bookings list → booking detail/EVV → cancel → refund status → review)
+
+## Current state
+
+The flow is complete and functionally rich but visually generic. C4 (`client/src/app/[locale]/(private-routes)/(customer)/bookings/request/page.tsx`) is a single long page of default MUI selects (patient, service variant, saved address, native date/time inputs, a 3-way gender ToggleButtonGroup, notes with counter), with per-field dashed-border empty states, a form skeleton, and a domain-code→message error mapper. C5 (`bookings/request/[id]/page.tsx`) polls the request and renders the shared `BookingRequestSummaryCard`, a raw-MUI `StepperHeader` 3-step tracker, `CountdownTimer` (server-frozen deadline, LTR-forced Persian digits), five distinct terminal cards, and a cancel confirm Dialog. The bookings list (`bookings/page.tsx`) is a flat stack of bordered Paper rows — counterparty name, Shamsi date, session count, `StatusChip`, Toman total, "view" button — with skeleton/error/empty branches but no tabs, filters, pagination, or row click.
+
+## Problems (19)
+
+- **[high]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/request/[id]/page.tsx` — In the cancel-request confirmation dialog, the DISMISS button is labeled with the destructive action's own label: `t('cancel_request')` = "انصراف از درخواست". Clicking the button that says "Cancel request" actually keeps the request and closes the dialog; the real destructive button is `cancel_confirm_yes`. Users who want to cancel will click the dismiss button; users who want to keep it may click confirm. Dismiss must read "بازگشت"/"نگه داشتن درخواست".
+ - evidence: Lines 223–225: ` setConfirmCancel(false)}>{t('cancel_request')} ` inside DialogActions, next to the contained error confirm button.
+- **[high]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/request/page.tsx` — All required-field inline errors are unreachable dead code. The submit button is `disabled={!requiredChosen …}`, but `setAttempted(true)` only runs inside `handleSubmit` — which can never fire while a required field is missing. So `error={attempted && patientId === ''}` etc. never render, and the only feedback for an incomplete form is a silently disabled button with no explanation of what's missing.
+ - evidence: Line 439 `disabled={!requiredChosen || genderMismatch || createRequest.isPending}` vs line 128 `setAttempted(true)` (only in handleSubmit) and line 230 `error={attempted && patientId === ''}`.
+- **[high]** `client/src/services/bookingRequests/hooks/useCustomerRequests.ts` — The customer has NO way back to a pending request. `useCustomerRequests` (the customer requests inbox hook) is exported but consumed by zero pages; the bottom nav (CustomerLayout: Home/Bookings/Patients/Wallet/Profile) has no requests entry, and `bookings/page.tsx` lists only post-payment bookings (`list_empty_body`: "پس از تایید پرستار و پرداخت…"). If the user leaves C5 or closes the app while a request is pending — or during the 30-minute payment window — the request is orphaned unless they remember the URL.
+ - evidence: Grep: `useCustomerRequests` appears only in services/bookingRequests/index.ts and its own hook file; CustomerLayout.tsx lines 33–37 show the five nav items with no requests route.
+- **[high]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/request/page.tsx` — The request form never shows WHO the request is for. `useNurseProfile` is fetched (used only for the gender-mismatch check and hidden display-context), but no nurse name, avatar, rating, or verification badge renders anywhere on the page — the header is just "request_title" + a generic subtitle. In a trust-first nursing marketplace, asking a family to hand over patient + home address to an unnamed party is the single biggest trust failure in the flow.
+ - evidence: Lines 206–215 render only `t('request_title')`/`t('form_subtitle')`; `profile.nurseName` is referenced only inside the `context` object at line 144.
+- **[high]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/request/page.tsx` — Date selection uses the browser-native Gregorian `type="date"` input while every displayed date in the product is Shamsi (`formatShamsiDate`). Persian users must mentally convert Jalali → Gregorian to book a visit, then see the confirmation back in Shamsi. Time inputs are fine; the date picker needs a Jalali calendar.
+ - evidence: Lines 335–347: ` ` with `slotProps={{ inputLabel: { shrink: true } }}`.
+- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/request/page.tsx` — The address preview embeds the fake-map stand-in (`AddressMapPicker` with `pointerEvents: 'none'`) — a 220px grid-pattern canvas with a pin and lat/lng labels that the component's own doc admits "is NOT a real map". In a read-only confirmation context it communicates nothing a text line doesn't, looks like placeholder scaffolding, and eats a large chunk of the form's vertical space.
+ - evidence: Lines 315–327 wrap ` ` in ``; AddressMapPicker.tsx lines 29–35 document the stand-in nature.
+- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/page.tsx` — The bookings list has no status tabs/filters, no upcoming-vs-past grouping, and no pagination even though the service is paginated (BOOKINGS_PAGE_SIZE=20, `total` returned) — booking #21 is unreachable. Rows aren't clickable (only the small outlined button navigates), the error state has no retry, and the empty state has no CTA into search. Scannability is one-note: every status renders identically except the chip.
+ - evidence: Line 19 `useBookingList('customer')` with no page/status params; lines 39–54 error/empty branches with no action; row nav only via AppButton at lines 91–99.
+- **[medium]** `client/src/components/booking/BookingDetailView/BookingDetailView.tsx` — The booking detail header omits the facts a customer most needs: no visit address/location, no headline date/time (buried per-session), no nurse avatar or contact affordance, no total-at-a-glance. The header is service name + booking ref + two tiny label/value pairs — a customer opening "my booking" cannot answer where/when without scanning session cards.
+ - evidence: Lines 63–95: header Paper contains only `service ?? t('bd_title')`, `bd_ref`, and two `HeaderFact`s (patient, nurse name).
+- **[medium]** `client/src/components/CountdownTimer/CountdownTimer.tsx` — The countdown — the emotional core of C5 — is bare `HH:MM:SS` digits next to a generic 'pending' icon, ticking every second. There is no progress ring/bar showing how much of the response window remains, no humanized framing ("پاسخ معمولاً تا چند ساعت"), and for multi-hour windows a per-second ticker reads as anxiety-inducing rather than calm. The 'urgent' variant only swaps the color to terracotta.
+ - evidence: Lines 92–109: label caption + `AppIcon icon="pending"` + a 1.5rem tabular-nums span; no other presentation.
+- **[medium]** `client/messages/en.json` — A directional arrow is hard-coded inside the translated CTA copy: fa "ادامه پرداخت ←" and en "Continue to payment ←" — in English (LTR) the forward arrow should be →, so the EN button points backwards; and the button already carries `endIcon="payment"`, duplicating the affordance. Directionality must come from layout/icons, never from string literals.
+ - evidence: fa.json:445 / en.json:445 `"continue_payment": "Continue to payment ←"`; consumed at bookings/request/[id]/page.tsx lines 182–191.
+- **[medium]** `client/src/components/BookingRequestSummaryCard/BookingRequestSummaryCard.tsx` — The date·time-range label is not bidi-isolated: `whenLabel` concatenates Shamsi date, '·', and "start – end" times with no `dir="ltr"` wrapper (unlike SessionCard, which wraps its identical time range in a `dir="ltr"` span). With Persian (AN-class) digits in an RTL paragraph, the range can visually render end-before-start.
+ - evidence: Line 60 builds `whenLabel`; lines 121–125 render it in a plain Typography with `textAlign: 'end'` — compare SessionCard.tsx line 99's `{timeLabel}`.
+- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/[id]/review/page.tsx` — The review form lacks context and expectation-setting: no recap of the booking being reviewed (service/date), no up-front note that reviews are moderated before publishing (the user only learns post-submit via the 'under review' state), no visible character counter for the 2000-char body (input silently sliced), and no hover/selected labels on the stars (just 5 identical icons).
+ - evidence: Lines 135–185: heading is only nurse name; line 150 `onChange={(e) => setBody(e.target.value.slice(0, REVIEW_BODY_MAX))}` with no counter; RatingInput has no per-star labels.
+- **[medium]** `client/src/components/StatusChip/StatusChip.tsx` — Every status in the entire flow renders as a solid, fully-saturated pill (solid success/error/warning/info backgrounds with cream text), all at equal visual weight — a cancelled booking shouts as loudly as a disputed one, and screens with several chips (list rows, sessions, refund card, review state) read as a loud, cold dashboard rather than the calm clinical-warm tone. Soft-tinted chips (bg-soft + strong fg) would fit the brand and let severity actually rank.
+ - evidence: Lines 15–22: `verified: { bg: 'var(--bal-success)', fg: 'var(--bal-success-contrast)' … }` — solid token fill for all six kinds.
+- **[low]** `client/src/components/booking/BookingDetailView/BookingDetailView.tsx` — The i18n fallback key `unnamed_nurse` ("پرستار", intended as the no-name placeholder) is repurposed as the field LABEL for the nurse in the header facts — semantically wrong key reuse that will break the moment the fallback copy changes.
+ - evidence: Line 92: ` `.
+- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/request/page.tsx` — Negative-margin layout hacks stitch related elements together instead of composed grouping: the price display is pulled up under the service select with `mt: -1.5`, and the notes counter is pulled under its TextField with `mt: -2` — fragile spacing that breaks when helper text appears.
+ - evidence: Line 272 `` and line 424 `sx={{ …, textAlign: 'end', mt: -2 }}`.
+- **[low]** `client/src/components/booking/BookingStatusTimeline/BookingStatusTimeline.tsx` — The cancelled terminal row uses the brand-teal info tint (`--bal-primary-soft`) as its background while the sibling StatusNote in BookingDetailView uses `--bal-divider` for the same cancelled state — two different 'neutral' treatments for one status on one screen, and teal reads as informational/brand, not terminated.
+ - evidence: Line 45 `bgcolor: 'var(--bal-primary-soft)'` vs BookingDetailView.tsx line 185 `tone === 'neutral' ? 'var(--bal-divider)'`.
+- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/[id]/cancel/page.tsx` — The cancellation reason select pre-defaults to 'changed_mind', so users can submit without ever choosing — biasing the reason analytics and skipping a moment of reflection the trust-first flow otherwise builds carefully.
+ - evidence: Line 58: `useState('changed_mind')` with no empty/placeholder option.
+- **[low]** `client/src/components/StepperHeader/StepperHeader.tsx` — StepperHeader is a raw default-MUI Stepper (default numbered circles, default connectors, default typography) and it is the status-communication backbone of the whole lifecycle — C5 tracker, booking timeline, refund progress, cancel flow all render this unstyled starter component, which is the single most 'MUI beginner example' element in the flow.
+ - evidence: Lines 20–28: bare `` with zero styling beyond `py: 2`.
+- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/[id]/review/page.tsx` — `useMyReviewForBooking(bookingId)` fires unconditionally on the review page, while the detail page carefully gates the same hook with `{ enabled: reviewable }` — inconsistent pattern and a wasted request for non-eligible bookings.
+ - evidence: Line 43 `const myReview = useMyReviewForBooking(bookingId);` vs bookings/[id]/page.tsx line 52's gated call.
+
+## Opportunities (10)
+
+- **Customer requests inbox: merge requests + bookings under one tabbed surface** (impact: high, effort: medium) — Wire the already-built `useCustomerRequests` hook into a customer-facing surface: segmented tabs on /bookings (در انتظار پاسخ / فعال / گذشته). Pending-request rows carry a live mini-countdown chip and deep-link to C5; accepted-awaiting-payment rows surface the payment deadline as the row's primary CTA. This closes the orphaned-request hole (the highest-stakes UX gap: money-adjacent deadlines the user cannot find again) with zero new backend work.
+- **Redesign C4 as a trust-anchored request flow** (impact: high, effort: medium) — Put a sticky nurse identity card at the top (avatar, name, rating + review count, verified badge, gender) so the family always sees who they're inviting home; add a 3-step 'what happens next' strip (درخواست → پاسخ پرستار → پرداخت) reinforcing the money-free promise already in form_subtitle. Replace the dead-end disabled submit with always-enabled submit + scroll-to-first-error. Optionally split into two steps: details → review-and-send with a compact BookingRequestSummaryCard preview, which doubles as the fix for the missing pre-submit recap.
+- **Jalali date picker + time-window presets** (impact: high, effort: large) — Replace native type=date with a Shamsi calendar picker (weekend/holiday aware — the ops holiday table already exists server-side), and replace free start/end time fields with tappable window chips (صبح ۸–۱۲ / بعدازظهر ۱۲–۱۶ / عصر ۱۶–۲۰ + custom). This removes the Gregorian mental conversion, kills the end<=start error class for most users, and later becomes the seam for nurse-availability hints.
+- **Booking detail 'next visit' hero + designed vertical timeline** (impact: high, effort: medium) — Rebuild the detail header as an actionable hero: next upcoming session ("ویزیت ۲ · فردا ۹:۰۰"), the visit address, nurse avatar with message/support entry, and add-to-calendar. Replace the generic horizontal Stepper with a designed vertical timeline (per-stage icons, timestamps where known, terracotta marker on the current stage, distinct terminal branch rendering) — this is where 'status via raw chips + default stepper' should become the product's signature trust visual.
+- **Countdown as a calm progress ring** (impact: medium, effort: small) — Wrap the CountdownTimer digits in a circular progress ring fed by (deadline − createdAt) so users see the fraction of the window remaining, drop per-second ticking above 10 minutes remaining (show 'حدود ۳ ساعت'), switch to seconds only in the final minutes, and add a one-line 'we'll notify you' note tied to the notifications bell so users feel safe leaving the page.
+- **Live 'nurse is on site' presence for in-progress bookings** (impact: medium, effort: small) — The customer already receives EVV banners per session — elevate this into a headline presence state on the detail (and list row): 'پرستار در محل است · ورود ۰۹:۰۲' while checked-in. It converts the EVV plumbing into the platform's most visceral trust cue for the family member who is not at home with the patient.
+- **Cancellation off-ramps before the kill switch** (impact: medium, effort: medium) — Before the fee disclosure, offer alternatives: 'تغییر زمان' (reschedule request via support ticket until real rescheduling exists) and 'گفتگو با پشتیبانی'. Add a one-line human note about nurse impact. The existing disclosure is excellent; giving an exit that isn't destruction both reduces cancellations and reads as fair.
+- **Post-completion review nudge on the list** (impact: medium, effort: small) — Completed bookings without a review should show a compact star-strip CTA directly on the bookings-list row (the eligibility + my-review hooks already exist), instead of relying on the user opening the detail and finding the button below the money summary.
+- **Terminal-state cards with smarter recovery** (impact: medium, effort: medium) — C5's rejected/expired cards all funnel to generic search. Offer 'درخواست دوباره از همین پرستار با زمان دیگر' (prefilled C4) when the rejection reason isn't gender/coverage, and 'پرستاران مشابه' (same service + area) otherwise — recovering the booking intent rather than restarting discovery from zero.
+- **Soft-tint status system + status-differentiated list rows** (impact: medium, effort: small) — Introduce a soft chip variant (token -soft backgrounds + strong text, reserving solid fills for EVV banners) and give list rows a status-colored inline-start border accent so the eye can rank a page of bookings without reading every chip — aligning the whole flow with the calm/warm brand direction.
+
+## Keep (do not regress)
+
+- Token discipline is genuinely excellent in this area: zero hard-coded hexes across all audited files — every color resolves through --bal-* semantic tokens with both light/dark schemes defined (StatusChip, EvvStatusBanner, RatingInput even document the rule in comments).
+- Server-truth discipline: CountdownTimer never computes deadlines client-side (renders diff vs server-frozen instant, LTR-forced Persian digits); BookingStatusTimeline never advances a step client-side; money is display-only IRR digit-strings through formatIrrToToman — never summed or re-split in the UI (BookingMoneySummary doc + props contract).
+- Two-stage disclosure as a hard UI gate: the customer's care-instructions query never fires (enabled only for nurse on confirmed+), replaced by a designed lock affordance with honest copy ('visible to your assigned nurse and support only') — BookingDetailView CareSection.
+- The cancellation flow's trust architecture: full pre-submit disclosure of policy tier, refund %/fee %, concrete Toman split via PriceBreakdown, per-session refundable/locked breakdown, admin-approval explainer, and an explicit acknowledgement checkbox gating confirm (CancellationPolicyDisclosure + cancel page).
+- Honest refund UX: a failed refund suppresses ALL success-framed progress/amount/ETA (explicit comment in RefundStatusCard), BNPL's ~7–10-business-day window is surfaced plainly in RefundEtaBanner, and retry is deliberately absent (admin-only).
+- EVV advisory semantics done right: out-of-range is warning-toned (never error), GPS-unavailable is neutral, and a mismatch never blocks the flow — tri-state handled end-to-end (EvvStatusBanner styleFor + SessionCard).
+- Every screen in the flow has real skeleton loading states shaped like the final layout (FormSkeleton, StatusSkeleton, DetailSkeleton, review skeletons) plus distinct empty/error branches — no spinner-only pages.
+- RTL/Persian craft in most components: dir="ltr" isolation on countdown digits, session time ranges and refund references; borderInlineStart logical accents; textAlign:'end'; Shamsi dates via fa-IR-u-ca-persian and Persian digits via Intl everywhere; the AddressMapPicker's documented inline-style workaround against stylis RTL flipping.
+- First-class caregiver-gender preference with culturally-informed hint copy, never silently defaulted, and gender-mismatch blocked inline before the round-trip with the server staying authoritative (request form lines 109–113).
+- The single terracotta-accent rule holds: --bal-secondary appears only at money/urgency moments (payment countdown, payment CTA, payout rows, nurse-view chip) — exactly the sparing-accent brand intent.
diff --git a/dev/post-phase/ui/audit/checkout-money.md b/dev/post-phase/ui/audit/checkout-money.md
new file mode 100644
index 0000000..7b56e9b
--- /dev/null
+++ b/dev/post-phase/ui/audit/checkout-money.md
@@ -0,0 +1,70 @@
+# Checkout & money surfaces (customer side)
+
+## Current state
+
+The card flow is a four-screen chain: C6 checkout (`bookings/checkout/page.tsx`) renders an acceptance StatusChip, an EngagementSummary mini-card (variant/nurse/patient/Shamsi date), a terracotta CountdownTimer for the payment deadline, the reconciling `PriceBreakdown` (service cost × visit count / commission / VAT / total), the shared `EscrowNotice`, and a terracotta contained pay CTA plus an outlined BNPL branch button. `checkout/return/page.tsx` fires an idempotent gateway-return report then polls `usePaymentOutcome`, rendering a pending StateCard (CircularProgress + PaymentStatusBadge + manual «بررسی دوباره»), a designed failure card with retry/back, and a window-expired card; success hands off to `checkout/confirmation/page.tsx` (64px verified icon, total-paid panel, view-booking + invoice CTAs, optional «پرداختشده با اقساط» line). BNPL is a 4-step wizard (`checkout/bnpl/page.tsx` + MethodStep/PlanStep/EligibilityStep/ScheduleStep) with a MUI StepperHeader, ButtonBase selection cards (`BnplPlanCard`, provider rows with two-letter glyph logo stand-ins), consent-gated eligibility and contract steps, and its own return surface; `checkout/bnpl/gateway/page.tsx` is a dev provider harness still shipped in the route tree. The invoice page (`bookings/[id]/invoice/page.tsx`) reuses PriceBreakdown, shows invoice number (dir=ltr), Shamsi issue date and read-only مودیان status, and prints via a visibility-scoped print area with a dark→light token flip. `/wallet` is a thin shell around `WalletInstallments` — provider-reported BNPL plans only (terracotta outstanding-balance card, InstallmentScheduleRow due list, provider-ownership note).
+
+All money passes through the BigInt-safe `utils/money.ts` (`formatIrrToToman` → Intl `fa-IR` Persian digits + grouping; Toman display-only, Rial digit-strings on the wire; a dev-mode guard in PriceBreakdown asserts rows sum to the total). Styling is token-disciplined (`var(--bal-*)` everywhere, no hard-coded hexes found in this area, both schemes defined in `tokens.css`), RTL hygiene is genuinely good (logical `textAlign:'start'`, `insetInlineStart`, LTR-forced countdown clock and invoice number). The weaknesses are compositional rather than mechanical: every screen is a flat single column of `elevation={0}` bordered Papers with h6-as-h1 headings, amount+«تومان» is hand-composed ad hoc on six-plus surfaces (no shared Money primitive beyond the catalog-specific PriceDisplay), and the trust moments (total, escrow, confirmation receipt) are visually underweighted for a payment product.
+
+## Problems (18)
+
+- **[high]** `client/src/app/[locale]/(private-routes)/(customer)/wallet/WalletInstallments.tsx` — The top-level Wallet bottom-nav tab (labelled «کیفپول» / title «کیفپول و اقساط») contains only BNPL installment plans — no payment history, no refund status, no receipts, no balance. For every customer who paid by card (the default path, BNPL is mock-gated) this is a permanently empty tab showing «طرح اقساط فعالی ندارید», which both under-delivers on the 'wallet' promise and wastes 1 of 5 nav slots on the money product's most trust-sensitive surface.
+ - evidence: WalletPage renders only (wallet/page.tsx:9); empty state at WalletInstallments.tsx:50-51 is a PlaceholderScreen about installments only
+- **[high]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/confirmation/page.tsx` — The post-payment confirmation has no payment reference: no transaction/tracking code (کد پیگیری), no payment date-time, no payment method, no booking number. Iranian users screenshot payment receipts and expect a reference number to quote in disputes; here the receipt panel is just amount + variant + nurse name. Worse, the amount panel is fetched via useCheckoutSummary with no error/loading handling — if that fetch fails the paid amount silently disappears (lines 63-87 render nothing on !summary) and the 'receipt' is just a title and two buttons.
+ - evidence: lines 51-113: only totalIrr, variantLabel, nurseName rendered; `{summary ? (...) : null}` with no fallback
+- **[high]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/page.tsx` — Weak visual hierarchy for a payment page: the page h1 is a plain `variant="h6"` (line 159), the total — the single most important number — is a small `subtitle2` row inside the breakdown card (PriceBreakdown.tsx:62), and the pay CTA is the last element of a scroll column after countdown + breakdown + escrow notice, not sticky and not paired with the amount. On mobile (the primary experience) the user must scroll past everything to find the button, and nothing on screen answers 'how much am I about to pay' at the moment of tapping pay.
+ - evidence: checkout/page.tsx:155-226 — flat Stack; CTA at lines 203-212 with no sticky container and no amount on/near the button
+- **[medium]** `client/src/components/PriceBreakdown/PriceBreakdown.tsx` — Currency-unit ambiguity — the classic Iranian Toman/Rial trust hazard: individual breakdown rows render bare grouped numbers with no «تومان» label (line 53), only the total row appends the unit (line 63). InstallmentScheduleRow.tsx:57 omits the currency label entirely on every installment amount, while BnplPlanCard and MethodStep do show it. Amounts are wire-Rials rendered as Toman, so an unlabelled number is exactly the case users second-guess.
+ - evidence: PriceBreakdown.tsx:52-55 rows have no currency label; InstallmentScheduleRow.tsx:57 `{formatIrrToToman(row.amountIrr, locale)}` with no unit
+- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/bnpl/return/page.tsx` — Mislabelled CTA in the invalid-link state: the button reads t('pay_with_card') («پرداخت با کارت») but navigates to the bookings list, not a card checkout — the label promises a payment action the click cannot perform.
+ - evidence: lines 100-104: `onClick={() => router.replace(`/${locale}${ROUTES.BOOKINGS}`)}` with label `{t('pay_with_card')}`
+- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/page.tsx` — During the pay-initiate busy state (isPending/isSuccess) only the card CTA is disabled — the «پرداخت اقساطی» BNPL button stays fully tappable (no `disabled={busy}`), so a user can launch the BNPL wizard while a card payment initiation/redirect is in flight, racing two payment paths for the same request.
+ - evidence: lines 214-224: BNPL AppButton has onClick router.push but no disabled prop; `busy` (line 148) is applied only to the pay button (line 207)
+- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/bnpl/MethodStep.tsx` — Financial-provider selection uses two-letter text glyphs (DG/SP/TA/…) as logo stand-ins in a 40×28 tinted box. Choosing a credit provider from fake wordmark chips reads as unfinished and undermines exactly the trust the BNPL step needs; the code itself marks them as stand-ins awaiting real assets.
+ - evidence: lines 17-24 PROVIDER_GLYPH map + comment 'real logos land with the provider assets'
+- **[medium]** `client/src/components/BnplPlanCard/BnplPlanCard.tsx` — The down payment is communicated only as a percentage plus a LinearProgress bar (lines 77-98) — a static fact styled as a loading indicator — and the actual down-payment amount in Toman is never shown anywhere in the plan card or D2 step; the user must mentally compute percent × total to know what they'll pay today. The plan's total repayment cost (with fee) is likewise absent from the card.
+ - evidence: lines 79-96: percent label + ``, no Toman figure
+- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/bnpl/PlanStep.tsx` — The «مبلغ کل» header card shows the first plan's total before any selection and silently swaps to the selected plan's total on tap (fee plans differ from interest-free ones) — an amount that changes without explanation, with no label saying which plan it reflects and no fee delta called out.
+ - evidence: lines 54-56: `const shownPlan = plans.find(...) ?? plans[0];` feeding the 'total_amount' header
+- **[medium]** `client/messages/fa.json` — The brand is spelled two different ways in fa: `common.brand` = «بالین یار» (plain space) while `payment.issuer_platform` = «بالینیار» (ZWNJ, the product-docs spelling). An inconsistently spelled brand name — on money surfaces and a fiscal invoice of all places — is a direct trust leak; the invoice page even carries a code comment acknowledging the mismatch instead of fixing it.
+ - evidence: fa.json common.brand «بالین یار» vs payment.issuer_platform «بالینیار»; invoice/page.tsx:148-149 comment
+- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/[id]/invoice/page.tsx` — The invoice is print-capable but not audit-worthy: no customer name, no service/nurse identification, no service date, no booking reference, no payment method/transaction id, and no seller fiscal identity (tax/economic ID, address) that a real Iranian VAT invoice carries — just number, issue date, three lines and a مودیان chip. As the document families keep for reimbursement/dispute it is too thin.
+ - evidence: lines 138-184: header + MetaRow(invoiceNumber, issuedAt) + 3-row PriceBreakdown + moadian chip is the entire document
+- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/return/page.tsx` — The payment-pending state stacks three redundant signals of the same fact — title «در حال تایید پرداخت…», a generic CircularProgress, and a PaymentStatusBadge 'pending' chip (lines 139-147) — a spinner-centric default rather than a designed wait state; the failure state similarly doubles an error icon with a 'failed' chip (lines 113-115). Functional, but reads default-MUI at the flow's most anxious moment.
+ - evidence: lines 139-147 pending StateCard; lines 112-135 failure StateCard
+- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/page.tsx` — MessageCard is copy-pasted verbatim into bnpl/page.tsx (lines 178-209 there) and StateCard is duplicated between checkout/return and bnpl/return — four private near-identical terminal-state cards across one flow, guaranteeing visual drift in the surface where consistency signals reliability.
+ - evidence: checkout/page.tsx:255-286 vs bnpl/page.tsx:178-209; return/page.tsx:151-180 vs bnpl/return/page.tsx:158-187
+- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/bnpl/gateway/page.tsx` — A dev-only provider-handoff harness (dashed-border card with 'pay success / pay fail' buttons) ships inside the customer route tree and is reachable by URL in production builds; the equivalent card-gateway harness was already deleted in refinement, this one remains.
+ - evidence: file header comment 'a **test harness, not a product feature**'; USE_BNPL_MOCK=true in services/bnpl/constants.ts:18
+- **[low]** `client/messages/fa.json` — Directional arrow glyphs are baked into the translated CTA copy (fa `cta_pay` ends with «←», en with «→») instead of an icon slot on the button — brittle typography that any copy edit or font change degrades, and inconsistent with every other CTA in the flow which uses AppButton startIcon.
+ - evidence: payment.cta_pay: «ادامه پرداخت ←» (fa) / 'Continue to payment →' (en)
+- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/bnpl/EligibilityStep.tsx` — The provider credit check — plausibly a multi-second external call — gives no in-progress feedback beyond a disabled button (line 163): no spinner, no label change (C6's pay button at least swaps to «در حال شروع…»), leaving the user staring at a dead form. The prefilled mobile field also uses `disabled` (low-contrast, skipped by screen readers) where read-only presentation would be clearer.
+ - evidence: lines 140-146 disabled PhoneNumberField; line 163 `disabled={!consent || check.isPending}` with static label
+- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/wallet/WalletInstallments.tsx` — Money-surface column widths are inconsistent: the wallet self-constrains to maxWidth 560 (line 24) while checkout/confirmation/invoice stretch to the shell's full 800px (CONTENT_MAX_WIDTH, components/config.ts:4) with edge-to-edge CTAs — the same flow renders at two different reading widths on desktop.
+ - evidence: WalletInstallments.tsx:24 `maxWidth: 560` vs CustomerLayout.tsx:70 CONTENT_MAX_WIDTH=800
+- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/page.tsx` — The main payable state offers no back/cancel affordance — the customer shell's TopBar has no back arrow and only the error/expired branches render navigation, so a user who wants to re-read the request before paying must rely on browser back or abandon via bottom nav under a ticking payment countdown.
+ - evidence: lines 155-226 render no link back to the request; CustomerLayout TopBar startNode is a support icon only
+
+## Opportunities (8)
+
+- **Trust-forward checkout redesign: sticky pay bar + identity moment** (impact: high, effort: medium) — Restructure C6 around the two questions users ask at payment: 'who am I paying for' and 'how much'. Give the EngagementSummary the nurse's avatar + verified TrustBadge (the components exist elsewhere in the app), lift the total out of the breakdown into a prominent h4 figure, and pin a sticky bottom pay bar (total + «پرداخت» button) above the customer shell's bottom nav so amount and action are always co-located on mobile. Add the expected Iranian trust marks near the CTA: gateway/Shaparak logos and a lock + «پرداخت امن از طریق درگاه بانکی» line so users know a bank gateway redirect is coming.
+- **Real receipt on confirmation: reference code, timestamp, share** (impact: high, effort: small) — Turn the confirmation into a screenshot-worthy receipt card: transaction/tracking code (کد پیگیری) in a copyable dir=ltr row, Shamsi payment date-time, payment method, booking number, and the escrow reassurance restated ('مبلغ بهصورت امانی نگهداری میشود'). Add a share/save action and an 'SMS receipt sent' note. This is also where a 'what happens next' 2-step strip (nurse notified → visit day check-in) would extend trust past the payment.
+- **Make /wallet the customer money hub** (impact: high, effort: large) — The tab already exists and is empty for most users — fill it: payment history (all card + BNPL transactions with PaymentStatusBadge), refund entries reusing the existing RefundStatusCard, a receipts/invoices list deep-linking to the invoice page, and the current installments section beneath. This converts a dead nav slot into the single place families verify 'where my money went' — the core promise of an escrow marketplace.
+- **Escrow explainer beyond one sentence** (impact: high, effort: small) — EscrowNotice is one mandated sentence. Add an optional expandable 'چطور کار میکند' with a 3-step visual (پرداخت → امانت نزد بالینیار → آزادسازی پس از تایید پایان ویزیت) plus the cancellation/refund implication, linked from checkout and confirmation. Escrow is the platform's reason-to-pay-on-platform; one alert line under-sells it at the exact moment of maximum skepticism.
+- **Shared primitive** (impact: medium, effort: small) — Introduce one Money component (amount + «تومان» + size/tone/emphasis variants, optional strike-through for fee comparisons) and replace the six-plus ad-hoc `formatIrrToToman(...) {tc('currency_toman')}` compositions (confirmation, MethodStep, PlanStep, EligibilityStep, WalletInstallments, BnplPlanCard, PriceBreakdown rows, InstallmentScheduleRow). Guarantees the currency label is never dropped and money typography is identical everywhere.
+- **Honest BNPL plan comparison** (impact: medium, effort: medium) — On each plan card show the concrete Toman figures users actually decide with: down payment amount due today, monthly amount, and total repayment (with the fee delta vs interest-free made explicit, e.g. '+۴۵۰٬۰۰۰ تومان کارمزد'). Replace the LinearProgress down-payment bar with a plain labelled amount row. Consider a compact compare view when a provider offers 3+ plans.
+- **Designed payment wait/result states** (impact: medium, effort: medium) — Replace the spinner+chip+title pending card with a staged wait state (e.g. a 2-node progress: 'بازگشت از درگاه ✓ → در انتظار تایید بانک' with a calm animated indicator and expected duration), and give success a small warm moment (brand-toned check animation). Extract MessageCard/StateCard into one shared PaymentStateCard so card and BNPL flows can't drift.
+- **Fiscal-grade invoice + A4 print stylesheet** (impact: medium, effort: medium) — Extend the invoice with buyer name, service description + visit date(s), booking and transaction references, payment method, seller fiscal identity, and the مودیان tax reference once registered; add an @media print A4 layout (margins, footer with issue metadata) so the printed artifact looks like a document rather than a cropped web card.
+
+## Keep (do not regress)
+
+- The money pipeline itself: BigInt-safe utils/money.ts, Rial digit-strings on the wire, Toman display-only, Intl fa-IR Persian digits + grouping — no float money math anywhere in the area, and PriceBreakdown's dev-mode guard that displayed rows must reconcile to the total to the rial (PriceBreakdown.tsx:35-42).
+- EscrowNotice as a single shared, product-mandated component (with a thoughtful dark-scheme token comment) reused verbatim across surfaces instead of re-written copy.
+- State coverage discipline: every screen has a skeleton, designed error+retry, and empty states (no providers, no plans, empty wallet); benign 409s converge silently to the outcome read instead of surfacing scary toasts; idempotency-key-per-attempt on both pay and BNPL issue.
+- CountdownTimer: server-frozen deadline, self-contained 1s tick, LTR-forced clock with tabular-nums so HH:MM:SS survives RTL.
+- Token discipline and dark-mode care: no hard-coded hexes in the whole area, all colors via scheme-aware --bal-* tokens, and the invoice's data-mui-color-scheme flip so dark-mode users print on paper colors (invoice/page.tsx:101-113).
+- RTL hygiene: logical properties throughout (textAlign:'start', insetInlineStart in print rules), dir='ltr' on the invoice number and national-id input — no marginLeft/left hazards found in these files.
+- Terracotta used exactly as intended — the single money accent (pay CTA, BNPL selection borders/tints, outstanding-balance card, breakdown total) against calm teal/neutral chrome.
+- BNPL honesty architecture: ownership notes at the point of choice and in the contract step ('the agreement is customer ↔ provider'), consent checkboxes gating both credit check and contract, and every decline path offering the card fallback — never a dead end (EligibilityStep DeclinedPanel).
+- Invoice print mechanics: visibility-scoped print area so only the receipt prints, buttons excluded, position anchored with insetInlineStart.
+- VAT transparency: the invoice explicitly labels VAT as 'on Balinyaar's commission' with the served rate rendered as a percent — an unusually honest fee disclosure worth preserving through any redesign.
diff --git a/dev/post-phase/ui/audit/component-primitives.md b/dev/post-phase/ui/audit/component-primitives.md
new file mode 100644
index 0000000..fc4c9f8
--- /dev/null
+++ b/dev/post-phase/ui/audit/component-primitives.md
@@ -0,0 +1,68 @@
+# Shared component primitives + icon system (client/src/components/common/, components/config.ts, components/index.tsx, PlaceholderScreen, and the primitives pages are forced to hand-roll)
+
+## Current state
+
+The `client/src/components/common/` layer is eight wrappers inherited nearly verbatim from the karpolan react-mui starter: AppButton, AppIconButton, AppIcon (+ its `config.ts` string registry, `icons/PencilIcon.tsx`, `utils.ts`), AppLink (AppLinkNextNavigation.tsx), AppAlert, AppImage, AppLoading, and a class-based ErrorBoundary. Their defaults live as module constants in `components/config.ts` (APP_BUTTON_VARIANT='contained', APP_ALERT_SEVERITY='error', APP_ICON_SIZE=24, CONTENT_MAX_WIDTH=800) rather than in the theme — and `theme/theme.ts` has no `components` overrides at all, so everything MUI renders default-MUI. The wrappers carry starter DNA: AppButton ships a `margin: 1` on-all-sides default plus `label`/`text` duplicate props and spreads a `underline` prop onto non-link buttons; AppIconButton wraps its output in a useMemo keyed on a fresh `restOfProps` object; AppImage is entirely unused in product code; ErrorBoundary's fallback is raw English `{name} - Something went wrong ` plus a componentStack dump, and it is the app's ONLY error surface — there is no error.tsx/not-found.tsx/loading.tsx anywhere in the App Router tree.
+
+The icon system is `` resolving through a ~85-entry lowercase registry (`common/AppIcon/config.ts`). Feature phases added coherent snake_case domain names (check_in, post_surgery, escrow-adjacent earnings/refunds/moderation icons — all MUI `*Outlined`), but they sit next to the starter's filled set (Home, Dashboard, EventNote, Groups, PeopleAlt, AccountBalanceWallet, MedicalServices, CheckCircle, Cancel, Star, Info, Settings, AdminPanelSettings) plus eight dead starter entries (daynight/night/day/visibilityon/visibilityoff/signup/login/settings — zero usages). Two structural defects: (1) AppIcon passes `size` as SVG width/height *attributes*, which MUI SvgIcon's class CSS (`width:'1em'; height:'1em'`, SvgIcon.js:45) overrides — so every `size={14..48}` request across 40+ callsites silently renders at 24px; only the one custom SVG respects size, and that SVG is (2) the starter's Twemoji *pencil* with hard-coded cartoon fills (#EA596E, #FFCC4D…), registered as `logo` and used as the brand mark in the top bar and auth splash.
+
+Above `common/`, `components/index.tsx` exports ~35 domain composites (StatusChip, TrustBadge, PriceDisplay, OtpInput, DocumentUpload, NurseResultCard…), several of which are genuinely well built (token-driven `--bal-*` colors, i18n-agnostic contracts, BigInt-safe money). But the reusable *page* primitives were built only for the backoffice — AdminPageHeader, AdminEmptyState, AdminErrorState, ConfirmDialog, AdminDataTable, AdminPager all live in `components/admin/` and are used nowhere else — so 40+ customer/nurse pages hand-roll the same header (47 `component="h1"` occurrences), 24 files hand-roll dashed-border empty states, 12 page files assemble 57 raw MUI `` confirm flows, 81 files place 139 ad-hoc ``s, 19 files hand-concatenate `formatIrrToToman(x) + t('currency_toman')`, and 17 files new up their own `Intl.NumberFormat/DateTimeFormat`. Loading language is split between bare AppLoading spinners (~18 screens) and per-page improvised Skeleton stacks.
+
+## Problems (16)
+
+- **[high]** `client/src/components/common/AppIcon/AppIcon.tsx` — The `size` prop is silently broken for every MUI-registry icon: size is passed as SVG width/height attributes (propsToRender, lines 38-46), but MUI SvgIcon's emotion class sets `width:'1em'; height:'1em'` (node_modules/@mui/material/SvgIcon/SvgIcon.js:45) and class CSS beats presentation attributes — so all ~40 callsites requesting 14-56px (PlaceholderScreen size={48}, SelectRole size={28}, VisitNoteCard size={14}, TicketInboxScreen size={36}) render at the default 24px. Icon hierarchy across the whole app is flattened to one size; only the custom PencilIcon actually scales.
+ - evidence: AppIcon.tsx:38-46 `propsToRender = { height: size, ... size, width: size }`; SvgIcon.js:45 `width: '1em', height: '1em'` in the styled root
+- **[high]** `client/src/components/common/AppIcon/icons/PencilIcon.tsx` — The brand mark of a trust-first nursing marketplace is the starter's Twemoji cartoon pencil with hard-coded fills (#D99E82 #EA596E #FFCC4D #292F33 #CCD6DD #99AAB5) that ignore the color prop. It is registered as `logo` and rendered in the top bar (TopBarAndSideBarLayout.tsx:70) and on the auth splash (auth/BrandMark.tsx:21, which passes color="var(--bal-primary)" to no effect). First-impression trust surface = a writing-tool emoji in off-brand colors.
+ - evidence: PencilIcon.tsx:8-24 hard-coded Twemoji palette; BrandMark.tsx:21 ` `
+- **[high]** `client/src/components/common/ErrorBoundary.tsx` — The app's only crash UI is the starter ErrorBoundary: an unstyled, untranslated, LTR English `{name} - Something went wrong ` plus a `` block that dumps error.toString() and the full React componentStack to end users. No retry affordance, no brand styling, and it's what a Persian-speaking family sees if anything throws. There are also zero error.tsx / not-found.tsx / loading.tsx files in the entire App Router tree (glob over client/src/app returns nothing), so route-level failures and 404s fall through to framework defaults.
+ - evidence: ErrorBoundary.tsx:44-53 renders `… Something went wrong ` + `{this.state?.errorInfo?.componentStack}`; mounted at layout/TopBarAndSideBarLayout.tsx:104 and layout/CustomerLayout.tsx:78
+- **[high]** `client/src/components/common/AppButton/AppButton.tsx` — Starter default `margin: 1` on all sides (DEFAULT_SX_VALUES, lines 9-11) fights every real layout: 63+ callsites across 38 files pass `sx={{ m: 0 }}` just to neutralize it, and because the default only applies when NO sx is given, any button that passes other sx silently loses the margin — outer button spacing is therefore inconsistent app-wide. Default `color='inherit'` (line 45) instead of primary also forces `color="primary"` boilerplate on every CTA.
+ - evidence: grep ` ` with NO user prop at all, so every authenticated sidebar permanently shows an empty avatar with English 'Current User / Loading...' in the fa-default app.
+ - evidence: UserInfo.tsx:6 `user?: any`, :34 `'Current User'`, :36 `'Loading...'`; layout/components/SideBar.tsx:56 ` ` (plus :76 hardcoded English tooltip 'Logout Current User')
+- **[medium]** `client/src/components/admin/ConfirmDialog.tsx` — The four page-level primitives that exist — ConfirmDialog, AdminPageHeader, AdminEmptyState, AdminErrorState — are exiled under components/admin/ and used only there, so customer/nurse pages hand-roll the identical patterns: patients/page.tsx builds its own confirm Dialog + title header + skeleton + dashed empty state; addresses/page.tsx has 7 raw usages, nurse/coverage 4, nurse/services/MyServicesList 4, bookings/request/[id] 4; 47 hand-rolled `variant="h5" component="h1"` headers across 44 files; 30 `border: '1px dashed'` empty-states across 24 files.
+ - evidence: grep `` display primitive, so 19 files hand-assemble `{formatIrrToToman(x)} {tc('currency_toman')}` and 17 files construct their own `Intl.NumberFormat/DateTimeFormat` — money rendering (grouping, Persian digits, signed negative styling on earnings/refunds) and date rendering are re-decided per page on the most trust-sensitive surfaces (checkout, invoice, earnings, refund status). PriceDisplay only covers catalog unit-rates.
+ - evidence: grep currency_toman → 24 occurrences / 19 files (checkout/page.tsx, invoice/page.tsx, EarningsBalanceHeader…); grep Intl.(NumberFormat|DateTimeFormat) → 26 / 17 files outside utils
+- **[medium]** `client/src/components/config.ts` — Starter config as app-wide defaults: a bare `` renders a FILLED ERROR alert (APP_ALERT_SEVERITY='error'), CONTENT_MAX_WIDTH=800 with the nonsensical starter comment 'CONTENT_MIN_WIDTH = 320 // CONTENT_MAX_WIDTH - Sidebar width'. These component defaults belong in `createTheme({ components })` — and theme/theme.ts (lines 20-35) defines NO components overrides at all, which is exactly why the whole app renders as default MUI.
+ - evidence: components/config.ts:10 `APP_ALERT_SEVERITY = 'error'`, :4-5 starter comment; theme/theme.ts:20-35 createTheme with no `components` key
+- **[low]** `client/src/components/common/AppButton/AppButton.tsx` — Starter prop cruft: duplicate `label`/`text` props ('Alternate to .text'), a `// Missing props` comment block, jsdoc claiming a 'Box around to specify margins' that doesn't exist, and line 85 `{...{ ...restOfProps, underline }}` spreads `underline="none"` onto plain (non-link) MUI Buttons as an invalid DOM attribute.
+ - evidence: AppButton.tsx:16-19 label/text + ':19 // Missing props'; :28 jsdoc 'with Box around'; :85 underline spread
+- **[low]** `client/src/components/common/AppIcon/AppIcon.tsx` — Unknown icon names `console.warn` in production and silently render MoreHoriz ('…') — a wrong icon name in a trust surface degrades to an ellipsis nobody notices; the invalid `size` attribute is also spread onto the DOM , and the documented `title` tooltip does nothing (title attribute on inline SVG is not a tooltip; MUI wants `titleAccess`).
+ - evidence: AppIcon.tsx:33-35 warn + `ICONS.default` fallback; :8-13 Props documents `title` as hover hint
+- **[low]** `client/src/components/common/AppImage/AppImage.tsx` — Dead starter component: zero product usages (only its own test imports it) yet still exported from the common barrel; hard-codes `unoptimized={true}` citing a 'custom loader' that doesn't exist, defaults 256x256, English `alt='Image'` fallback.
+ - evidence: grep )`) — two competing loading languages and no reusable ListSkeleton/CardSkeleton/DetailSkeleton.
+ - evidence: AppLoading used in 18 page files (search, checkout, bnpl, profile…); 40+ files import MUI Skeleton directly with per-page layouts
+- **[low]** `client/src/components/common/AppIconButton/AppIconButton.tsx` — Cargo-cult useMemo wraps the rendered IconButton keyed on `restOfProps` — a new object every render — so it never memoizes anything; alpha() hover hack for non-MUI colors is starter residue.
+ - evidence: AppIconButton.tsx:62-85 useMemo deps include restOfProps
+- **[low]** `client/src/app/[locale]/(private-routes)/nurse/page.tsx` — The nurse's post-login home is still a PlaceholderScreen ('dashboard' icon + generic placeholder body) — for the supply side of the marketplace, the landing screen is literally the empty-state scaffold; admin/users and admin/notifications are also placeholders.
+ - evidence: nurse/page.tsx:7 `return `; grep PlaceholderScreen under app/ → 4 files
+
+## Opportunities (10)
+
+- **One-file icon-system swap to a single coherent family** (impact: high, effort: medium) — The string-registry indirection means the entire app's iconography can be replaced by editing only AppIcon/config.ts: pick ONE family (all MUI *Rounded for warmth, or an inlined open set like Phosphor/Solar with softer strokes that suits 'clinical-but-human'), map all 85 names to it, add the missing names (back — RTL-flippable via a wrapper that rotates in rtl, receipt, copy, share, help, kebab, phone, plain check), delete the 8 dead starter entries, and type IconName strictly so unknown names fail at compile time instead of console.warn + '…'.
+- **Fix AppIcon sizing via fontSize, not attributes** (impact: high, effort: small) — Change AppIcon to drive MUI icons with `style={{ fontSize: size }}` (or sx) instead of width/height attributes. This single fix restores the intended 14-56px hierarchy at every existing callsite simultaneously — the highest leverage-per-line change available in the codebase.
+- **Real Balinyaar brand mark** (impact: high, effort: medium) — Replace the Twemoji pencil: design a simple symbol (e.g. a home + pulse/leaf motif in deep teal with a cream counter, terracotta only as micro-accent) as a currentColor SVG so BrandMark's `color="var(--bal-primary)"` actually works, and use it in the top bar, auth splash, favicon, and the future email/invoice header. For a trust-first healthcare product the mark is a functional trust cue, not decoration.
+- **Promote the admin primitives + build the missing shared kit** (impact: high, effort: large) — Move ConfirmDialog, PageHeader, EmptyState, ErrorState out of components/admin into common/ and add the primitives pages provably keep hand-rolling: Section/AppCard (one Paper recipe — 139 ad-hoc Papers today), ListSkeleton/CardSkeleton, Money (signed coloring, Persian digits, Toman label — 19 hand-rolled sites), DateText (Shamsi via existing utils/date.ts), DescriptionList (61 hand-rolled label/value rows), StatCard, FormSection, BackLink. Then sweep pages onto them. This is the difference between 'restyled starter' and 'design system'.
+- **Theme components layer instead of wrapper constants** (impact: high, effort: medium) — Add a `components` section to createTheme: MuiButton (defaultProps color='primary', disableElevation, no default margin — retire DEFAULT_SX_VALUES and the 63 `m:0` patches), MuiAlert (standard severity semantics, soft brand-tinted backgrounds), MuiPaper (outlined-by-default with --bal tokens), MuiChip radius, MuiTextField shape, focus-visible rings in teal. Wrappers then shrink to genuine additions (icon-by-name, link composition) instead of re-defaulting MUI per instance.
+- **Branded error / 404 / loading route files** (impact: high, effort: medium) — Add locale-aware error.tsx, global-error.tsx, not-found.tsx and per-shell loading.tsx with calm Persian copy, the brand mark, and a retry CTA; give ErrorBoundary the same visual fallback and stop printing componentStack to users (log it instead). Crash surfaces are trust surfaces in healthcare.
+- **Nurse home dashboard (currently a placeholder)** (impact: high, effort: large) — Replace the nurse PlaceholderScreen with a real home: today's visits with check-in shortcuts, pending request countdowns, earnings snapshot (reusing EarningsBalanceHeader), verification/credential-expiry nudges, and unread support tickets. The supply side currently lands on an empty scaffold every login.
+- **Richer trust presentation built on TrustBadge** (impact: high, effort: medium) — TrustBadge is a small chip; trust is the product. Add a VerificationPanel primitive for nurse profile/search detail: what was verified (identity, license, Shahkar), when, by whom, with an expandable 'how Balinyaar verifies' explainer — turning the existing honest badge state into a persuasive, inspectable trust story.
+- **Warm empty-state illustration set** (impact: medium, effort: medium) — Replace the 24 dashed-border Paper empty states with a shared EmptyState primitive that accepts a small branded SVG illustration (cream/teal line style, terracotta accent) per domain — patients, addresses, bookings, earnings, search-no-results — moving the tone from 'unconfigured dashboard' to 'calm, human product'.
+- **Fix the sidebar identity block** (impact: medium, effort: small) — Replace starter UserInfo with a typed ProfileSummary fed by the /me query (display name, masked phone with Persian digits, role label, TrustBadge for nurses) — the current always-'Loading...' English block undermines every authenticated screen.
+
+## Keep (do not regress)
+
+- The AppIcon string-registry indirection itself (`` + one config.ts) — it concentrates the entire icon system into a single swap point, and the snake_case domain names (check_in, post_surgery, escrow-era earnings/refunds/moderation) are well-chosen and consistently used.
+- StatusChip and TrustBadge (components/StatusChip, components/TrustBadge): fully token-driven via --bal-* semantic vars (auto dark-scheme), data-status/data-badge-state test hooks, and TrustBadge's honest-by-construction states (verified only when aggregate approved; expired visually distinct from never-verified, unverified deliberately non-alarming) — the right foundation for trust UI.
+- utils/money.ts + PriceDisplay discipline: BigInt integer-safe Rial↔Toman, Persian digit formatting, totals only ever price×count at the field boundary — never regress this on any new Money primitive.
+- components/admin/ConfirmDialog's interaction contract: required-reason gating, loading state that disables both buttons and prevents double-submit, caller-owns-the-mutation separation — promote it, don't rewrite it.
+- The 'already-translated props' presentational contract (PlaceholderScreen, AdminPageHeader, StepperHeader document it explicitly) keeping primitives i18n-agnostic, and their RTL-safe logical flex layouts with no directional CSS.
+- booking/format.ts and utils/date.ts: locale-aware clocks and Shamsi dates via Intl (fa-IR-u-ca-persian) with no date library, with honest null returns for open check-ins.
+- AppLink's Next.js+MUI composition: external links auto-get target=_blank + rel='noopener noreferrer', internal links go through NextLink, active-class support — works and is RTL-neutral.
+- The consistent `@/components` barrel import pattern — every page already imports primitives from one place, which makes the coming design-system sweep mechanical.
diff --git a/dev/post-phase/ui/audit/cross-cutting-ux.md b/dev/post-phase/ui/audit/cross-cutting-ux.md
new file mode 100644
index 0000000..dab7699
--- /dev/null
+++ b/dev/post-phase/ui/audit/cross-cutting-ux.md
@@ -0,0 +1,74 @@
+# CROSS-CUTTING UX pattern audit — client/src (loading/empty/error states, tokens, RTL, responsiveness, a11y, motion, toasts, metadata, formatting)
+
+## Current state
+
+The client has a two-tier quality profile: feature surfaces built during the f0–f15 phases are disciplined, while everything inherited from the MUI starter is untouched. Loading is a dual system — the starter `AppLoading` spinner (CircularProgress wrapper, 47 refs in 23 files) gates whole pages (customer home, checkout, auth splash), while MUI `Skeleton` is broadly adopted for list/detail loads (54 files). Nearly every data screen implements a real four-state branch (skeleton → error-with-retry → empty → data; ~95 `isError` refs across 55 files, ~124 retry/refetch refs), e.g. `CategoryGrid` in `(customer)/page.tsx:169-207`. Errors surface through a unified toast pipeline (`lib/toast/dispatchToast` → ToastBridge → notistack, used in 37 files including the fetch layer, brand-styled via tokens) plus inline `AppAlert`. Empty states are shared only in the backoffice (`components/admin/AdminEmptyState/AdminErrorState`); customer/nurse pages hand-roll the same dashed-border Paper 29 times across 23 files. There are zero Next.js `loading.tsx`/`error.tsx`/`not-found.tsx` files, the React `ErrorBoundary` is raw starter HTML with a stack trace, and the only `metadata` export in the whole app is the root layout's single static title.
+
+Theming and internationalization are the strongest cross-cutting layers. Colors flow almost exclusively through `theme/tokens.css` custom properties (331 `var(--bal-*)` refs across 103 files; the only hard-coded hexes in .tsx are the starter PencilIcon SVG and tests), with complete light/dark schemes flipped by `data-mui-color-scheme`. RTL discipline is excellent: logical `start`/`end` everywhere, deliberate `dir="ltr"` islands for phone numbers, IBANs, OTP boxes, countdowns and coordinates (30+ sites), a direction-aware Emotion cache, and `PhoneNumberField` normalizing Persian/Arabic digits. Dates render Shamsi via `utils/date.ts` (`fa-IR-u-ca-persian`, 82 uses in 37 files) and numbers via `Intl.NumberFormat('fa-IR')` — though the `locale === 'fa' ? 'fa-IR' : 'en-US'` ternary is copy-pasted at 25+ call sites and there is no relative-time formatting anywhere. Responsive behavior relies on single-column flows and the mobile-first customer shell (TopBar + 5-tab BottomBar, content capped at 800px); explicit breakpoints appear only 12 times in 10 files and `useIsMobile` only in layouts. Motion is essentially absent (about 39 `transition` matches, almost all hover border-color; no keyframes, no reduced-motion handling). A11y is moderate: 47 `aria-` attributes in 28 of ~300 tsx files, with excellent pockets (OtpInput, NurseResultCard keyboard activation) and gaps (icon-only buttons named solely via Tooltip `title`). The nurse/admin/partner shells still run the starter `TopBarAndSideBarLayout` + `SideBar` + `UserInfo` chrome, `theme/theme.ts` defines no `components` overrides at all, and `AppButton` ships the starter's default `margin: 1` — visible as 213 `m: 0` workarounds across 82 files.
+
+## Problems (19)
+
+- **[high]** `client/src/components/common/ErrorBoundary.tsx` — The app-wide error boundary (wrapping every shell via TopBarAndSideBarLayout.tsx:104 and CustomerLayout.tsx:78) is raw starter UI: unstyled English '{name} - Something went wrong' plus the raw error.toString() and full componentStack inside a — untranslated, unbranded, leaks internals to end users, and offers no retry/back affordance.
+ - evidence: lines 44-53: `{this.props.name} - Something went wrong ... {this.state?.errorInfo?.componentStack}`
+- **[high]** `client/src/components/UserInfo/UserInfo.tsx` — The sidebar identity block is dead starter code: SideBar.tsx:56 renders ` ` with no user prop, so every nurse/admin/partner permanently sees the English literals 'Current User' and 'Loading...' in the drawer of the fa-default app.
+ - evidence: lines 34-36: `{fullName || 'Current User'}` / `{userPhoneOrEmail || 'Loading...'}`; prop typed `user?: any` and never supplied
+- **[high]** `client/src/app/[locale]/layout.tsx` — The single metadata export in the entire app — every one of ~60 routes shares the title 'Balinyaar | بالینیار' and the placeholder description 'Balinyaar web application'; no generateMetadata, no per-page or per-locale titles, so browser tabs, history and share previews are indistinguishable.
+ - evidence: lines 54-58; grep for generateMetadata/ across client/src returns only this file
+- **[high]** `client/src/components/common/AppIcon/config.ts` — The brand 'logo' icon is the starter's PencilIcon (a pencil is the logo in the nurse/admin top bar and auth), and the icon set mixes filled and outlined MUI weights (Star, CheckCircle, VerifiedUser, AccountCircle, Groups filled vs ~50 Outlined imports) — the direct source of the 'ugly icons' problem.
+ - evidence: line 115: `logo: PencilIcon,`; lines 4-32 filled imports vs lines 35-98 Outlined imports
+- **[high]** `client/src/app/[locale]/(public-routes)/layout.tsx` — No route-level loading.tsx, error.tsx, not-found.tsx or global-error.tsx exists anywhere under client/src/app — unknown URLs render Next's default unbranded English 404, route transitions have no suspense fallback, and server-render failures show the default Next error screen (the public segment contains only /login).
+ - evidence: Glob client/src/app/**/{loading,error,not-found}.tsx → 'No files found'
+- **[medium]** `client/src/components/common/AppButton/AppButton.tsx` — Starter default `margin: 1` on every button (DEFAULT_SX_VALUES) forces callers to write `sx={{ m: 0 }}` everywhere — 213 occurrences across 82 files — and passing any custom sx silently drops the default, making button spacing inconsistent by construction.
+ - evidence: lines 9-11 and 50: `sx: propSx = DEFAULT_SX_VALUES` where `DEFAULT_SX_VALUES = { margin: 1 }`
+- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/page.tsx` — No shared user-facing EmptyState/ErrorState component: the dashed-border Paper pattern (`p: 3-4, textAlign: 'center', border: '1px dashed', borderColor: 'divider'`) is hand-rolled 29 times across 23 files (this file x2, search/results/page.tsx x2, nurse/visits/page.tsx, bookings/request/page.tsx x2, …) while admin got AdminEmptyState — all text-only, no icon/illustration, inconsistent copy and CTA presence.
+ - evidence: lines 176-195 vs identical blocks in search/results/page.tsx:79,115 — grep `border: '1px dashed'` → 29 hits in 23 files
+- **[medium]** `client/src/app/[locale]/(private-routes)/nurse/page.tsx` — The nurse dashboard — the landing screen for the entire nurse role — is still a PlaceholderScreen stub ('placeholder_body'), despite requests/visits/earnings/verification data all existing in services.
+ - evidence: line 7: `return `
+- **[medium]** `client/src/theme/theme.ts` — createTheme defines no `components` overrides at all — every MUI control (AppBar, Button, TextField, Chip, Tabs, Dialog) renders stock MUI apart from palette/radius/font, which is precisely why the app reads as a default-MUI starter rather than the calm warm brand.
+ - evidence: lines 19-36: theme = cssVariables + colorSchemes + typography + shape only
+- **[medium]** `client/src/layout/TopBarAndSideBarLayout.tsx` — Nurse/admin/partner chrome is untouched starter: untranslated English tooltips in the fa-default app ('Open Sidebar' here, 'Logout Current User' in components/SideBar.tsx:76), physical paddingLeft/paddingRight keyed off anchor strings, and TopBar.tsx still carries the starter comment `// boxShadow: 'none', // Uncomment to hide shadow` with a centered nowrap title that can clip long Persian titles.
+ - evidence: line 71: `title={sidebarProps.open ? undefined : 'Open Sidebar'}`; TopBar.tsx:20,34
+- **[medium]** `client/src/components/notifications/NotificationRow.tsx` — No relative-time formatting exists anywhere in the client (grep for ago/RelativeTimeFormat only hits a mock) — notifications, ticket inbox rows, and audit entries all show full absolute Shamsi timestamps, which is the wrong grain for inbox-style UIs ('۲ ساعت پیش' expected).
+ - evidence: grep `RelativeTime|timeago|ago` → only services/payouts/apis/mockApi.ts
+- **[medium]** `client/src/components/NurseResultCard/NurseResultCard.tsx` — The `locale === 'fa' ? 'fa-IR' : 'en-US'` Intl ternary is duplicated at 25+ call sites (here lines 17, 39; CountdownTimer.tsx:84; booking/format.ts:11,21,45; earnings pages; checkout) instead of a shared helper next to utils/date.ts — and it has already drifted: admin/partners/[id]/page.tsx:121 passes the raw app locale ('fa') which resolves to Gregorian-locale digits differently than 'fa-IR'.
+ - evidence: grep `locale === 'fa' ? 'fa-IR'` → 25+ occurrences; admin/partners/[id]/page.tsx:121 uses `new Intl.NumberFormat(locale, …)`
+- **[medium]** `client/src/components/common/AppIconButton/AppIconButton.tsx` — Icon-only buttons are named for AT solely via MUI Tooltip `title` (aria-describedby, not an accessible name) and disabled buttons drop the Tooltip entirely — combined with overall thin aria coverage (47 aria- attributes across 28 of ~300 tsx files), most icon buttons have no accessible name.
+ - evidence: lines 89-95: Tooltip wrap only when `title && !disabled`; no aria-label fallback
+- **[low]** `client/src/components/config.ts` — Explicit responsive design is nearly absent outside the shells — 12 breakpoint usages in 10 files app-wide and useIsMobile only in layout/ — so desktop renders as a centered 800px phone column (CONTENT_MAX_WIDTH = 800) with no use of wider viewports on any customer or nurse screen.
+ - evidence: line 4: `export const CONTENT_MAX_WIDTH = 800`; grep `xs:|sm: |md: |breakpoints` in *.tsx → 12 hits
+- **[low]** `client/src/app/globals.css` — Starter reset still sets `max-height: 100vh` on html/body plus blanket `overflow-x: hidden` — max-height on body serves no purpose and is a latent scroll/sticky bug; the shells then re-implement their own scroll containers around it.
+ - evidence: `html, body { max-width: 100vw; overflow-x: hidden; max-height: 100vh; }`
+- **[low]** `client/src/hooks/layout.ts` — Starter mobile-detection module kept verbatim: three alternative hooks with commented-out variants, a body-classList mutation hook, and SSR always guessing mobile (SERVER_SIDE_MOBILE_FIRST = true), which makes desktop users see a one-frame mobile-first layout shift after hydration in the sidebar shells.
+ - evidence: lines 7-8, 39-48, 58-71, 76-79
+- **[low]** `client/src/theme/typography.ts` — The EN brand display font (Space Grotesk) is referenced in the font stack but never loaded — the comment admits 'Not currently wired to a font loader' — so English headings silently fall back to system fonts; no type-scale tuning (sizes/line-heights) exists for Persian body text either.
+ - evidence: lines 3-5: `/** Space Grotesk … Not currently wired to a font loader */`
+- **[low]** `client/src/components/common/AppIcon/AppIcon.tsx` — Unknown icon names log a console.warn in production and silently render the MoreHoriz starter 'default' icon — misspelled icon keys degrade invisibly (icon prop is typed `IconName | string`, so typos compile).
+ - evidence: lines 33-36: `console.warn(`AppIcon: icon "${iconName}" is not found!`)`
+- **[low]** `client/src/layout/CustomerLayout.tsx` — No motion system anywhere: page/content transitions, list item entrances and skeleton→content swaps are all hard cuts (only ~39 transition matches app-wide, almost all hover border-color), and there is no prefers-reduced-motion handling to pair one with — the app feels static rather than calm.
+ - evidence: grep `keyframes|Fade|Grow|Collapse|animation` → 39 hits in 27 files, none page-level
+
+## Opportunities (10)
+
+- **One shared StateView system (empty / error / offline) for user-facing pages** (impact: high, effort: medium) — Promote the 29 hand-rolled dashed-Paper blocks into a single branded StateView component (icon or small warm illustration, title, body, optional CTA/retry), with the admin AdminEmptyState/AdminErrorState folded in as variants. Every list and detail page immediately gains consistent, warmer empty/error moments, and future screens get them for free.
+- **Route-level chrome: loading.tsx skeleton shells, branded error.tsx and not-found.tsx, ErrorBoundary rewrite** (impact: high, effort: small) — Add per-route-group loading.tsx (skeleton of each shell), a Persian-first branded 404 with a way home, and error.tsx/global-error.tsx sharing one calm 'something went wrong' design with a retry button; rewrite components/common/ErrorBoundary.tsx to the same design (dev-only stack). Small files, disproportionate perceived-quality lift — these are the screens users hit at their most anxious moments in a healthcare product.
+- **Brand pass via theme.components — kill the default-MUI look in one file** (impact: high, effort: medium) — Add component overrides to theme.ts: cream AppBar with teal text instead of the default filled bar, softer Paper/Card treatment, pill-ish buttons without AppButton's default margin (deleting 213 m:0 workarounds), branded TextField/Chip/Tabs focus and hover states, and terracotta reserved for the single accent. This is the highest-leverage restyle since zero overrides exist today — every screen changes at once.
+- **Real nurse dashboard replacing the placeholder** (impact: high, effort: medium) — nurse/page.tsx should compose data that already exists in services/: today's visits with check-in CTA (f8), pending requests with countdown (f7), verification progress ring (f5), this-week earnings (f12), and profile-completeness nudges. The nurse role currently lands on a stub — the single biggest missing screen in the app.
+- **Single-weight icon system + a real logomark** (impact: high, effort: medium) — Replace the mixed filled/outlined MUI set with one consistent weight (e.g. Material Symbols Rounded outlined, or a licensed healthcare set) mapped through the existing AppIcon registry — the abstraction makes the swap mechanical — and replace PencilIcon with an actual Balinyaar logomark used in TopBar, auth, favicon and the future 404/empty states.
+- **Trust-forward nurse cards and profile hero** (impact: high, effort: medium) — Trust is the product: extend NurseResultCard and the public nurse profile beyond the single ✓ badge with credential chips (پروانه نظام پرستاری), completed-visit counts, years of experience, response-time, and a 'payments held in escrow' reassurance strip reusing EscrowNotice; add a 'how we verify' sheet linked from every badge. All data exists in search/verification services or is one field away.
+- **Per-page titles + PageHeader unification** (impact: medium, effort: small) — Introduce a title template ('%s | بالینیار') with generateMetadata per route (localized), and generalize the admin-only AdminPageHeader into a shared PageHeader so customer/nurse pages stop hand-rolling h5/h1 blocks — fixing tab/history UX and heading consistency together.
+- **Locale formatting helpers: formatNumber + relative time** (impact: medium, effort: small) — Add utils/number.ts (wrapping the copy-pasted `locale === 'fa' ? 'fa-IR' : 'en-US'` ternary) and a formatRelativeTime using Intl.RelativeTimeFormat('fa') for notifications, ticket inboxes and audit rows; migrate the 25+ inline call sites. Removes drift risk and makes inbox surfaces read naturally.
+- **Public landing + public nurse profiles** (impact: high, effort: large) — The anonymous web surface is only /login today. A trust-first marketplace needs a public front door: hero with search, how-it-works (request → escrow payment → verified visit → weekly payout), category grid (reusing CategoryTile), verified-nurse counters, and indexable public nurse profiles — the acquisition and SEO surface the product currently lacks entirely.
+- **Desktop-aware layouts + gentle motion pass** (impact: medium, effort: medium) — Above ~900px let key customer flows use the space (search results as list+detail or two-column checkout with a sticky order summary instead of the 800px phone column), and add one restrained motion layer (150–200ms content fade/slide, skeleton crossfade) behind a prefers-reduced-motion guard to make the app feel calm rather than static.
+
+## Keep (do not regress)
+
+- Token discipline: essentially zero hard-coded colors in feature .tsx (only the starter PencilIcon SVG and tests); 331 var(--bal-*) references across 103 files, with complete, deliberate light and dark schemes in theme/tokens.css — dark mode flips cleanly via data-mui-color-scheme.
+- RTL correctness as a habit: logical start/end and marginInline throughout, deliberate dir="ltr" islands for phone numbers, IBANs, OTP boxes, countdowns, ticket codes and map coordinates (30+ sites), direction-aware Emotion cache with stylis-plugin-rtl, and PhoneNumberField/PatientForm normalizing Persian/Arabic digit input.
+- Shamsi-first formatting: utils/date.ts renders fa-IR-u-ca-persian via Intl (82 uses across 37 files), money utils centralize IRR→Toman with fa-IR digits, and tabular-nums + dir=ltr is applied where numbers sit in RTL text.
+- The four-state data pattern (skeleton → error-with-retry → empty → data) is genuinely implemented across feature pages (e.g. CategoryGrid in (customer)/page.tsx:169-207) — ~95 isError branches and ~124 retry/refetch affordances; state coverage needs restyling, not rebuilding.
+- Unified toast pipeline: dispatchToast → ToastBridge → notistack, callable from non-React fetch code, brand-styled via tokens in NotistackProvider, used consistently across 37 files.
+- AdminDataTable: RTL-safe align='inherit', horizontal scroll inside its own container so wide worklists never break the page, typed column renderers, aria-label support.
+- TrustBadge's honest three-state design (verified/unverified/expired, 'never a hard-coded hex', unverified deliberately non-alarming) and the verified-only search invariant surfaced on every result card.
+- Customer shell fundamentals: mobile-first TopBar + 5-tab BottomBar with locale-aware longest-prefix active-tab matching, reading-width content column, and support/notification affordances in the header.
+- OtpInput: exemplary a11y/RTL work — dir=ltr group with role=group, per-box aria-labels, paste splitting, and automatic focus advance; NurseResultCard is keyboard-activatable with a visible focus ring.
+- Mikhak font strategy (fa-only attachment, preload:false so /en never downloads it) and the root-layout locale/dir/color-scheme wiring with its documented reasoning.
diff --git a/dev/post-phase/ui/audit/customer-account.md b/dev/post-phase/ui/audit/customer-account.md
new file mode 100644
index 0000000..f614138
--- /dev/null
+++ b/dev/post-phase/ui/audit/customer-account.md
@@ -0,0 +1,68 @@
+# Customer account & care-circle management (profile, patients, patient care record, addresses + map picker)
+
+## Current state
+
+The area lives under `client/src/app/[locale]/(private-routes)/(customer)/` inside `CustomerLayout` (slim TopBar + 5-tab BottomBar, content column capped at CONTENT_MAX_WIDTH). `profile/page.tsx` is a single flat form: first/last name, a preferred-language select, an emergency-contact section (name + `PhoneNumberField`), one save button, and an outlined Paper card linking to the address book. `patients/page.tsx` is a header + `PatientCard` list with add/edit via `PatientForm` reused in a `Dialog maxWidth="sm"`, soft-archive with a confirm dialog, a 2-row Skeleton loader, and a dashed-border empty state with icon + CTA. `patients/[id]/record/page.tsx` is the care-record viewer: shared `PatientHeader`, a family-ownership banner on `--bal-primary-soft`, four scrollable Tabs (داروها/روتین/سوابق/وظایف); the three editable tabs use a whole-list "edit mode" (every row becomes small TextFields, save-all), history is read-only `VisitNoteCard`s with text prev/next pagination; access is gated by `useRecordAccess` with a non-leaking access-denied card. `addresses/page.tsx` mirrors the patients page: `AddressCard` list (primary badge via `StatusChip status="verified"`), add/edit dialog hosting `AddressForm` = title + `CascadingRegionSelect` (province→city→district with loading adornments and an explicit "whole city" option) + `AddressMapPicker` + multiline address line + set-primary switch.
+
+Styling is disciplined and token-driven: everything is `elevation={0}` Paper with `border: 1px solid divider, borderRadius: 2`, colors come from `--bal-*` CSS variables (both schemes), text uses `text.secondary`, and RTL is handled with logical properties (`textAlign:'start'`, `marginInlineStart:'auto'`) — `AddressMapPicker` even pins itself `dir="ltr"` with inline styles and a comment explaining the stylis-RTL transform hazard. The weak points are structural rather than cosmetic: the "map" is a coordinate-grid stand-in (no tiles/search/geocoding, raw lat/lng shown), query errors collapse into empty/blank states, long forms are crammed into non-fullscreen modals on a mobile-first shell, there is no avatar/photo concept anywhere in the customer identity system, and the Profile tab lacks basic account affordances (no sign-out anywhere in the customer shell, no phone display, no locale switch).
+
+## Problems (16)
+
+- **[high]** `client/src/app/[locale]/(private-routes)/(customer)/patients/page.tsx` — Query errors collapse into the empty state: a failed usePatients() leaves data undefined so the page shows 'هنوز بیماری ثبت نشده' with an add CTA — telling a family their care recipients don't exist and inviting duplicate re-entry. No isError branch or retry exists. Identical bug in addresses/page.tsx line 91.
+ - evidence: line 83: `const isEmpty = !isLoading && patients.length === 0;` — isError is never read from the query
+- **[high]** `client/src/app/[locale]/(private-routes)/(customer)/profile/page.tsx` — Profile load error is silently swallowed: only isLoading is handled, so on a failed useCustomerProfile() the form renders blank (initial=null) and a save would overwrite server truth with empty fields. No isError state, no retry.
+ - evidence: lines 16-18: `const { data: profile, isLoading } = useCustomerProfile(); ... if (isLoading) return ;` then `initial={profile ?? null}`
+- **[high]** `client/src/layout/CustomerLayout.tsx` — No sign-out affordance exists anywhere in the customer experience: CustomerLayout has only support/bell/dark-toggle chrome and the 5-tab BottomBar; logout lives only in SideBar.tsx, which the customer shell never renders, and the Profile tab (the natural home for it) has no account section, no phone-number display, and no sign-out. A logged-in customer literally cannot log out.
+ - evidence: CustomerLayout renders TopBar(startNode=support, endNode=bell+DarkModeToggleButton)+BottomBar only; grep for logout hits layout/components/SideBar.tsx but no (customer) file
+- **[high]** `client/src/components/geography/AddressMapPicker.tsx` — The 'map pin picker' is a blank coordinate grid, not a map: no tiles, no address search, no geocode, no locate-me — a family user is asked to place a pin on a featureless 220px grid whose output feeds the later EVV proximity check, so a meaningless pin is near-guaranteed. It also surfaces raw latitude/longitude captions ('عرض: 35.71234') to consumers — developer-grade UI in the most trust-sensitive form of the account area.
+ - evidence: lines 30-34 doc: 'It is NOT a real map (no Neshan/Google tiles), only a bounded canvas'; lines 143-152 render `{latLabel}: {value.latitude.toFixed(5)}`
+- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/addresses/page.tsx` — The long address form (title + 3 cascading selects + 220px map + multiline line + switch + actions) is hosted in a Dialog maxWidth='sm' that is not fullScreen on mobile — on the phone-first customer shell this yields a cramped double-scroll (DialogContent + keyboard) modal; additionally backdrop-click/onClose silently discards a half-completed form with no dirty-state guard. Same pattern for PatientForm in patients/page.tsx line 160.
+ - evidence: line 170: `` — no fullScreen={isMobile}, no discard confirmation
+- **[medium]** `client/src/services/profiles/types.ts` — No avatar/photo exists anywhere in the customer identity system: avatarUrl is nurse-only (NurseProfile line 29-31), CustomerProfile has none, and PatientHeader/PatientCard render text-only with no Avatar/initials slot. For a marketplace where a nurse walks into a stranger's home, a photo (or at least a generated-initials avatar) of the care recipient and the account holder is an expected identification and warmth affordance — its absence makes the patients list read as a data table of names.
+ - evidence: CustomerProfile (lines 50-54) has firstName/lastName/preferredLanguage only; PatientHeader.tsx imports no Avatar
+- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/patients/[id]/record/page.tsx` — Care-record editing is a whole-list 'edit mode' per tab: tap edit → every medication becomes a stack of 4 free-text TextFields → save-all. There is no per-item add/edit sheet, dose/frequency/time-of-day are unstructured free text (routine_time line 321 is a plain TextField), and switching tabs while editing unmounts the tab component and silently destroys the draft (tab state lives in the parent, draft in the child) with no warning. For medication data this is an error-prone editing surface.
+ - evidence: lines 118-135 Tabs drive `` which conditionally mounts MedicationsTab/RoutineTab/TasksTab; each holds `const [draft, setDraft] = useState(...)` lost on unmount
+- **[medium]** `client/src/components/PatientCard/PatientCard.tsx` — The tappable identity area that opens the care record is an invisible affordance: an unstyled `component="button"` (background:none, border:none) with no chevron, no 'view record' label, no hover/pressed state — nothing signals that the card body is clickable, so the record viewer (the richest screen in the area) is undiscoverable; the only visible actions are edit/archive icons.
+ - evidence: lines 66-85: `{header} ` — no visual affordance styles
+- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/patients/[id]/record/page.tsx` — Visit-note history (سوابق) presents longitudinal clinical info as a flat, undifferentiated card list with bare text 'قبلی/بعدی' pagination — no grouping by date/booking, no link to the booking the note came from, no indication of which nurse/service, no filtering. For the one place a family reviews their loved one's care over time, the presentation carries no narrative or hierarchy.
+ - evidence: lines 431-448: `items.map((note) => )` followed by prev/next text buttons + 'page_of' caption
+- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/profile/page.tsx` — The Profile tab — one of only five bottom-nav destinations — is a flat settings form, not an account hub: no identity header, no avatar, no account phone, no links to notifications/support/bookings history/locale, and the 'profile completeness' cue is a single color-coded text line (color-only state signal). It reads as a leftover form page where users will expect the app's account center.
+ - evidence: lines 75-77: completeness is `` — the page's only status affordance
+- **[low]** `client/messages/fa.json` — Persian copy bug in the address-line hint: 'پلاک، خیابان، واحد — جزئیاتی که پرستار برای یافتن در نیاز دارد.' — 'برای یافتن در' is grammatically broken (word dropped, likely 'یافتن درِ منزل' or just 'یافتن نشانی'). This text sits under the most-filled field of the address form.
+ - evidence: address.line_hint value in messages/fa.json
+- **[low]** `client/src/components/GenderToggle/GenderToggle.tsx` — ToggleButtonGroup is left at its default inline-flex sizing so the `flex: 1` on child buttons has no room to distribute — the required gender toggle renders content-width and visually misaligned against the fullWidth TextFields above it in PatientForm; the intended equal-half layout never materializes.
+ - evidence: lines 43-49: sx sets `'& .MuiToggleButton-root': { flex: 1 }` but the group has no fullWidth/width:'100%'
+- **[low]** `client/src/components/RelationSelect/RelationSelect.tsx` — Radio-card selection is communicated by border color alone — no check icon, no fill change, no focus-visible or hover styling on the Paper cards — a color-only state signal that is weak in dark mode and fails WCAG 1.4.1 use-of-color for the selected state.
+ - evidence: lines 53-55: `borderColor: selected ? 'primary.main' : 'divider'` is the entire selected treatment
+- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/profile/page.tsx` — Loading-state inconsistency within the same tab cluster: profile uses the full-page AppLoading spinner while patients/addresses/record all use content-shaped Skeletons — the account area flickers between two different loading languages.
+ - evidence: line 18: `if (isLoading) return ;` vs patients/page.tsx lines 103-108 Skeleton rows
+- **[low]** `client/src/components/geography/AddressMapPicker.tsx` — Hard-coded rgba shadow on the pin (`drop-shadow(0 1px 2px rgba(0,0,0,0.35))`) instead of a token — the only non-token color in the audited area; slightly heavy against the light --bal-primary-soft canvas in dark mode.
+ - evidence: line 116: `filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.35))'`
+- **[low]** `client/src/components/PatientForm/PatientForm.tsx` — The form collects only a whole-year age that is stored as a fabricated Jan-1 birthDate (age.ts), and a single full-name field naively split into first/last (splitName lines 32-38, lastName falls back to firstName). Acceptable MVP shortcuts, but the fabricated birth date will surface later (records, invoices) as a false precision the family never entered.
+ - evidence: age.ts line 10: `return `${year}-01-01`;`
+
+## Opportunities (10)
+
+- **Real map with search + locate-me for addresses** (impact: high, effort: large) — Replace the grid stand-in behind the existing AddressMapPicker component boundary with real Neshan tiles (a Neshan adapter already exists server-side from refinement phase 8): address search box, GPS locate-me button, reverse-geocode preview of the pin ('پین روی: خیابان ولیعصر، ...'), and drop the raw lat/lng captions. This is the single highest-trust upgrade in the area since the pin feeds nurse arrival and EVV.
+- **Turn the Profile tab into an account hub** (impact: high, effort: medium) — Redesign profile/page.tsx as a settings hub: identity header (avatar/initials + name + masked phone from /me), then grouped tappable rows — personal info, emergency contact, addresses, preferred language, dark mode, support/tickets, about/terms — ending with a sign-out row. This fixes the missing-logout hole, gives the 5th nav tab the weight users expect, and creates a natural home for future items (payment methods, saved nurses).
+- **Care-circle reframe with patient avatars** (impact: high, effort: medium) — Rename/reframe 'بیماران' toward a care-circle ('عزیزان شما' / حلقه مراقبت), add an Avatar slot to PatientHeader (photo upload or warm auto-colored initials per person), and let the card lead with the person, not the data. In a home-care product the emotional register of this list matters; today it reads like an admin table of patients.
+- **Structured per-item care-record editing** (impact: high, effort: large) — Replace whole-list edit mode with per-item bottom sheets: an 'add medication' sheet with structured dose unit, frequency presets (روزی ۱ بار…), and time-of-day chips (صبح/ظهر/شب); routine items with a time-of-day chip row instead of free text; a draft-loss guard when leaving edit mode or switching tabs. Then a read-mode 'daily schedule' view (meds grouped by morning/noon/night) becomes possible — genuinely useful to both family and arriving nurse.
+- **Full-screen mobile form flows** (impact: medium, effort: small) — Make the patient and address dialogs fullScreen below the sm breakpoint (MUI useMediaQuery pattern) with an app-bar-style header (title + close + save), and add a dirty-state confirm before discarding. Small change, removes the most-felt mobile papercut in the area.
+- **Visit-note timeline with booking context** (impact: medium, effort: medium) — Group سوابق notes by month with a subtle timeline rail, show the service name and a link back to the booking each note came from, and add a done/undone task summary per note. Turns the flat card list into the 'story of care' — a differentiating trust surface no competitor form-clone will have.
+- **Proper error states with retry** (impact: medium, effort: small) — Introduce a shared QueryErrorCard (icon + 'مشکلی پیش آمد' + retry button) and use it on patients/addresses/profile/record instead of collapsing to empty/blank states; distinguish offline from server error. Pairs with converting profile's AppLoading to a form-shaped skeleton for loading consistency.
+- **Emergency-contact card with call affordance** (impact: medium, effort: small) — Present the emergency contact as its own status card (complete = green check + tel: link; incomplete = warm nudge explaining why nurses need it) rather than two bare fields, and surface incompleteness as a checklist item during booking. Reinforces the safety story that is the product's core promise.
+- **Address card map thumbnail + pin quality cue** (impact: low, effort: medium) — Once real tiles exist, show a small static-map thumbnail on each AddressCard and a 'pin set / pin missing' status so families can see at a glance which addresses a nurse can actually navigate to.
+- **Record affordance on PatientCard** (impact: medium, effort: small) — Add a visible 'مشاهده پرونده' chevron row (or make the whole card a hover/press-styled CardActionArea with the action icons overlaid) so the record viewer is discoverable; include last-visit date on the card as a teaser ('آخرین ویزیت: ۱۲ تیر').
+
+## Keep (do not regress)
+
+- Token discipline is genuinely good across the whole area: every color is a --bal-* CSS variable or theme key (StatusChip.tsx even documents 'Never hard-code a hex here'), so dark mode works by construction — do not regress this.
+- AddressMapPicker's RTL engineering: dir="ltr" on the canvas plus inline-style marker positioning with an explicit comment on why the stylis RTL plugin would break `left`/`translate` — exemplary hazard handling; keep it when swapping in real tiles.
+- The non-leaking access-denied card and family-ownership banner on the care record (record/page.tsx lines 59-74, 108-116) — privacy-correct (no partial clinical data on 403) and a real trust cue; preserve verbatim in any redesign.
+- Consistent empty states on patients and addresses: dashed-border Paper, brand icon, warm two-line copy, CTA — the empty_body strings ('اولین آدرس را اضافه کنید تا پرستار بداند کجا بیاید') are the best copy in the app.
+- PatientHeader shared between the E1 list card and the E2 record viewer — one identity block, rendered identically in both places; keep this single source when adding avatars.
+- CascadingRegionSelect UX details: parent-gated enabling, per-level CircularProgress adornments, the explicit 'whole city' MenuItem (empty district as a real choice, never an error), and the out-of-range value guard for edit prefill.
+- Soft-archive semantics and copy for patients ('از فهرست شما حذف میشود اما سوابق رزروهای گذشته حفظ میماند') with an optimistic mutation plus an explanatory error toast when the card reappears — respectful and honest.
+- GenderToggle's never-defaulted, non-deselectable required gender — load-bearing for same-gender caregiver matching; keep the constraint regardless of restyling.
+- Skeleton loaders shaped like the content they replace on patients, addresses, and the record page (RecordSkeleton mirrors header/banner/tabs/card).
+- Inline single-field validation with error-clearing on change (name/phone/city/pin) and dedicated Persian error strings per field — the validation pattern itself is sound, only the surfaces around it need work.
diff --git a/dev/post-phase/ui/audit/customer-storefront.md b/dev/post-phase/ui/audit/customer-storefront.md
new file mode 100644
index 0000000..31dd42b
--- /dev/null
+++ b/dev/post-phase/ui/audit/customer-storefront.md
@@ -0,0 +1,77 @@
+# Customer storefront — home, search, results, nurse public profile
+
+## Current state
+
+The storefront is four client-rendered screens, all behind auth: CustomerHomePage (client/src/app/[locale]/(private-routes)/(customer)/page.tsx — greeting+avatar, free-text search bar, data-driven CategoryTile grid, two NudgeCard prompts), SearchPage/C1 (search/page.tsx — a vertical filter form: category grid, CascadingRegionSelect province/city/district, 3-way gender ToggleButtonGroup, native date input, debounced Toman price range, and a live-count CTA driven by useSearchFilters + useNurseSearch), SearchResultsPage/C2 (search/results/page.tsx — URL-is-the-filter-state list of NurseResultCard with load-more, one-option sort select, skeleton/empty/error states), and NurseProfilePage/C3 (search/nurse/[nurseId]/page.tsx — avatar header, rating, TrustBadge + INO chip, attribute Chips, MUI Tabs for services (ServicePriceRow list) and reviews (infinite published-review list with RatingInput stars), and a bottom "درخواست رزرو" AppButton). Chrome comes from CustomerLayout (client/src/layout/CustomerLayout.tsx): a fixed default-MUI AppBar (TopBar with a centered static title "اپلیکیشن خانواده", support icon, notification bell, dark-mode toggle) plus a 5-tab MUI BottomNavigation BottomBar, content constrained to 800px.
+
+Styling is disciplined but minimal: everything in this area resolves through the --bal-* CSS variables in client/src/theme/tokens.css (deep teal light + lifted-teal dark schemes, both defined), Mikhak is loaded for fa via next/font, MUI v9 CSS-vars theme with an RTL Emotion cache, and I found zero hard-coded hexes in the storefront pages or their components. Visually, however, it is a bare utility app, not a storefront: cards are 1px-border Papers with no elevation/warmth, the terracotta accent is used nowhere in the storefront (only checkout/BNPL screens use --bal-secondary), all icons are stock @mui/icons-material, the "logo" is the starter kit's multicolor cartoon-pencil SVG, and there is no hero, no value props, no how-it-works, and no public/guest-accessible page other than /login — the entire marketing face of the marketplace requires an account and a completed patient record to even see.
+
+## Problems (21)
+
+- **[high]** `client/src/app/[locale]/(public-routes)` — There is no public storefront at all. The only unauthenticated route is /login; home, search, results, and nurse profiles are all wrapped in RoleGuard(customer) via (customer)/layout.tsx, and the home page additionally redirects to onboarding until a patient record exists. A family evaluating the service cannot see a single nurse, price, or trust signal before registering — fatal for acquisition in a trust-first marketplace.
+ - evidence: (public-routes)/ contains only layout.tsx and login/page.tsx; (customer)/layout.tsx wraps children in
+- **[high]** `client/src/app/[locale]/(private-routes)/(customer)/page.tsx` — The home free-text search bar is a dead affordance: HomeSearchBar pushes `?q=` to /search, but SearchFilterScreen reads only `category_id` and silently discards `q`. The placeholder promises «جستجوی خدمت یا پرستار…» and the input does nothing.
+ - evidence: page.tsx L130 pushes `?q=`; search/page.tsx L49-50 reads only params.get('category_id')
+- **[high]** `client/src/app/[locale]/(private-routes)/(customer)/page.tsx` — If the patients query errors, the customer home hangs on a bare spinner forever — the gate `if (data == null || isEmpty) return ` has no isError/retry branch, so a transient API failure bricks the app's front door.
+ - evidence: L66-68: `if (data == null || isEmpty) { return ; }` — usePatients() error never handled
+- **[high]** `client/src/app/[locale]/(private-routes)/(customer)/search/page.tsx` — The visit-date filter is a native ` `, which renders a Gregorian calendar in browser chrome. The default locale is fa and every date the app displays is Shamsi (formatShamsiDate); Iranian families plan by the Jalali calendar, so this field is unusable-in-practice localization breakage on the main discovery flow.
+ - evidence: L104-110: ` `
+- **[high]** `client/src/components/NurseResultCard/NurseResultCard.tsx` — The result unit is the variant, but the card never names the service/variant — a nurse offering three variants appears as three near-identical cards (same avatar, name, rating) differing only in price, with no explanation. NurseSearchResult also carries no variant display name, so the card cannot label it even if it wanted to.
+ - evidence: Card renders only name/badge/rating/distance/price; services/search/types.ts NurseSearchResult has no variant displayName field
+- **[high]** `client/src/components/common/AppIcon/icons/PencilIcon.tsx` — The brand 'logo' icon is the starter kit's emoji-style multicolor pencil SVG with hard-coded fills (#EA596E, #FFCC4D, #D99E82…) that ignore the color prop; it is rendered as the BrandMark on the auth splash and the sidebar logo. A cartoon pencil as the mark of a healthcare-trust brand actively undermines the product.
+ - evidence: PencilIcon paths carry fill="#EA596E" etc.; AppIcon/config.ts maps `logo: PencilIcon`; components/auth/BrandMark.tsx renders
+- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/search/results/page.tsx` — The populated results state has no filter recap and no way to edit filters — the header is only a count plus a fake sort control; `backToFilters` is rendered exclusively inside the EmptyState, so users must use browser-back to change city/gender/price.
+ - evidence: L62-70 header renders count + sort only; backToFilters referenced only in EmptyState (L88, L131)
+- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/search/results/page.tsx` — The sort dropdown is a non-functional control: a TextField select with a single MenuItem, hard-coded value="rating", and no onChange — it looks interactive but does nothing, which erodes perceived quality.
+ - evidence: L67-69: `` with one and no onChange
+- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/search/nurse/[nurseId]/page.tsx` — The primary CTA «درخواست رزرو» sits at the very bottom of the page flow — after the full infinite reviews list when that tab is open — and is not sticky; and when no variant_id was carried it silently falls back to services[0] while the ServicePriceRow list offers no way to pick a specific service to book.
+ - evidence: L62-64: `const variantId = carriedVariant ?? String(profile.services[0]?.variantId ?? '')`; CTA is the last child of the page Stack (L92-101); ServicePriceRow has no onClick/select affordance
+- **[medium]** `client/src/services/search/types.ts` — Core trust data is fetched but never rendered: totalCompletedBookings and nurseGender exist on both NurseSearchResult and NurseProfile, yet neither the result card nor the profile shows completed-visit count or confirms the nurse's gender — exactly the cues a family choosing an in-home caregiver checks first.
+ - evidence: types.ts L67 `totalCompletedBookings`, L73/L117 `nurseGender` — no usage in NurseResultCard.tsx or nurse/[nurseId]/page.tsx
+- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/page.tsx` — The «تکمیل پروندهٔ بیمار» NudgeCard renders unconditionally forever — unlike the profile nudge (gated on hasCustomerProfile), there is no completeness check, so a customer with fully-filled patient records sees a permanent stale prompt occupying prime home real estate.
+ - evidence: L96-102 renders NudgeCard with no condition; L103 gates only the profile nudge
+- **[medium]** `client/src/components/RatingInput/RatingInput.tsx` — Rating stars are colored with --bal-warning, a token documented in tokens.css as a toast/alert *background* meant to sit under cream text. As a foreground fill it renders dark mustard (#8a6418) in light mode and muddy olive (#97701f, ~3.7:1) on the dark #0f1c19 background — ratings, a primary trust signal, look dim rather than gold in both schemes.
+ - evidence: RatingInput.tsx L53 and NurseResultCard.tsx L87 use `var(--bal-warning)`; tokens.css L50 comment: 'Feedback — toast / alert backgrounds'
+- **[medium]** `client/src/layout/components/TopBar.tsx` — The shell is stock starter chrome: a default-elevation fixed MUI AppBar with a nowrap centered Typography title, and CustomerLayout titles it with the system label «اپلیکیشن خانواده» instead of the brand «بالین یار» — the brand name never appears anywhere in the logged-in storefront.
+ - evidence: TopBar.tsx L25-38 (centered whiteSpace:'nowrap' title, commented-out boxShadow toggle); CustomerLayout.tsx L46 `title={tShell('customer_app')}`
+- **[medium]** `client/src/components/common/AppButton/AppButton.tsx` — Starter-grade AppButton defaults to `margin: 1` on all sides, so every storefront call site must fight it with `sx={{ m: 0 }}` (10+ occurrences across home/search/results/profile alone); passing any sx also silently drops the default, making spacing inconsistent by construction.
+ - evidence: AppButton.tsx L9-11 DEFAULT_SX_VALUES = { margin: 1 }; e.g. results/page.tsx L83/L100, nurse/[nurseId]/page.tsx L56/L98 all set `sx={{ m: 0 }}`
+- **[medium]** `client/src/components/common/AppIcon/config.ts` — The entire icon vocabulary is stock @mui/icons-material (ElderlyOutlined, ChildCareOutlined, VolunteerActivismOutlined…), giving category tiles and chrome the generic Material-dashboard look the brand explicitly wants to avoid; category icons are the emotional face of the home grid and read cold/clinical.
+ - evidence: config.ts L4-98: every icon imported from @mui/icons-material; KNOWN_CATEGORY_ICONS in CategoryTile.tsx resolve to these
+- **[medium]** `client/src/layout/components/BottomBar.tsx` — The bottom navigation has no iOS safe-area handling — no env(safe-area-inset-bottom) padding — so on notch/home-indicator phones (the primary device class for this mobile-first app) the 5 tab targets sit under the system gesture bar.
+ - evidence: L50-62: Paper + BottomNavigation with only `borderTop`, no safe-area padding
+- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/search/page.tsx` — The live-count CTA «مشاهده N پرستار» — the best affordance on the screen — is the last element of a long scrolling form instead of a sticky bottom bar, so the count is invisible while adjusting the upper filters where it would guide relaxation/tightening in real time.
+ - evidence: L130-140: AppButton rendered as final Stack child, no position:sticky
+- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/search/page.tsx` — The gender facet re-implements an inline ToggleButtonGroup instead of extending the shared GenderToggle component, leaving two divergent gender-control implementations (different padding/fontWeight, no error affordance) to keep visually in sync.
+ - evidence: search/page.tsx L86-100 inline group vs components/GenderToggle/GenderToggle.tsx
+- **[low]** `client/messages/fa.json` — The empty-results suggestion «شهر نزدیک دیگری مانند مشهد، اصفهان یا شیراز را امتحان کنید» hard-codes city names and suggests an impossible relaxation — the patient lives where they live; a family cannot 'try Shiraz'. Reads as filler copy and dents credibility.
+ - evidence: search.empty_suggest_city key
+- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/search/nurse/[nurseId]/page.tsx` — The verified badge is a static chip everywhere it appears — nothing lets a customer discover *what* was verified (identity, license, INO, background check), even though the verification pipeline is the product's core differentiator; on results it is also repeated identically on every card (all rows are verified by invariant), so it stops carrying information.
+ - evidence: TrustBadge rendered with no onClick/link (page.tsx L141, NurseResultCard.tsx L82); TrustBadge.tsx has no interactive affordance
+- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/search/results/page.tsx` — First paint of both search screens under Suspense is a bare centered CircularProgress (AppLoading) rather than the page's own skeleton layout, causing a spinner→skeleton→content double transition.
+ - evidence: results/page.tsx L22 and search/page.tsx L35 ` }>` while in-page skeletons exist at L72-77
+
+## Opportunities (10)
+
+- **Public landing + guest browse (the real storefront)** (impact: high, effort: large) — Create a public marketing home under (public-routes): hero with the existing tagline «مراقبت مطمئن در خانه», the category grid as entry points, a 3-step how-it-works (search verified nurses → escrow-protected payment → pay-out only after confirmed check-out), a verification-pipeline trust strip, and sample verified-nurse cards. Let guests run search and view nurse profiles read-only, deferring the login gate to the «درخواست رزرو» tap (intent is highest there). Nearly all pieces already exist as components — this is mostly routing + a hero section, and it is the single biggest acquisition lever the product has.
+- **Nurse profile as a trust dossier** (impact: high, effort: medium) — Redesign C3 around the question 'would I let this person into my mother's home?': a header card with photo, gender chip, years of experience, completed-visits count (data already fetched), and rating; an expandable 'verification checklist' section listing each passed check (identity ✓, nursing license ✓, INO membership ✓) fed by the badge state instead of one flat chip; a rating-distribution bar + review tag summary above the review list; per-service rows that are tappable to book that exact variant; and a sticky bottom CTA bar showing price-from + «درخواست رزرو». This screen is where the marketplace either earns or loses the booking.
+- **Jalali date selection** (impact: high, effort: small) — Replace the native Gregorian type="date" with a Shamsi-native control: a horizontal chip strip («امروز», «فردا», day+Shamsi-date chips for the next 7 days) plus an optional full Jalali picker. Small build (the Shamsi format util already exists) and removes the most jarring localization break in the funnel.
+- **Results page upgrade: recap chips, variant labels, honest sorting** (impact: high, effort: medium) — Add a tappable filter-recap chip row (category · city/district · gender · price) that deep-links back to C1 with state preserved; show the service/variant name on each card (requires adding displayName to the search row — already filed as a backend gap pattern); collapse multiple variants of one nurse into a single card with a price range and 'N خدمت' disclosure; and either implement price/distance sort or remove the single-option dropdown. Also surface totalCompletedBookings («۱۲۴ ویزیت موفق») on cards — a stronger differentiator than the uniform verified chip.
+- **Warmth pass: brand mark, custom icons, terracotta accents** (impact: high, effort: medium) — Replace the pencil logo with a real Balinyaar mark and put the brand (not «اپلیکیشن خانواده») in the top bar; commission/adopt a single warm stroke-icon set for the 5-6 category icons and bottom-nav tabs; introduce the terracotta accent sparingly in the storefront — e.g. the home greeting card background tint, the star rating fill (a proper warm gold/terracotta token instead of --bal-warning), and the profile CTA. Today the storefront is monochrome teal borders on white and reads like an internal tool.
+- **Sticky conversion bars** (impact: medium, effort: small) — Make the C1 live-count CTA and the C3 booking CTA sticky bottom bars (above the BottomBar, with safe-area padding). The live count becomes a real-time feedback instrument while filtering; the profile CTA stays reachable regardless of review-list depth. One shared SlotBar component covers both.
+- **Home that sells and remembers** (impact: medium, effort: medium) — Wire the free-text search to actually filter (variant/category name match) or remove the field; add a compact trust strip (پرداخت امن اسکرو · پرستاران تاییدشده · پشتیبانی ۲۴ ساعته); add a 'rebook' shortcut card sourced from the bookings cache («رزرو دوباره با خانم …» — repeat care is the dominant pattern in home nursing); and make nudges completeness-aware and dismissible.
+- **Tappable verification explainer** (impact: medium, effort: small) — Make every TrustBadge open a bottom sheet that narrates the verification pipeline ('این پرستار این مراحل را گذرانده است: …' with check dates). Turns a static chip into the product's trust story at exactly the moment of doubt, at trivial cost.
+- **Save/favorite and share nurses** (impact: medium, effort: medium) — Add a heart action on result cards and the profile plus a share-profile link. Families deliberate and compare with relatives before booking a caregiver; today there is no way to shortlist or send a profile to a sibling, forcing screenshot workflows.
+- **Nurse photo emphasis and richer media** (impact: medium, effort: large) — Today avatars are initial-letter fallbacks in teal discs. Prioritize real photos in the verification flow and give them prominence (larger profile photo, photo-forward result cards); longer-term, a short recorded self-introduction. In a market with low institutional trust, seeing the person is the strongest trust cue available.
+
+## Keep (do not regress)
+
+- Token discipline: zero hard-coded hexes in storefront pages/components — every color resolves through the --bal-* variables in client/src/theme/tokens.css, with complete light and dark scheme definitions, so a restyle can happen at the token layer.
+- All four data states are consistently implemented everywhere: skeleton grids sized to match real tiles (home/C1), error states with working retry, product-aware empty states, and a dedicated ProfileSkeleton on C3 — no blank screens.
+- RTL correctness: no physical marginLeft/left/textAlign:'left' anywhere in the audited area; logical properties used where needed (marginInlineStart:'auto' in ReviewCard), paired LTR/RTL themes with stylis-plugin-rtl, and Mikhak loaded for fa with full-body coverage.
+- Search architecture: the filter object is the URL is the React Query cache key (deep-linkable, back/forward-safe results), the C1 CTA shows a live result count, price inputs are debounced, and 'empty district = whole city' is an explicit, honestly-labeled choice in CascadingRegionSelect.
+- Trust honesty by construction: the C1 subtitle states only verified nurses appear, TrustBadge has three visually distinct states (verified/unverified/expired) driven by server state and semantic tokens, and the UI never re-derives or fakes verification.
+- Money handling: PriceDisplay renders exclusively through the IRR→Toman money util with fa digit grouping, unit labels from i18n keys, and totals only ever computed as price × sessionCount (BigInt-safe) — never a parsed float.
+- Accessibility groundwork: CategoryTile is a real ButtonBase with aria-pressed, NurseResultCard has Enter/Space key handling and a focus-visible outline, RatingInput exposes radiogroup/radio semantics, and the home search form uses role='search'.
+- The gender facet is treated with the care the domain demands: prominent placement, humane hint copy explaining same-gender preference, never defaulted, and carried explicitly into the booking request.
+- Persian copy quality: the fa strings are natural, specific, and warm (e.g. the patient-record nudge «…تا پرستار آماده حاضر شود») — not machine-translated placeholder text.
diff --git a/dev/post-phase/ui/audit/feature-components.md b/dev/post-phase/ui/audit/feature-components.md
new file mode 100644
index 0000000..1a66f5d
--- /dev/null
+++ b/dev/post-phase/ui/audit/feature-components.md
@@ -0,0 +1,64 @@
+# Shared feature components (client/src/components/* excluding common/, admin/, auth/, booking/, geography/, messaging/, notifications/)
+
+## Current state
+
+This layer is ~34 single-purpose presentational components, one folder each (component + index barrel + test), re-exported from client/src/components/index.tsx. Authorship is remarkably uniform: typed FunctionComponent, a JSDoc header explaining the domain rule the component encodes, caller-owned or namespace-scoped i18n (never hard-coded strings, except one component), data-* attributes for tests, and display-only money/dates through shared utils (formatIrrToToman/parseIrr BigInt, formatShamsiDate). The visual language that exists is: flat Paper (elevation={0}, 1px 'divider' border, borderRadius 2), a borderInlineStart accent stripe keyed to a semantic token for stateful panels (BankStatusPanel, EarningsBalanceHeader, PayoutHistoryRow failure, DocumentUpload error/rejected), and MUI Chip-based badges (StatusChip, TrustBadge, PaymentStatusBadge) whose colors come exclusively from the --bal-* CSS custom properties in src/theme/tokens.css (both light and dark schemes defined). RTL is handled with logical properties (borderInlineStart, marginInlineStart:'auto', textAlign 'start'/'end') plus deliberate dir="ltr" islands for IBANs, phone numbers, OTP boxes, transfer references, and the countdown clock.
+
+Contrary to the "no design pass" expectation, this layer is NOT starter-grade — it was clearly written during the f0–f15 feature phases with real domain intent (honest refund/BNPL copy, escrow trust notice, negative-balance "owed back" framing, no-delete variant rows). The one true starter fossil is UserInfo (any-typed props, English 'Current User'/'Loading...' fallbacks, rendered in the sidebar for every logged-in user). The real weaknesses are systemic rather than per-component: (1) the card/row/badge anatomy is repeated by convention, not shared — the same Paper recipe is hand-rolled ~12 times with padding drifting across p:1.5/2/2.5/3 and accent stripes at 3px vs 4px, and the label/value row is re-implemented three times (SummaryRow in BookingRequestSummaryCard, MetaLine in PayoutHistoryRow, inline rows in PriceBreakdown); (2) the trust-critical surfaces are the flattest — TrustBadge is pixel-identical in anatomy and color to a generic StatusChip 'verified', rating stars are colored with the dark-ochre alert token over a near-invisible 14%-alpha empty state, and NurseResultCard carries only name/rating/distance/price; (3) terracotta (--bal-secondary #d98c6a) has quietly become the default "money text" color across six components and fails WCAG contrast on white; (4) selection states speak five different visual dialects (filled chip vs border-only vs border+tint vs terracotta vs default ToggleButton); and (5) everything composes stock MUI Material icons and raw MUI Stepper, so despite the token discipline the rendered result still reads default-MUI. Theme.ts has no component-level overrides, so any anatomy not written in sx falls back to MUI defaults.
+
+## Problems (13)
+
+- **[high]** `client/src/components/UserInfo/UserInfo.tsx` — Untouched starter scaffolding shipped in the authenticated sidebar: `user?: any` prop, hard-coded English fallbacks 'Current User' and 'Loading...' in a fa-default product, email-based fallback display in a phone-OTP product, and a 3rem glyph inside a 64px avatar. Violates the repo's own 'no starter scaffolding / no dead template code' rule and is the first thing every logged-in user sees (rendered by src/layout/components/SideBar.tsx:56).
+ - evidence: lines 6 (`user?: any`), 34 (`{fullName || 'Current User'}`), 36 (`{userPhoneOrEmail || 'Loading...'}`)
+- **[high]** `client/src/components/PriceBreakdown/PriceBreakdown.tsx` — Terracotta used as small-text color fails WCAG contrast in light mode on the most trust-critical numbers. `--bal-secondary` #d98c6a on white ≈ 2.7:1 (AA needs 4.5:1 at these sizes) is the grand-total color here and also in RefundStatusCard.tsx:89 (refunded amount), BnplPlanCard.tsx:68 (monthly amount), InstallmentScheduleRow.tsx:55 (down payment), CancellationPolicyDisclosure.tsx:55 (fee line); `--bal-secondary-dark` #bf6f4d caption in BnplPlanCard.tsx:62 ≈ 3.8:1 also fails. This simultaneously breaks the 'terracotta as a SINGLE sparing accent' brand rule — it is now the default money color across 6+ components.
+ - evidence: line 62: `sx={{ fontWeight: 800, color: 'var(--bal-secondary)' }}` on the total amount
+- **[high]** `client/src/components/RatingInput/RatingInput.tsx` — The trust-critical star rating renders badly three ways: filled stars use `var(--bal-warning)` — a dark-ochre alert-background token (#8a6418 light / #97701f dark) — so 'gold' stars read muddy brown; empty stars use `var(--bal-divider)` (a 14%-alpha rgba) and are near-invisible on white paper and dark surfaces alike; and fill is integer-only (`n <= value`), so fractional averages can't render — the nurse profile works around it with Math.round, displaying a 4.5 average as a perfect 5-star row (search/nurse/[nurseId]/page.tsx:260), overstating ratings on the platform's core trust surface. NurseResultCard.tsx:87 and BookingRequestSummaryCard.tsx:86 use the same ochre star.
+ - evidence: line 53: `const color = n <= value ? 'var(--bal-warning)' : 'var(--bal-divider)';`
+- **[medium]** `client/src/components/TrustBadge/TrustBadge.tsx` — The platform's core trust mark has no distinct visual identity: it is anatomically identical to StatusChip (same small filled MUI Chip, same 16px icon, same `--bal-success` background that StatusChip uses for generic 'active'/'verified' states — compare StatusChip.tsx:16-17). A verified-identity healthcare credential and an 'active service variant' chip are indistinguishable at a glance, and there is no affordance to see WHAT was verified (identity, license, background check).
+ - evidence: lines 18 + 39-46: verified = `{ bg: 'var(--bal-success)', ... }` rendered as a plain ``
+- **[medium]** `client/src/components/NurseResultCard/NurseResultCard.tsx` — The search decision point is information-thin for a trust-first marketplace: no service/variant name (each result row IS a bookable variant), no nurse gender indicator (same-gender matching is a load-bearing product rule carried in the query), no experience/completed-bookings count, no review snippet — just avatar, name, one badge, rating, optional distance, and price. Families are choosing an in-home caregiver off four data points.
+ - evidence: lines 77-112 render only name + TrustBadge + rating/count + distance + price_from
+- **[medium]** `client/src/components/RelationSelect/RelationSelect.tsx` — Selection states are inconsistent across the five choice controls in this layer, and this one is the weakest: selected relation cards get only a 2px `primary.main` border — no background tint, no check glyph, no hover/pressed feedback — while ConditionChips/ReviewTagSelector use a filled primary chip, CategoryTile uses border+`--bal-primary-soft` tint, BnplPlanCard uses 2px terracotta border+tint, and GenderToggle falls back to the default gray MUI ToggleButton selected state. The role="radio" group also lacks roving tabindex/arrow-key navigation (every card is tabIndex={0}).
+ - evidence: lines 53-55: `border: '2px solid', borderColor: selected ? 'primary.main' : 'divider'` is the entire selected treatment
+- **[medium]** `client/src/components/StepperHeader/StepperHeader.tsx` — A bare default-MUI Stepper wrap (numbered circles, default connector — theme.ts defines no component overrides), doing double duty as wizard progress (onboarding/verification) AND as a refund status timeline inside RefundStatusCard.tsx:76-79. A status tracker rendered as a form-wizard control, with no timestamps and the stock MUI look the owner is trying to escape.
+ - evidence: lines 20-28: raw `` with zero styling
+- **[medium]** `client/src/components/OtpInput/OtpInput.tsx` — Missing `autoComplete="one-time-code"` on the digit inputs, so iOS/Android SMS code autofill never triggers — on the product's ONLY login path, in a market where OTP login is the norm. (PhoneNumberField.tsx similarly omits `autoComplete="tel"`.) The multi-box pattern itself also fights autofill; a single hidden input with visual boxes would receive the OS-suggested code.
+ - evidence: lines 121-128: `htmlInput: { inputMode: 'numeric', maxLength: 1, ... }` — no autoComplete
+- **[medium]** `client/src/components/EarningsRow/EarningsRow.tsx` — Shared card anatomy exists only by convention: the `Paper elevation={0} / 1px divider / borderRadius 2` recipe is hand-rolled here and in ~11 sibling components with drifting padding (p:1.5 InstallmentScheduleRow, p:2 PatientCard/VariantCard/VisitNoteCard, p:2.5 here/PriceBreakdown/PayoutHistoryRow, p:3 EarningsBalanceHeader) and accent stripes at 3px (EarningsBalanceHeader.tsx:97, PayoutHistoryRow.tsx:91) vs 4px (BankStatusPanel.tsx:67, EarningsBalanceHeader.tsx:55, DocumentUpload.tsx:164); the label/value row is re-implemented three times (SummaryRow, MetaLine, PriceBreakdown rows). No shared Card/Row primitive means every future restyle is a 12-file change and drift is inevitable.
+ - evidence: line 57: `sx={{ p: 2.5, border: '1px solid', borderColor: 'divider', borderRadius: 2 }}` — the 12th copy of this literal
+- **[low]** `client/src/components/BnplPlanCard/BnplPlanCard.tsx` — Selecting a plan changes borderWidth 1→2, shifting the card contents by 1px (RelationSelect avoids this with a constant 2px border); and the card shows only monthly amount + fee% + down-payment% with no total-cost-of-credit line, so plans with different terms are not honestly comparable.
+ - evidence: lines 49-50: `borderColor: selected ? ... , borderWidth: selected ? 2 : 1`
+- **[low]** `client/src/components/CountdownTimer/CountdownTimer.tsx` — Uses the 'pending' hourglass glyph as a clock (a 'schedule' clock icon exists in the registry), hard-codes fontSize '1.5rem' outside the type scale, and 'urgent' mode only swaps teal→terracotta — no progress ring/bar or intensifying treatment for the payment-deadline window it was built for.
+ - evidence: lines 66 + 100-104: `urgent ? 'var(--bal-secondary)' : ...` and `` beside `fontSize: '1.5rem'`
+- **[low]** `client/src/components/EarningsRow/EarningsRow.tsx` — The negative commission row is fed through the generic PriceBreakdown with no deduction treatment — same weight/color as positive rows, relying entirely on Intl fa-IR minus-sign placement inside an RTL paragraph; a deduction should read as one (parentheses, muted/error tone, or an explicit 'کسر' prefix).
+ - evidence: lines 48-51: `String(-parseIrr(item.balinyaarCommissionIrr))` passed as a plain PriceBreakdown row
+- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/search/results/page.tsx` — Loading skeletons are generic rectangles that don't match the card anatomy they replace (bare `Skeleton height={112}` for NurseResultCard rows) — a pattern repeated across pages because no card in this layer ships a skeleton twin; content jumps when avatars/chips/price rows pop in.
+ - evidence: line 75: ` `
+
+## Opportunities (12)
+
+- **Extract the shared card/row/badge kit the layer already implies** (impact: high, effort: medium) — Codify the de-facto anatomy into 4-5 primitives and migrate the ~12 hand-rollers: `SurfaceCard` (flat Paper, one padding scale sm/md/lg, radius token), `AccentCard` (SurfaceCard + semantic borderInlineStart stripe at one width), `LabelValueRow` (replaces SummaryRow/MetaLine/PriceBreakdown rows, with an ltr-value option for refs/IBANs), `SelectableCard` (one selected language: constant border + soft tint + check glyph, used by RelationSelect/CategoryTile/BnplPlanCard/GenderToggle), and `MoneyText` (amount + currency + optional deduction styling, correct contrast). Every future brand pass then touches one file per primitive instead of twelve.
+- **Design a real trust system around TrustBadge** (impact: high, effort: medium) — Trust is the product: give the verified mark a proprietary shape (e.g. a teal shield/seal distinct from status chips), and make it expandable — tapping opens a bottom sheet listing what Balinyaar verified (identity ✓, nursing license ✓, background check ✓, IBAN ownership ✓) with dates, fed by the existing verification-status query. Reuse the same sheet on NurseResultCard, the nurse profile, and booking summary. This converts an ambient chip into an explorable trust artifact — the single highest-leverage design move available.
+- **NurseResultCard v2 — the decision card** (impact: high, effort: medium) — Add the variant/service name (the row is a variant), a gender indicator (load-bearing for matching), completed-visits count, and a one-line top review tag (e.g. «منظم و دقیق» from the review vocabulary); make the avatar larger and photo-forward with the trust seal overlapping it. Ship a `NurseResultCardSkeleton` twin. Consider a compact/comfortable density prop so the same card serves list and map views later.
+- **Dedicated rating tokens + fractional stars** (impact: high, effort: small) — Add `--bal-rating` / `--bal-rating-empty` token pairs (a warm amber-gold with a visible empty outline in both schemes) and give RatingInput fractional fill (clip-path or dual-layer) so a 4.5 average renders honestly instead of being rounded to 5. Swap NurseResultCard/BookingRequestSummaryCard star colors to the same token. Small change, visible on every trust surface.
+- **Retire terracotta as the money color** (impact: high, effort: small) — Define a `--bal-money-emphasis` token (ink/teal-dark in light, lifted cream in dark) for totals and amounts, reserving terracotta for the one primary financial CTA per screen (pay button, selected BNPL plan) per the brand's 'single sparing accent' rule. Fixes the contrast failures in PriceBreakdown/RefundStatusCard/BnplPlanCard/InstallmentScheduleRow/CancellationPolicyDisclosure in one pass.
+- **Swap the icon registry to a coherent humane set** (impact: medium, effort: medium) — All glyphs are stock MUI Material icons, which is a big contributor to the default-MUI feel. Because AppIcon centralizes the registry (common/AppIcon/config.ts), replacing Material with a single warm stroke set (Lucide/Phosphor/Solar, 1.5-2px stroke, rounded caps) is a one-file change that instantly re-skins all 34 feature components — including replacing the leftover Twemoji PencilIcon 'logo'.
+- **StatusTimeline component for refunds (and future booking states)** (impact: medium, effort: small) — Replace StepperHeader inside RefundStatusCard with a purpose-built vertical timeline: step label + timestamp + channel note per node, teal completed nodes, animated 'in transit' node. Reusable for booking lifecycle and verification progress; leaves StepperHeader to actual wizards.
+- **Answer 'when do I get paid?' in EarningsBalanceHeader** (impact: medium, effort: small) — The header shows four buckets but not the one thing nurses actually ask: the next weekly payout date (server-derivable from the batch schedule + holiday shift). Add a 'برداشت بعدی' line with the Shamsi date under the net balance, and optionally a mini trend of recent payouts.
+- **OTP autofill + single-input architecture** (impact: medium, effort: small) — Add autoComplete="one-time-code" now (one line), then refactor OtpInput to one hidden input driving visual digit boxes so OS keyboard code-suggestion works reliably; add autoComplete="tel" to PhoneNumberField. Directly reduces login friction for every user.
+- **Skeleton twins co-located with cards** (impact: medium, effort: small) — Ship ` ` statics for NurseResultCard, EarningsRow, PayoutHistoryRow, PatientCard, VisitNoteCard matching their exact anatomy (avatar disc, chip row, price line), so pages stop hand-rolling height-guessed rectangles and loading feels designed.
+- **Total-cost honesty line on BnplPlanCard** (impact: medium, effort: small) — Add a served 'total you will pay' line (down payment + n×installment) so interest-free vs fee-bearing plans are comparable at a glance — an honest-lending pattern consistent with the platform's existing BNPL-honesty rules (RefundEtaBanner already models this tone).
+- **Rewrite or delete UserInfo** (impact: medium, effort: small) — Replace the starter UserInfo with a branded profile block: initials avatar off --bal-primary-soft (matching NurseResultCard/BookingRequestSummaryCard), i18n'd fallbacks, masked phone via the existing maskIranMobile util, and the user's role chip — or fold it into the redesigned sidebar entirely.
+
+## Keep (do not regress)
+
+- Token discipline is exemplary and must not regress: zero hard-coded hexes across all 34 feature components (verified by grep — only the starter PencilIcon in common/ has literals); every color resolves from --bal-* semantic tokens defined for both light and dark schemes, with in-code comments enforcing the rule (StatusChip.tsx:13-14, TrustBadge.tsx:14-16).
+- RTL discipline: logical properties throughout (borderInlineStart accent stripes, marginInlineStart:'auto' in VisitNoteCard, textAlign 'start'/'end') plus deliberate dir="ltr" islands for IBANs (BankStatusPanel:96, PayoutHistoryRow:71-79), phone/OTP digits, transfer references, and the HH:MM:SS countdown clock (CountdownTimer:103) — a grep for marginLeft/textAlign:'left' finds nothing.
+- Money invariants: all amounts are served IRR digit-strings formatted through the BigInt-safe money util; PriceBreakdown's dev-mode reconciliation guard (rows must sum to the total or console.error) catches upstream data bugs; no component ever computes money except the sanctioned price×sessionCount estimate in PriceDisplay.
+- Honest trust copy encoded as components: EscrowNotice's product-mandated verbatim fa escrow message, RefundEtaBanner's BNPL ~7-10-business-day honesty, RefundStatusCard suppressing ALL success framing (progress/amount/ETA) on failed refunds, EarningsBalanceHeader's explicit 'owed back' state instead of a bare minus sign — these are design decisions, not accidents; any restyle must preserve the copy and the state logic.
+- DocumentUpload's complete state machine — idle → uploading (progress %) → success (✓ + preview) → error (retry) → rejected (reason + re-upload, never a dead end) — with client-side type/size validation, object-URL cleanup, and server-metadata as the only 'uploaded' truth.
+- CountdownTimer's isolation architecture: self-owned 1-second tick so only it re-renders, server-frozen deadline (never recomputed client-side), single onElapsed fire, tabular-nums digits.
+- Exhaustive typed enum→chip mappings (PaymentStatusBadge, EarningsRow, PayoutHistoryRow, InstallmentScheduleRow): a wire-enum change fails the build instead of rendering an unmapped status — keep this pattern in any badge redesign.
+- StatusChip as the single source of status color+icon that BankStatusPanel, VariantCard, PaymentStatusBadge, EarningsRow, PayoutHistoryRow, RefundStatusCard, CancellationPolicyDisclosure all delegate to — the consolidation point already exists; redesign the one component, not seven.
+- Accessibility groundwork: keyboard handlers + role=button on tappable cards (NurseResultCard, DocumentUpload dropzone), radiogroup/radio semantics (RatingInput, RelationSelect), aria-pressed on toggle chips, focus-visible outline on NurseResultCard, and data-* hooks on every component for tests.
+- Presentational purity with caller-owned i18n (labels are i18n keys off stable codes, never derived from wire values) — this is precisely what makes a ground-up visual redesign cheap and low-risk; don't let a restyle introduce data-fetching or hard-coded strings into this layer.
diff --git a/dev/post-phase/ui/audit/messaging-notifications.md b/dev/post-phase/ui/audit/messaging-notifications.md
new file mode 100644
index 0000000..5bf2db5
--- /dev/null
+++ b/dev/post-phase/ui/audit/messaging-notifications.md
@@ -0,0 +1,67 @@
+# Notifications, messaging/tickets, and content surfaces across actors
+
+## Current state
+
+Notifications are a small, well-factored system: `NotificationBell` (container, polled `useUnreadCount` at 60s interval, auth-gated) + `NotificationBellView` (Badge over `AppIcon icon="notifications"`, tokenized error-red badge) mounted in both `CustomerLayout` (top bar end) and `NurseLayout` (headerActions). The bell navigates to a full-page `NotificationCenter` (`client/src/components/notifications/NotificationCenter.tsx`), shared by `(customer)/notifications/page.tsx` and `nurse/notifications/page.tsx`, which renders an unread-first flat list of `NotificationRow` cards (ButtonBase, unread = dot + fontWeight 800 + `--bal-primary-soft` tint), mark-read-on-open (optimistic), mark-all-read, load-more by growing a `limit`, and skeleton/empty/error-retry states. Deep links are centralized and role-aware in `services/notifications/deepLink.ts`; icons per kind in `notificationIcon.ts`. Admin has no notification center — `admin/notifications/page.tsx` is a `PlaceholderScreen`.
+
+Messaging is ticket-based (the only sanctioned channel, by product design): `TicketInboxScreen` (title + "contact support" CTA + always-on `EmergencyBanner` + flat `TicketListCard` list) shared by customer `/support/tickets` and `/nurse/support/tickets`; `TicketThreadScreen` (`referenceCode` header card + `TicketMessageList` bubbles + sticky `MessageComposer`) shared by the two `[id]` pages. `MessageBubble` does mine/theirs alignment with logical corner radii (RTL-mirrors automatically), teal fill for mine, paper+border for theirs, author label only on theirs, full Shamsi date-time per bubble. Sends are genuinely optimistic (`usePostMessage`): clientMessageId reconciliation, draft cleared only on server confirm, pending bubble at 0.75 opacity with a "sending…" label, rollback + caption error on failure; the composer is remount-keyed per ticket so drafts never cross threads. `ContactSupportDialog` handles category/subject/body then shows the created `referenceCode` with a "view thread" action; `BookingSupportEntry` hangs off booking detail and shows the nurse-only post-confirmation `EmergencyBanner` with the sole sanctioned `tel:` click-to-call. The admin thread (`admin/tickets/[id]/page.tsx`) is a separate build: a 520px scrollbox of `AdminMessageBubble` (internal notes = dashed warning border + badge), a reply/internal ToggleButtonGroup composer, and an inline collapsible `RefundPanel`. Everything is styled from `--bal-*` CSS tokens (both schemes), all icons are stock MUI Material icons via the `AppIcon` registry, all list surfaces have loading/empty/error states, and copy in `messages/fa.json` is human and decent. Critically, `USE_TICKETS_MOCK = false` (real API) while `unreadCount`/`lastMessageAt` on `TicketSummary` are documented as mock-only (REQ-028), and the thread query has no refetch interval — so several "messaging" behaviors exist only under the mock.
+
+## Problems (16)
+
+- **[high]** `client/src/components/messaging/TicketInboxScreen.tsx` — The inbox has no pagination and no status/category filter: `useMyTickets({})` pins page 1 / pageSize 20 with no load-more UI, so any ticket beyond the first 20 is unreachable. This is not theoretical — a coordination ticket is auto-created for every confirmed booking, so an active nurse's inbox outgrows one page quickly. The hook already supports `status` and `page`; the screen just never exposes them.
+ - evidence: line 35: `const { data, isLoading, isError, refetch } = useMyTickets({});` — no setLimit/setPage anywhere, unlike NotificationCenter's load-more
+- **[high]** `client/src/components/messaging/TicketListCard.tsx` — On the real API (USE_TICKETS_MOCK=false in services/tickets/constants.ts:15) the inbox's core messaging affordances are dead: `unreadCount` and `lastMessageAt` are mock-only (REQ-028 gap, types.ts:56-59), so no unread pill, no bold-unread subject, and times silently fall back to `createdAt`. There is also no last-message preview snippet at all (not even in the type). Users cannot tell which ticket has new support/nurse activity — the single most important signal of an inbox.
+ - evidence: types.ts:56-59 "REQ-028 gap — mock-only until delivered"; TicketListCard.tsx:44 `(ticket.unreadCount ?? 0) > 0`
+- **[high]** `client/src/services/tickets/hooks/useTicket.ts` — The thread never live-updates: `useTicket`/`useTicketThread` have staleTime 15s but no `refetchInterval`, so a support or nurse reply that arrives while the user is sitting on the thread never appears until they blur/refocus the window or navigate away. For the platform's only communication channel this is below messaging baseline — the user sends a message and stares at a screen that will not answer.
+ - evidence: useTicket.ts:15-21 — queryKey/staleTime/gcTime only, no refetchInterval (contrast useUnreadCount.ts:21 which polls)
+- **[high]** `client/src/components/messaging/TicketMessageList.tsx` — No scroll management anywhere in the thread: messages render as a plain flow list with no scroll-to-newest on open or on send/receive, so a long thread opens at the top (oldest message) and the user must manually scroll down past the history every visit. The admin thread has the inverse bug — a `maxHeight: 520, overflowY: 'auto'` box that also opens scrolled to the top, hiding the newest messages below the fold inside the scrollbox.
+ - evidence: TicketMessageList.tsx:51-63 — no ref/useEffect/scrollIntoView; admin/tickets/[id]/page.tsx:150
+- **[medium]** `client/src/components/messaging/MessageBubble.tsx` — The bubble timestamp forces `direction: 'ltr'` on a Persian Shamsi string (formatShamsiDateTime for fa yields e.g. "۲۵ تیر ۱۴۰۵، ۱۴:۳۰" with a Persian month name). Forcing an RTL-language string into an LTR paragraph makes bidi reorder its segments visually (time/date tokens swap around the comma). `direction:'ltr'` is correct for the Latin referenceCode but wrong here.
+ - evidence: MessageBubble.tsx:71 `direction: 'ltr'` on the timeLabel Typography
+- **[medium]** `client/src/components/common/AppIcon/config.ts` — The `send` icon (SendOutlined paper plane) is never mirrored for RTL. The theme's stylis-plugin-rtl mirrors generated CSS, not SVG glyphs, so in the default fa layout the send arrow points right — back into the text field instead of toward the inline send direction. Material's own RTL guidance lists Send among icons that must be mirrored.
+ - evidence: config.ts:85 `import SendIcon from '@mui/icons-material/SendOutlined'`; no scaleX(-1)/rtl transform anywhere in client/src (grep confirmed)
+- **[medium]** `client/src/components/messaging/MessageComposer.tsx` — Enter always sends — including on mobile touch keyboards, where Enter is how users write a multi-line message. The customer shell is explicitly mobile-first; standard chat behavior is Enter=send on desktop only, and a send button tap on touch. There is also no way to insert a newline on mobile at all (Shift+Enter doesn't exist on touch keyboards).
+ - evidence: MessageComposer.tsx:46-51 `if (event.key === 'Enter' && !event.shiftKey) { … submit(); }` with no pointer/viewport check
+- **[medium]** `client/src/components/messaging/TicketInboxScreen.tsx` — The alarm-red EmergencyBanner renders permanently at the top of every ticket inbox with no `contactPhone` (the inbox never has one — the tel: contact only exists on the nurse's post-confirmation booking read). The copy tells users to "first call the emergency contact" on a surface that cannot show any number, which is confusing, and a full error-colored banner as permanent inbox chrome produces alarm fatigue and pushes the actual ticket list below the fold on mobile.
+ - evidence: line 57 ` ` — contactName/contactPhone never passed; EmergencyBanner.tsx:58 renders the call button only `phone ? …`
+- **[medium]** `client/src/components/notifications/NotificationRow.tsx` — Navigable and non-navigable notifications are visually identical ButtonBase cards: a `kind:'none'` row (deepLink returns null) still ripples on click and silently only marks itself read — the tap appears to do nothing. No chevron/affordance distinguishes rows that open something, and the ButtonBase has no hover state and no visible :focus-visible style, so keyboard users get no focus indication on either notifications or ticket cards (TicketListCard has the same construction).
+ - evidence: NotificationRow.tsx:30-43 sx has static bgcolor/border only; NotificationCenter.tsx:46-50 `if (target) router.push(…)` with no UI differentiation
+- **[medium]** `client/src/components/notifications/NotificationBell.tsx` — The bell is navigation-only: clicking it always route-pushes to the full notifications page, even on the nurse/admin desktop shells where the expected pattern is a popover preview (recent items + mark-all + "view all"). Full-page context switch for glancing at notifications is heavy on desktop; there is also no visual acknowledgment (animation/pulse) when the polled count increments.
+ - evidence: NotificationBell.tsx:31 `onClick={() => router.push(`/${locale}${notificationsPath(role)}`)}`
+- **[medium]** `client/src/components/messaging/TicketMessageList.tsx` — The thread has zero chat typography structure: every bubble carries a full "day longMonth year, hh:mm" Shamsi timestamp (huge for chat), there are no date separators, no grouping of consecutive messages from the same author, and `system` messages (author_system = "سیستم") render as ordinary left-side bubbles instead of centered event lines — so an auto-created coordination thread reads as a raw list, not a conversation.
+ - evidence: TicketMessageList.tsx:58 `timeLabel={formatShamsiDateTime(message.createdAt, locale)}` for every message; no separator logic in the map
+- **[medium]** `client/src/layout/CustomerLayout.tsx` — New support activity is invisible in the app chrome: the top-bar support entry is a bare AppIconButton with no unread badge (unlike the notification bell), and the 5-tab BottomBar has no support presence at all — so a customer with an unanswered support reply sees nothing anywhere unless a notification also fires. Partly blocked by REQ-028, but the affordance isn't even scaffolded.
+ - evidence: lines 48-53 ` ` with no Badge wrapper
+- **[low]** `client/src/components/messaging/MessageComposer.tsx` — Send failure feedback is a bare caption line above the input with no retry button and no aria-live region — the optimistic bubble disappears (rolled back) and the only trace is small error text; a user mid-scroll can easily believe the message was sent. Screen readers are never told the send failed.
+ - evidence: lines 55-59 — plain Typography, no role="alert"/aria-live, no retry action
+- **[low]** `client/src/components/messaging/TicketThreadScreen.tsx` — The sticky composer strip is only a bgcolor block — no top border, shadow, or fade — so message bubbles scroll flush into/under it and visually collide with the input. Also the surface widths are inconsistent across sibling messaging screens: inbox 640px, thread 720px, admin thread 820px.
+ - evidence: lines 100-107 `position:'sticky', bottom:0, bgcolor:'var(--bal-bg-default)'` and nothing else; TicketInboxScreen.tsx:41 maxWidth 640 vs TicketThreadScreen.tsx:45 maxWidth 720
+- **[low]** `client/src/components/notifications/NotificationCenter.tsx` — The error empty-state icon is `error` → MUI `Dangerous` (a filled hazard glyph) for a mundane "couldn't load" state — over-severe and one of several filled icons (Info, Star, Dangerous) mixed into an otherwise Outlined icon set on these surfaces, reinforcing the default-MUI look. Load-more also just grows `limit` and refetches the whole list from offset 0 (O(n) payload growth per click).
+ - evidence: line 79 ` `; config.ts:21 `DangerousIcon`; line 111 `setLimit((current) => current + NOTIFICATIONS_PAGE_SIZE)`
+- **[low]** `client/src/app/[locale]/(private-routes)/admin/notifications/page.tsx` — Admin notifications is still a PlaceholderScreen while the admin nav links to it — a dead-end "coming soon" page in a shipped backoffice.
+ - evidence: lines 4-8 return ` `
+
+## Opportunities (9)
+
+- **Deliver REQ-028 and rebuild the ticket inbox as a real messaging inbox** (impact: high, effort: medium) — Land the contract fields (unreadCount, lastMessageAt, plus a lastMessagePreview snippet and author role of the last message) and redesign TicketListCard around them: bold subject + one-line last-message snippet, unread pill, relative last-activity time, ordering by activity. Add status filter chips (باز/بسته) and load-more/pagination using the params useMyTickets already accepts. This turns a static ticket registry into an inbox users check willingly.
+- **Make the thread live: poll-while-mounted + scroll orchestration** (impact: high, effort: medium) — Give the ticket detail query a refetchInterval while the thread screen is mounted (the seam already exists; SSE can replace it later), auto-scroll to the newest message on open and on send/receive, and show a floating "پیام جدید" pill when the user has scrolled up. Pair with a subtle enter animation for new bubbles. This single change is the biggest step toward messaging-app quality on the only communication channel the platform allows.
+- **Chat-grade thread typography: date separators, grouping, time-only stamps, system event lines** (impact: high, effort: medium) — Insert centered Shamsi date separators (امروز / دیروز / ۲۵ تیر), collapse consecutive same-author messages into a group with one author label, show hh:mm-only inside bubbles (full date on long-press/hover), and render authorRole 'system' as centered chip-style event lines (e.g. "تیکت هماهنگی ایجاد شد"). Give the support author a terracotta-tinted label/avatar dot so "a human from Balinyaar" reads warm and distinct — a direct trust cue in a healthcare product.
+- **Desktop notification popover + bell micro-interaction** (impact: medium, effort: medium) — On the nurse/admin desktop shells, make the bell open a Popover with the 5 most recent notifications, mark-all-read, and a "view all" link to the full center; keep the direct navigation on the mobile customer shell. Add a one-shot badge pulse/ring animation when the polled count increases so arriving activity is felt, and a document.title/favicon unread hint since polling is 60s.
+- **Notification center: day grouping, relative time, per-kind visual identity** (impact: medium, effort: small) — Group rows under امروز / دیروز / این هفته headers, use relative timestamps ("۵ دقیقه پیش") that decay into Shamsi dates, and give each kind a soft tinted icon container (booking teal, payout success-green, ticket terracotta, refund info) instead of the uniform primary icon — the list gains scannable hierarchy without new data. Add a trailing chevron only on rows that deep-link.
+- **Right-size the emergency affordance per surface** (impact: medium, effort: small) — Keep the full red banner with tel: only where the phone exists (nurse booking detail, post-confirmation). In the ticket inboxes, replace the permanent alarm banner with a compact, neutral "موارد اضطراری" help row that expands to the playbook copy — and on the customer side rewrite the copy so it doesn't instruct calling a number the customer can never see. Reduces alarm fatigue while keeping the safety path one tap away.
+- **Ticket lifecycle + trust affordances** (impact: medium, effort: medium) — Add user-side close/reopen actions, a post-resolution satisfaction prompt, and — most valuable for trust — an expected-response-time promise in the thread and after ticket creation ("پشتیبانی معمولاً ظرف ۲ ساعت پاسخ میدهد") plus a "support has seen this" state when staff first views. In a marketplace where disputes are money-adjacent, telling families when to expect an answer is a product feature, not copy.
+- **Composer upgrades: photo attachments, retry-in-place, mobile keyboard behavior** (impact: medium, effort: large) — Refund and coordination tickets routinely need evidence (receipts, care-situation photos); add an attachment affordance to the composer (the object-storage seam already exists server-side for verification docs). Convert send failure into a failed bubble with an inline "تلاش مجدد" chip instead of the vanish-and-caption pattern, add aria-live, and make Enter insert a newline on touch devices.
+- **Build the admin notifications surface or remove the nav entry** (impact: low, effort: small) — Either implement a minimal admin alert feed (b1 alert facade already exists) behind the placeholder route, or drop the nav item until it exists — a "coming soon" page in a staff backoffice erodes confidence in the rest of the console.
+
+## Keep (do not regress)
+
+- The optimistic-send architecture is genuinely excellent: clientMessageId reconciliation (never a double bubble), draft cleared only on server confirm so a failure never loses typed text, and the composer remount-keyed per ticketId so drafts/in-flight state never leak across threads (TicketThreadScreen.tsx:117-119, MessageComposer.tsx:42).
+- RTL discipline via logical properties throughout: borderStartEndRadius/borderStartStartRadius bubble tails, marginInlineStart:'auto', borderInlineStart accent on EmergencyBanner, textAlign:'start' on ButtonBase cards, justifyContent flex-end/flex-start for mine/theirs that mirrors automatically in fa.
+- Everything colors from --bal-* semantic tokens with both schemes defined — zero hard-coded hexes in any of these components, so dark mode holds up by construction (StatusChip even documents 'Never hard-code a hex here').
+- Every list surface (notification center, ticket inbox, thread) ships all four states — skeleton (with alternating bubble-shaped skeletons in threads), empty with title+body copy, error with retry, populated.
+- The is_internal boundary is airtight in the UI layer: user-side types never model it, MessageBubble/TicketMessageList never render it, and the admin surface renders internal notes unmistakably (dashed warning border + badge + distinct bg) so staff can't confuse a note with a reply.
+- referenceCode treated as the support currency: shown prominently in the inbox card, thread header, and creation-success dialog, correctly forced LTR for the Latin code.
+- Polite, well-factored polling: only the unread count polls (60s, auth-gated, stale-while-revalidate) and only the tiny bell container re-renders on count change — the shell never does.
+- Role-aware deep-linking centralized in notificationDeepLink with null-safe fallbacks (a notification that doesn't apply to the role is non-navigable, never a broken route), shared by bell and center.
+- Emergency contact is tel:-only, gated to the nurse post-confirmation care read, with no VoIP or phone directory anywhere — the product's anti-disintermediation rule is faithfully enforced in the UI.
+- Persian copy in messages/fa.json is human and warm ('بهروز هستید', 'هنوز پیامی نیست، هماهنگی را شروع کنید'), with proper ICU plurals on the bell aria-label; Shamsi dates render via the Intl Persian calendar with no date library.
diff --git a/dev/post-phase/ui/audit/microcopy.md b/dev/post-phase/ui/audit/microcopy.md
new file mode 100644
index 0000000..ae5037d
--- /dev/null
+++ b/dev/post-phase/ui/audit/microcopy.md
@@ -0,0 +1,65 @@
+# UI microcopy quality (fa + en message catalogs)
+
+## Current state
+
+All UI copy lives in two flat-namespace catalogs, client/messages/fa.json and client/messages/en.json (~1,582 lines each, 28 namespaces, full key parity — no missing namespaces or keys on either side). Coverage is unusually complete for a young product: every namespace ships loading/empty/error strings (e.g. booking.list_error, payouts.history_empty_body, tickets.thread_empty_body), confirm-dialog bodies, toasts, and field-level hints. The Persian is clearly hand-written, not machine-translated: it consistently uses the formal شما register with polite imperatives (کنید), uses Persian digits in literals (۲۴ ساعت, ۷ تا ۱۰ روز کاری), and gets culturally sensitive register right — خانم/آقا for caregiver gender in search/booking vs the clinical مرد/زن for patient gender in onboarding. The English catalog is idiomatic and often better than the Persian ("Home care you can trust", "the detail a nurse needs to find the door", "Queue clear — nothing to review").
+
+The trust-critical copy — the product's core — is mostly strong: payment.escrow_notice explains escrow in plain language, payouts.explainer_point_1–3 explain the weekly batch / 72-hour dispute window / BNPL-fee-never-deducted invariant to nurses, refunds uses calming status language ("در راه" / "On its way", failed → "نیازمند بررسی" / "Needs attention"), and verification failure reasons (reason_shared_sim, reason_blurry_scan) tell the nurse exactly what to fix. The weaknesses are not tone but craft: inconsistent Persian orthography (hamza, ZWNJ, even the brand name itself is spelled two ways), two genuine grammar bugs that read as nonsense ("ورود بازی", "هشدار بازی"), one broken help-text sentence, a typo on a trust chip, missing ICU plural/zero handling on the fa side where en has it, one wrong-direction arrow baked into an en string, and policy numbers (24h/72h/7–10 days) hardcoded into copy that the admin config panel can change. Namespaces most needing a copy pass, in order: booking (largest at 183 keys, contains the grammar bug and arrow bug), verification (heaviest تایید/تأیید mixing), payment/auth/common (brand spelling split), bnpl (jargon), search (hardcoded city suggestions, missing plurals), and admin (second grammar bug).
+
+## Problems (16)
+
+- **[high]** `client/messages/fa.json` — The brand name itself is spelled two different ways: 'بالین یار' (space) in 5 keys vs 'بالینیار' (ZWNJ) in 17 keys. Users see one spelling on the login screen and another on the payment/escrow/refund screens — for a trust-first brand, an unstable brand mark in the money copy is the worst place to be inconsistent.
+ - evidence: common.brand + auth.customer_title/select_role_title + verification.start_body use 'بالین یار'; payment.row_commission, payment.escrow_notice, refunds.admin_approval_explainer, payouts.balance_owed_label and 13 more use 'بالینیار'
+- **[high]** `client/messages/fa.json` — Grammar bug that reads as nonsense: 'ورود بازی برای ثبت خروج وجود ندارد' — the indefinite ی attached to باز makes it read as 'there is no game-entrance'. Nurse-facing EVV error. Should be e.g. 'ورودِ ثبتشدهای برای خروج وجود ندارد؛ ابتدا ورود را ثبت کنید.' The same bug appears in admin.alert_empty: 'هشدار بازی وجود ندارد' ('no game alert') — better: 'هشداری برای رسیدگی نیست.'
+ - evidence: line 542 booking.evv_no_open_check_in and line 1360 admin.alert_empty
+- **[high]** `client/messages/fa.json` — Broken sentence in customer-facing address help text: 'جزئیاتی که پرستار برای یافتن در نیاز دارد' — a word-for-word translation of the en 'find the door' where 'در' (door) collides with the preposition 'در', so it reads as 'details the nurse needs for finding in'. Should be e.g. 'هر جزئیاتی که پرستار برای پیدا کردن منزل شما لازم دارد.'
+ - evidence: line 228 address.line_hint
+- **[high]** `client/messages/fa.json` — Typo on a trust-status chip: bank verification success chip reads 'تاییدشد' (missing final ه) instead of 'تأییدشده'. This is the chip a nurse stares at while waiting for IBAN ownership verification — a typo exactly where the product is asserting correctness.
+ - evidence: line 180 bank.status_verified_chip: "تاییدشد"
+- **[high]** `client/messages/fa.json` — The word تأیید (confirm/verify) — the single most frequent word in a verification product — is written without hamza ('تایید', 38 occurrences) and with hamza ('تأیید', 25 occurrences) with no pattern, often inside the same namespace (verification.status_passed 'تاییدشده' vs verification.identity_title 'تأیید هویت'; booking uses no-hamza, admin uses hamza). Same-status labels also diverge across namespaces (verification.status_failed 'رد شد' vs admin.step_failed 'ناموفق'). Pick one orthography (recommend the hamza form) and one status vocabulary.
+ - evidence: 38 no-hamza vs 25 hamza occurrences (grep count); e.g. bank.status_verified_title vs admin.ver_pass
+- **[medium]** `client/messages/en.json` — Wrong-direction arrow baked into an English string: booking.continue_payment is 'Continue to payment ←' (left arrow in LTR English) while the sibling key payment.cta_pay is 'Continue to payment →'. The fa file mirrors arrows manually per-string — directional glyphs inside translatable copy is exactly how this bug happens; arrows belong in the component as mirrored icons.
+ - evidence: en.json line 445 booking.continue_payment: "Continue to payment ←" vs line 577 payment.cta_pay: "Continue to payment →"
+- **[medium]** `client/messages/fa.json` — fa lacks ICU plural/zero handling where en has it, so Persian users get degenerate strings: search.cta_view_results is 'مشاهده {count} پرستار' → the primary search CTA can render 'مشاهده ۰ پرستار'; search.results_count, search.reviews_count and booking.session_count have the same gap. en handles =0 ('no nurses') — though en's own =0 case produces the odd button label 'View no nurses', so both sides need a proper zero-state ('پرستاری یافت نشد' / 'No nurses found').
+ - evidence: fa line 350 cta_view_results, 352 results_count, 364 reviews_count, 515 session_count vs en ICU-plural equivalents
+- **[medium]** `client/messages/fa.json` — BNPL trust copy is written from the platform's perspective with banking jargon: bnpl.ownership_note tells the customer 'ریسک نکول مشتری کاملاً با اوست' ('the customer's default risk is entirely the provider's') — 'نکول' is credit-desk vocabulary, and framing the reader as 'the customer' in third person is cold and confusing. Rewrite reader-first: 'قسطها را مستقیماً به {provider} میپردازید؛ بالینیار مبلغ کامل را همان ابتدا دریافت میکند و پرستار شما تحت تأثیر قرار نمیگیرد.'
+ - evidence: line 857 bnpl.ownership_note
+- **[medium]** `client/messages/fa.json` — Policy numbers are hardcoded into trust-critical copy while the admin config panel (cfg_group_deadlines, cfg_group_cancellation) can change them: the 72-hour dispute window (payouts.explainer_point_2), the 24-hour cancellation tiers (refunds.lead_gt_24h/lead_lt_24h), and the 7–10 business-day refund ETA (refunds.eta_business_days). One config edit silently makes the UI copy lie — these should be interpolated ({hours}, {days}) from server-served config.
+ - evidence: fa lines 958, 789–790, 850; same hardcodes in en.json
+- **[medium]** `client/messages/fa.json` — Search empty-state suggests hardcoded cities regardless of where the user searched: 'شهر نزدیک دیگری مانند مشهد، اصفهان یا شیراز را امتحان کنید' — Mashhad, Isfahan and Shiraz are ~900km apart and are nonsense advice for a Tehran user (the launch market). Replace with a location-neutral suggestion or interpolate actual nearby covered cities.
+ - evidence: line 361 search.empty_suggest_city (en line 361 identical pattern)
+- **[medium]** `client/messages/fa.json` — 'احراز هویت' is overloaded: it names the entire 7-step verification pipeline (nav.verification, verification.title) AND one specific step inside it (step_identity_kyc 'احراز هویت (ثبت احوال)', admin.step_identity_kyc 'احراز هویت'). A nurse who completed the KYC step but sees 'احراز هویت' still incomplete in nav gets contradictory signals. Name the pipeline differently, e.g. 'تأیید صلاحیت'.
+ - evidence: fa lines 13, 658 vs 676 and 1470
+- **[medium]** `client/messages/fa.json` — Domain-term drift: 'مددجو' (care recipient) appears exactly once as a parenthetical — booking.patient_label 'بیمار (مددجو)' — while every other surface says 'بیمار'. Either adopt مددجو consistently (it is the softer, industry-standard term for home care) or drop the one-off. Similarly 'جستجو' (9 keys) vs 'جستوجو' (2 keys) are mixed.
+ - evidence: line 398 booking.patient_label; جستوجو in coverage.empty_warning and booking.missing_nurse_body only
+- **[low]** `client/messages/fa.json` — The EVV acronym is exposed to nurses untranslated and never explained: 'ثبت ورود (EVV)', 'ویزیتهای امروز… با EVV ثبت کنید'. First occurrence should introduce it — 'ثبت حضور الکترونیکی (EVV)' — then short-form thereafter; a Latin acronym as the only name for a core nurse workflow is alienating in a fa-default product.
+ - evidence: lines 530–533 booking.evv_* keys; admin.cfg_group_evv does gloss it as 'ثبت حضور (EVV)'
+- **[low]** `client/messages/en.json` — en catalog mixes British and American conventions: 'licence' (auth.nurse_subtitle) against American 'center' (21 occurrences: 'Partner centers', nav.partner_home 'Center'); admin namespace also uses curly apostrophes ('don’t', 'Couldn’t') where the rest of the file uses straight ones. Pick American throughout.
+ - evidence: en line 628 'Nursing Council licence' vs nav.partners 'Partner centers'; en line 1199 'don’t'
+- **[low]** `client/messages/fa.json` — Register slips into bureaucratic officialese in a few money strings: refunds.confirm_restate ends 'کسر میگردد' (archaic میگردد) while the rest of the catalog uses 'میشود'; tickets.thread_empty_body is a comma splice ('هنوز پیامی نیست، هماهنگی را شروع کنید.'). Small, but the cancellation-confirm sentence is a high-anxiety moment where stiff prose reads as fine print.
+ - evidence: line 814 refunds.confirm_restate; line 1141 tickets.thread_empty_body
+- **[low]** `client/messages/fa.json` — The four app shells are named with four different metaphors: 'اپلیکیشن خانواده' (app), 'نمای پرستار' (view), 'کنسول مدیریت' (console), 'پرتال همکار' (portal). Harmless individually but signals no naming system; pick one pattern per audience.
+ - evidence: lines 57–61 shell namespace
+
+## Opportunities (7)
+
+- **Check in a Persian style guide + terminology glossary and lint the catalogs against it** (impact: high, effort: small) — One page in the repo (e.g. client/messages/STYLE.md) fixing: brand = 'بالینیار' (ZWNJ), hamza form 'تأیید', 'جستوجو' or 'جستجو' (pick one), ZWNJ rules for می/ها, the domain glossary (پرستار، مددجو vs بیمار، ویزیت، رزرو، نوبت، شبا), and one status vocabulary shared by nurse-facing and admin-facing keys. Then a 30-line node script in CI that greps fa.json for the banned variants (the space-brand, no-hamza تایید, 'بازی' word-boundary traps). This converts today's 60+ scattered orthography findings into a one-time fix that can never regress.
+- **Serve policy numbers into copy instead of hardcoding them** (impact: high, effort: medium) — The dispute-window hours, cancellation tiers/percentages, and refund ETA days already live in server config (admin cfg_ keys exist to edit them). Change the message keys to take parameters ('پس از بستهشدن پنجرهٔ {hours} ساعته اعتراض…') and feed them from the same endpoint that serves fees. This keeps the product's most legally-sensitive copy permanently truthful and unlocks per-tier cancellation copy on the C-side cancel screen.
+- **Add fa plural/zero variants + Persian digit formatting policy** (impact: medium, effort: small) — Give every {count} key an ICU form with a designed =0 case (fa: 'پرستاری یافت نشد' instead of '۰ پرستار'; fix en's 'View no nurses' button while there). Decide once whether interpolated numbers render as Persian digits ({count, number} with fa locale) — currently literals use Persian digits but interpolations will render Latin, so a card can read '۷۲ ساعت' next to '3 visits'.
+- **Trust-moments copy pass: write the reassurance where the money moves** (impact: high, effort: small) — The catalogs explain escrow and payouts well, but three anxiety peaks still have thin copy: (1) the OTP screen says nothing about why a family should trust the platform (auth.customer_subtitle is just 'sign in with your mobile'); (2) the checkout escrow notice is one sentence with no link to how disputes work; (3) the nurse 'accept request' screen never states the payout amount protection ('پرداخت خانواده نزد بالینیار امانت میماند'). Add one warm trust line per moment — this is copy, not UI work, and it is the cheapest trust lever the product has.
+- **Introduce EVV in Persian once, then abbreviate** (impact: medium, effort: small) — Add a first-run explainer key ('ثبت حضور الکترونیکی (EVV) — ورود و خروج شما موقعیتسنجی میشود تا ویزیت بدون اختلاف تأیید شود') shown on the nurse's first visit day, and keep the short chips thereafter. Turns an alienating acronym into a selling point (EVV is why the nurse gets paid without arguments).
+- **Move directional arrows out of strings into mirrored icon components** (impact: low, effort: small) — Five keys embed ←/→ literally and one (en booking.continue_payment) already points the wrong way. Replace with an end-icon in the button component that auto-mirrors with dir; also fixes admin.cfg_history_change ('{old} ← {new}') which relies on translators hand-mirroring.
+- **Disambiguate the verification pipeline name and rename status vocabulary once** (impact: medium, effort: medium) — Rename the pipeline (nav + page titles) to 'تأیید صلاحیت' while the KYC step keeps 'احراز هویت'; simultaneously unify the failed/rejected status words (رد شد vs ناموفق vs ردشده) into one nurse-facing and one admin-facing set. Do it as a single sweep because the terms cross-reference each other.
+
+## Keep (do not regress)
+
+- payment.escrow_notice — plain-language escrow explanation at checkout ('مبلغ بهصورت امانی نزد بالینیار میماند و پس از پایان ویزیت آزاد میشود'); exactly the right sentence at the right moment
+- payouts.explainer_point_1–3 — the three-bullet 'how payouts work' copy, especially point 3 guaranteeing the nurse the BNPL provider fee is never deducted from her; this is model trust writing
+- Calming money-status vocabulary: refund steps 'ثبتشده → در راه → انجامشده' and failed states softened to 'نیازمند بررسی'/'Needs attention' with 'you don't need to do anything else' reassurance (refunds.failed_body, payouts.failure_hint)
+- Two-stage disclosure copy is precise on both sides: booking.notes_hint tells the family exactly what the nurse sees pre-acceptance, and booking.disclosure_note tells the nurse exactly what unlocks after accepting
+- Culturally-tuned gender copy: خانم/آقا (polite) for caregiver preference vs مرد/زن (clinical) for patient gender, plus search.gender_hint explaining same-gender preference for bodily care without awkwardness
+- Verification failure reasons are specific and actionable (reason_shared_sim tells the nurse the SIM isn't in her name and what to do; reason_blurry_scan asks for a sharper copy) — no generic 'verification failed' anywhere
+- Consistent formal شما register across the entire fa catalog with warm touches where appropriate ('بهروز هستید' empty notifications state, 'طولی نمیکشد' during BNPL settling)
+- The en catalog is genuinely idiomatic hand-written English, not a translation dump — 'Queue clear — nothing to review', 'What stood out', 'Home care you can trust'
+- Every namespace ships complete loading/empty/error/confirm/toast strings — the string coverage for states is already better than most mature products
+- Admin confirm-dialog bodies state consequences honestly ('Money moves to nurses. This is protected by an idempotency key — a double-click can't pay a booking twice.') — keep this operational candor
diff --git a/dev/post-phase/ui/audit/nurse-trust-ops.md b/dev/post-phase/ui/audit/nurse-trust-ops.md
new file mode 100644
index 0000000..ae52e04
--- /dev/null
+++ b/dev/post-phase/ui/audit/nurse-trust-ops.md
@@ -0,0 +1,73 @@
+# Nurse trust & operations — verification, request inbox, visits/EVV, earnings & payouts
+
+## Current state
+
+The nurse side lives under client/src/app/[locale]/(private-routes)/nurse/ inside NurseLayout (client/src/layout/NurseLayout.tsx), a 10-item flat sidebar + fixed TopBar shell inherited from the starter (TopBarAndSideBarLayout.tsx). Verification is a hub-and-spoke: verification/page.tsx (B3 hub) renders VerificationChecklist.tsx (an "X از Y" LinearProgress meter + data-driven step rows via verificationSteps.ts, reusing the shared StatusChip) with a single "continue" CTA; identity/page.tsx (B4) collects national ID + two local DocumentUpload captures; credentials/page.tsx (B5) renders one DocumentUpload per manual step plus INO number, specialty Chips and native type="date" registry dates; review/page.tsx (B6) is a second view of the same cached query. B4/B5/B6 carry a bare default-MUI StepperHeader (3 macro steps) alongside the hub's 7-step checklist. DocumentUpload owns a full idle→uploading(progress %)→success(preview)→error state machine plus a rejected variant with reason + re-upload.
+
+The request inbox (requests/page.tsx) polls every 15s and lists pending-only cards (patient name, Shamsi time, required-gender chip, notes preview, per-card CountdownTimer) with an "open detail" button; requests/[id]/page.tsx shows the masked city·district location, stage-1 notes only, and 50/50 accept / reject-with-reason-dialog buttons plus 409-stale handling. Visits (visits/page.tsx) is a "today's sessions" list of shared SessionCard components with terracotta EVV check-in/out buttons driven by useEvvController (GPS via an ILocationProvider seam that never rejects — denied GPS still checks in, advisory) and EvvStatusBanner (success/warning/info tokens, mismatch is never an error); visits/[id]/page.tsx composes the both-roles BookingDetailView (timeline, sessions with EVV, money summary, gated CareInstructionsCard) + NurseVisitNotesPanel (append-only note + task checklist). Earnings (earnings/page.tsx) shows EarningsBalanceHeader (signed net balance with an explicit "owed back" negative state + four token-coded buckets), a collapsible explainer, state-filter Tabs and EarningsRow items (PriceBreakdown gross−commission=payout, per-state affordances, dispute-window countdown); payouts/page.tsx and payouts/[id]/page.tsx render PayoutHistoryRow / batch reconciliation with masked IBAN, transfer reference, and read-only failure banners. Styling throughout is flat bordered Paper cards with borderInlineStart accent strips, --bal-* CSS variables (mirrored dark scheme in src/theme/tokens.css), Persian digits via Intl fa-IR, and Shamsi dates via formatShamsiDate. Notably, the nurse landing page /nurse (nurse/page.tsx) is still a PlaceholderScreen.
+
+## Problems (19)
+
+- **[high]** `client/src/app/[locale]/(private-routes)/nurse/page.tsx` — The nurse landing page after login is a bare PlaceholderScreen — there is no operational home tying together today's visits, pending requests (with deadlines), verification progress, and earnings. Every session starts at a dead end and the sidebar is the only wayfinding.
+ - evidence: line 7: `return `
+- **[high]** `client/src/components/booking/BookingDetailView/BookingDetailView.tsx` — The nurse day-of flow contains no service address, no family/patient contact, and no navigate/call affordance anywhere — not on the visits day list, not on the booking detail. The header renders only patient + nurse names; the DTO's addressSnapshotJson is never rendered, and the bookings mock even nulls it for the nurse view (mockApi.ts:358). A field nurse cannot find where to go or reach the family from the app, even on a confirmed (post-payment, stage-2) booking.
+ - evidence: lines 90-93 render only HeaderFact(patient) + HeaderFact(nurse); `addressSnapshotJson` has zero render-site references in src/
+- **[high]** `client/src/components/DocumentUpload/DocumentUpload.tsx` — Rejected-step recovery loses all upload feedback: the `rejected` branch of the render ternary takes precedence over `state === 'uploading'`, and the prop only flips after the status query invalidates. So when a nurse re-uploads a rejected document, the card stays frozen on the red 'rejected' state for the whole upload (no progress bar, no success flash) and the re-upload button stays enabled mid-flight, inviting double submissions on the single most anxiety-laden path.
+ - evidence: line 155 `{rejected ? (` … precedes line 193 `: state === 'uploading' ? (`; re-upload AppButton (lines 182-191) is only disabled by the `disabled` prop
+- **[high]** `client/src/components/booking/SessionCard/SessionCard.tsx` — The EVV check-in/check-out CTA — the most important tap of a nurse's day, done on a phone at a doorstep — is a small start-aligned button (`alignSelf: 'flex-start', py: 1`) visually equal to tertiary links around it. No full-width layout, no large touch target, no sticky positioning, no visual weight distinguishing it from 'view booking'.
+ - evidence: lines 117-141: both EVV buttons use `sx={{ m: 0, alignSelf: 'flex-start', py: 1 }}`
+- **[high]** `client/src/app/[locale]/(private-routes)/nurse/verification/credentials/page.tsx` — The credentials form does not survive re-entry: INO number, specialties and registry fields start empty every visit (never hydrated from server status), and submit is disabled unless a document was uploaded in this session (`anyUploaded` reads only local `uploadedSteps` state) — so a returning nurse whose docs are already in_review sees blank fields and a dead submit button with no explanation.
+ - evidence: line 106 `const anyUploaded = Object.values(uploadedSteps).some(Boolean)` + line 255 `disabled={submitCredentials.isPending || !anyUploaded}`; state initialised to '' / [] at lines 37-44
+- **[high]** `client/src/app/[locale]/(private-routes)/nurse/verification/credentials/page.tsx` — License issue/expiry dates use native Gregorian `type="date"` inputs in a Persian-default UI — Iranian nurses read their license dates in Shamsi; forcing a Gregorian browser picker on a trust-critical form invites wrong dates (which feed credential-expiry logic).
+ - evidence: lines 220-235: two `` for issued_at / expires_at
+- **[medium]** `client/src/app/[locale]/(private-routes)/nurse/requests/page.tsx` — Inbox cards omit the requested service/variant and its price — the decision-critical facts. A nurse sees patient name, time, gender chip and a notes preview but must open each detail to learn what job is being requested and what it pays.
+ - evidence: InboxCard (lines 57-115) renders counterpartyName, whenLabel, gender chip, customerNotes only; variantLabel/variantPrice appear only in [id]/page.tsx
+- **[medium]** `client/src/app/[locale]/(private-routes)/nurse/requests/page.tsx` — Pending-only, page-1-only inbox: the page hardcodes the hook's default status ('pending_nurse_response') with no tabs for answered/expired history (no way to review past decisions or learn from expirations), and no pagination UI even though the API pages at 20 — a 21st pending request is unreachable.
+ - evidence: line 19 `useNurseRequestInbox()` called with no status/page args; no Pager rendered; BOOKING_REQUEST_PAGE_SIZE = 20
+- **[medium]** `client/src/components/CountdownTimer/CountdownTimer.tsx` — No urgency escalation: the response-deadline countdown stays calm teal from 24h down to 00:00:01 (the `urgent` prop is static and unused by the inbox), and on inbox cards it renders without a label — bare ticking digits floating at the card corner. Also ambiguous formats: under an hour it drops to MM:SS which reads like HH:MM, and multi-day dispute windows render as raw hour counts like ۱۲۶:۴۴:۰۲.
+ - evidence: line 66 `const accent = urgent ? ... : 'var(--bal-primary)'`; lines 88-90 drop the hours segment when 0; requests/page.tsx:79 passes no `label`
+- **[medium]** `client/src/app/[locale]/(private-routes)/nurse/verification/page.tsx` — Two competing progress metaphors in one journey: the B3 hub counts 7 granular checklist steps ("X از Y" + LinearProgress, inflated by the synthetic mobile step) while B4/B5/B6 show an unrelated default-MUI 3-step Stepper — the nurse gets two different answers to 'how far along am I'. StepperHeader itself is an unstyled starter Stepper (default numbered circles).
+ - evidence: VerificationChecklist ProgressMeter vs StepperHeader.tsx lines 20-28 (bare `` wrap); verificationSteps.ts MOBILE_STEP id 0 always 'passed'
+- **[medium]** `client/src/app/[locale]/(private-routes)/nurse/earnings/page.tsx` — ExplainerCard's collapse header is a clickable Stack with no button semantics (no role, tabIndex, aria-expanded — keyboard users can't open the 'how payouts work' copy), and it uses eye icons (visibilityon/visibilityoff) as an expand/collapse affordance instead of a chevron, even though an 'expand' icon exists in the registry.
+ - evidence: lines 127-139: `` + ` `
+- **[medium]** `client/src/app/[locale]/(private-routes)/nurse/earnings/payouts/[id]/page.tsx` — Raw vendor strings shown to nurses: failed payouts print the bank rail's `failureReason` verbatim (LTR English/bank codes) in a Persian UI, in both the payout detail and PayoutHistoryRow; likewise VerificationChecklist falls back to the raw snake_case failure code (e.g. 'blurry_scan') when an i18n key is missing.
+ - evidence: payouts/[id]/page.tsx lines 127-131 `{t('failure_reason_label')}: {data.failureReason}` dir="ltr"; PayoutHistoryRow.tsx 101-105; VerificationChecklist.tsx 75-79 `: step.failureReason`
+- **[medium]** `client/src/layout/TopBarAndSideBarLayout.tsx` — Starter-grade shell hazards directly affecting these flows: physical `paddingLeft`/`paddingRight` + physically 'left'-anchored desktop drawer put the nurse nav on the trailing side in RTL fa (mobile anchor is 'right' — inconsistent, leftover starter comments in config.ts); the logo icon doubles as the sidebar opener with a hard-coded English 'Open Sidebar' tooltip; the main content gutter is a fixed 8px at all breakpoints.
+ - evidence: lines 53-58 physical paddingLeft/Right keyed on `anchor?.includes('left')`; line 71 `title={... : 'Open Sidebar'}`; line 102 `paddingLeft: 1, paddingRight: 1`; config.ts `SIDE_BAR_DESKTOP_ANCHOR = 'left'; // 'right';`
+- **[medium]** `client/src/app/[locale]/(private-routes)/nurse/earnings/page.tsx` — Page-width chaos across adjacent nurse screens: verification (620) / requests / visits (640) cap maxWidth without mx:'auto' so content hugs the start edge beside a vast empty area on desktop; earnings and payout history have no maxWidth so money rows stretch the full viewport; BookingDetailView centers with mx:'auto'. Three different page shapes in one shell.
+ - evidence: earnings/page.tsx:48 `sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}` (no cap) vs verification/page.tsx:47 `maxWidth: 620` (no mx) vs BookingDetailView.tsx:62 `maxWidth: 640, mx: 'auto'`
+- **[low]** `client/src/app/[locale]/(private-routes)/nurse/requests/[id]/page.tsx` — Accept fires on a single tap with no confirmation, summary of consequence ('the family will be asked to pay; a booking will be created'), or undo — while sitting flex:1 directly beside Reject at equal width. A mis-tap is materially consequential and irreversible from this UI.
+ - evidence: lines 198-219: accept/reject both `sx={{ m: 0, flex: 1, py: 1.25 }}`, `onClick={handleAccept}` mutates immediately
+- **[low]** `client/src/components/common/AppButton/AppButton.tsx` — AppButton ships a starter default `margin: 1`, so virtually every call site in this area fights it with `sx={{ m: 0 }}` (30+ occurrences across the audited pages); any forgotten override produces phantom spacing.
+ - evidence: lines 9-11 `DEFAULT_SX_VALUES = { margin: 1, ... }`
+- **[low]** `client/src/components/booking/SessionCard/SessionCard.tsx` — Terracotta — the brand's 'single sparing accent' — is spread across the nurse surface: EVV contained+outlined buttons, the per-session payout amount text, the 'نمای پرستار' chip, and the notes-panel border/icon/submit all use secondary at once, diluting its urgency value.
+ - evidence: SessionCard line 112 `color: 'var(--bal-secondary-dark)'` + lines 119/133 `color="secondary"`; BookingDetailView 71/83; NurseVisitNotesPanel 73/77/120
+- **[low]** `client/src/app/[locale]/(private-routes)/nurse/visits/page.tsx` — The day surface has no date anchor (no 'امروز، ۲۴ تیر' header), the cards don't say which service the visit is for (patient name + session index only), and useTodaySessions has no refetchInterval — a same-day schedule change won't appear without re-navigation, unlike the polled inbox.
+ - evidence: page title is static `t('evv_visits_title')`; SessionCard receives no service name; useTodaySessions.ts sets only staleTime
+- **[low]** `client/src/app/[locale]/(private-routes)/nurse/verification/review/page.tsx` — The under-review screen gives a text ETA but no submitted-timestamp or 'what happens next' timeline, and unlike its sibling pages it has no page h1/subtitle above the stepper — the heading lives inside the status card, breaking the header rhythm established by every other nurse page.
+ - evidence: lines 26-29: page opens directly with StepperHeader; h1 is the Typography inside the Paper (line 47)
+
+## Opportunities (9)
+
+- **Nurse 'Today' home — replace the placeholder dashboard** (impact: high, effort: medium) — Build /nurse as an operational hub: (1) next visit card with a check-in shortcut and time-until, (2) pending requests strip with the most urgent countdown and an inline accept path, (3) a verification-progress card (reusing the cached status query) until approved, (4) this-week earnings snapshot with the next-payout date. All four data sources already exist as cached queries — this is composition, not new plumbing, and it is the single highest-leverage screen for making the product feel alive and trustworthy to nurses.
+- **Visit workspace: address, contact, and an in-visit mode** (impact: high, effort: large) — On a confirmed booking, surface the stage-2 disclosure the product already promises: render addressSnapshotJson as an address card with a map deep-link (Neshan/Balad/Google via geo: URI) and a tel: 'call family' action; make check-in a full-width sticky-bottom hero button; after check-in switch the screen into an 'in-visit' mode (elapsed timer, task checklist promoted from the notes panel, check-out CTA); after check-out show a payout confirmation moment ('این ویزیت X تومان به درآمد شما اضافه شد'). This is the flow nurses live in daily and today it's a stack of generic cards.
+- **Mobile bottom navigation for the nurse shell** (impact: high, effort: medium) — Field nurses are on phones; the current pattern (tap the logo to open a right-anchored drawer with 10 flat items) hides everything. A 4-5 item bottom nav (امروز / درخواستها with unread-count badge / درآمد / پروفایل) plus overflow drawer would transform day-of usability. The BottomBar component already exists in the layout folder, disabled by starter config.
+- **Decision-first inbox card redesign + urgency system** (impact: high, effort: medium) — Reshape InboxCard around the accept decision: service name + price as the headline, an urgency-tinted countdown pill (teal >2h → amber <2h → terracotta <30min, aria-live), patient/time/gender as secondary facts, and inline accept/decline directly on the card (dialog only for reject reason). Add tabs (در انتظار / پاسخداده / منقضی), a pager, and a sidebar badge for pending count. Pair with a 'fast responses win more bookings' trust nudge in the empty state.
+- **Unified verification journey with the trust payoff visible** (impact: high, effort: medium) — Merge the two progress metaphors into one vertical journey page: a hero showing a live TrustBadge preview ('این نشان را خانوادهها میبینند') that fills in as steps pass, grouped step cards (identity / credentials / bank) replacing both the 3-step Stepper and the flat 7-row list, per-step ETA chips for in_review items (the 24-48h promise, today only on B6), a submitted-at timestamp, and a celebratory approved state that leads into publish. Verification is the product's core trust ritual and currently reads as a settings checklist.
+- **Shamsi date picker component** (impact: medium, effort: medium) — A reusable Jalali date field (wheel or calendar) to replace native type="date" in credentials — and later everywhere dates are entered. Directly removes a correctness risk on license expiry data that feeds the credential-expiry sweep.
+- **'Next payout' forecast on earnings** (impact: medium, effort: small) — Add a single server-provided line above the tabs: next batch date (holiday-shifted) + the eligible amount expected in it ('پرداخت بعدی: شنبه — ۲٬۴۵۰٬۰۰۰ تومان'). Converts the four abstract buckets into the one answer nurses actually seek ('when do I get paid, how much'), and replace the raw HH:MM:SS dispute-window countdown with day-granular copy ('۲ روز تا آزادسازی').
+- **New-request notifications beyond the 15s poll** (impact: high, effort: large) — The 2h response deadline is only survivable if the nurse happens to have the tab open. Add a web-push opt-in banner on the inbox (service worker + the existing notifications service), falling back to SMS via the backend's Kavenegar rail for accepted-critical events. Deadline-driven marketplaces live or die on this.
+- **Upload polish: fix rejected-state machine and add capture guidance** (impact: medium, effort: small) — Beyond the precedence bug fix, elevate DocumentUpload for the trust flow: show a frame overlay/illustration for the ID-card capture, image-too-dark/blurry client hints, and keep the rejected reason visible above (not instead of) the progress UI during re-upload so nurses see their recovery succeeding.
+
+## Keep (do not regress)
+
+- Semantic token discipline: every audited component colors via --bal-* CSS variables (StatusChip, TrustBadge, EvvStatusBanner, EarningsBalanceHeader) with explicit 'never hard-code a hex' comments and a mirrored dark scheme in src/theme/tokens.css — no raw hexes found in this area.
+- Complete loading/empty/error coverage: every list and detail screen (verification hub, inbox, request detail, visits, earnings, payout history, payout detail) has a real skeleton, a designed dashed-border empty state with icon+copy, and an error panel with retry.
+- The EVV advisory philosophy in code: GPS denial/timeout never blocks a check-in (locationProvider never rejects; controller submits null coords with a warning toast), out-of-range renders warning-toned never error (EvvStatusBanner), and per-session busy state isolates the acting card.
+- CountdownTimer correctness: server-frozen deadlines only, an isolated 1s tick that re-renders nothing around it, Persian digits forced dir=ltr so HH:MM:SS order survives RTL, and a single onElapsed refetch hook.
+- Money honesty in EarningsBalanceHeader/EarningsRow/PriceBreakdown: negative net renders as an explicit error-toned 'owed back' card (never a bare minus), gross − commission = payout is reconciled visually, clawbacks get their own explanatory breakdown, and all money is BigInt-safe display-only strings.
+- Two-stage disclosure enforced in the UI layer: request detail renders only customerNotes + coarse city·district with an explanatory disclosure note; the care-instructions query is enabled only for the nurse on a confirmed+ booking (never fired for customers).
+- Rejected-step recovery paths exist everywhere and are data-driven: checklist rows surface the failure reason with a 'fix' CTA routed by step code, DocumentUpload has a dedicated rejected variant with reason + re-upload, and the Shahkar shared-SIM failure gets deliberately non-accusatory warning copy.
+- RTL-aware physical CSS throughout content components: borderInlineStart accent strips, textAlign 'start'/'end' (no left/right), and dir="ltr" islands on IBANs, transfer references, national-ID input, and clock strings.
+- Locale-correct numerals and dates: Intl NumberFormat fa-IR for all digits (progress counts, pagers, countdown) and formatShamsiDate for every date — no Gregorian date strings leak into the fa UI (except the native date-input problem flagged separately).
+- The 'honesty constraint' pattern in verification: only genuinely automated checks advertise استعلام خودکار, manual-review copy never claims an authority check, and TrustBadge 'verified' renders only from the approved aggregate with expired visually distinct from unverified.
diff --git a/dev/post-phase/ui/audit/nurse-workspace.md b/dev/post-phase/ui/audit/nurse-workspace.md
new file mode 100644
index 0000000..40300cf
--- /dev/null
+++ b/dev/post-phase/ui/audit/nurse-workspace.md
@@ -0,0 +1,70 @@
+# Nurse-side workspace — dashboard, profile, service pricing (variant builder), coverage, bank
+
+## Current state
+
+The nurse workspace lives under client/src/app/[locale]/(private-routes)/nurse/, wrapped by RoleGuard + NurseLayout (client/src/layout/NurseLayout.tsx), which renders the starter-derived TopBarAndSideBarLayout: fixed top bar + a 240px persistent desktop sidebar with a flat 10-item nav (dashboard, requests, profile, services, coverage, bank, verification, visits, earnings, support). The nurse home (nurse/page.tsx) is literally PlaceholderScreen — an icon, the nav title, and generic "coming later" copy — so the landing surface of the entire nurse business is empty while every ingredient of a real dashboard already exists as a cached hook elsewhere (useNurseRequestInbox, useNurseEarningsBalance, useVerificationStatus, bank/coverage/variant queries).
+
+The functional pages are competent, narrow form columns (each sets its own maxWidth 560–640 and hugs the start edge of the wide shell). Profile (nurse/profile/page.tsx) edits only avatar + bio + years, shows the TrustBadge and a blocked-until-verified banner, and passes education/specialization fields through untouched. Services (nurse/services/page.tsx) switches in-page between MyServicesList (VariantCard rows with soft deactivate/reactivate, skeletons, a good dashed empty state, and the PublishGate verification banner) and VariantBuilder — a 3-step create stepper (CategoryTile grid → option-group ToggleButtonGroups with required badges → Toman price entry with a live PriceDisplay estimate and auto-generated display name); edit mode locks category/options and edits price only. Coverage (nurse/coverage/page.tsx) renders areas as chips, an add card with a whole-city/districts scope toggle plus CascadingRegionSelect (province→city→district, aggressively cached, loading adornments), inline duplicate blocking mirrored to the server 409, and a confirm dialog on remove. Bank (nurse/bank/page.tsx) renders each account through BankStatusPanel's three semantic states (pending/verified/mismatch) with masked LTR IBAN, a make-primary action, and a re-enter path on mismatch.
+
+Styling is token-disciplined: zero hard-coded hexes in the whole nurse tree (grep-verified), all color through --bal-* semantic tokens, accents via RTL-safe borderInlineStart. But the composition is default-MUI: plain bordered Papers, MUI Stepper header, ToggleButtonGroups, h5+body2 page headers repeated by hand, and the AppButton starter component whose built-in margin every call site cancels with sx={{ m: 0 }}. The systemic behavioral gap is error handling: most queries destructure only { data, isLoading }, so a failed request renders the *empty* state, and several mutations have no onError at all.
+
+## Problems (16)
+
+- **[high]** `client/src/app/[locale]/(private-routes)/nurse/page.tsx` — The nurse dashboard — the landing page of the whole workspace — is a PlaceholderScreen with generic 'placeholder_body' copy. A nurse running their business here gets no today's visits, no pending-request count, no earnings snapshot, no verification/setup status, even though every one of those hooks already exists (useNurseRequestInbox, useNurseEarningsBalance, useVerificationStatus, useMyVariants, useServiceAreas, useNurseBankAccounts).
+ - evidence: line 7: `return ;`
+- **[high]** `client/src/app/[locale]/(private-routes)/nurse/requests/page.tsx` — The requests inbox has no error state: it destructures only { data, isLoading }, so a failed useNurseRequestInbox query renders the 'no incoming requests' empty state. A nurse can silently miss paid work while requests are actually pending — with per-request response deadlines ticking. Income-critical false negative.
+ - evidence: line 19 `const { data, isLoading } = useNurseRequestInbox();` + lines 33–45: only `isLoading ? skeleton : items.length === 0 ? empty : list`
+- **[high]** `client/src/app/[locale]/(private-routes)/nurse/bank/page.tsx` — Once an account is verified there is no way to add another bank account or change IBAN: the form only appears when accounts.length === 0 or after a mismatch re-enter (setShowForm(true) exists only in the mismatch branch). Yet 'make primary' (line 85) implies multiple accounts are supported. A nurse who switches banks is dead-ended on the money path. Additionally, a failed useNurseBankAccounts query renders the 'no account yet' empty state + open form, inviting a duplicate IBAN submission.
+ - evidence: line 31 `const showFormNow = !isLoading && (accounts.length === 0 || showForm);` — showForm set only at line 81 `onReenter={status === 'mismatch' ? () => setShowForm(true) : undefined}`
+- **[high]** `client/src/app/[locale]/(private-routes)/nurse/services/PublishGate.tsx` — The 'publish' primary CTA is a no-op that fires a success snackbar — nothing is published, but the UI claims completion. On a trust-first platform a fake success on the go-live action is a product-integrity bug, and the panel occupies prime space on every visit to the services list even when approved.
+ - evidence: line 64: `onClick={() => enqueueSnackbar(t('publish_done'), { variant: 'success' })}`
+- **[high]** `client/src/app/[locale]/(private-routes)/nurse/profile/page.tsx` — Profile save and avatar upload fail silently: both mutations pass only onSuccess (lines 44, 63), and the hooks (services/profiles/hooks/useUpsertNurseProfile.ts, useUploadAvatar.ts) define no onError — a failed save shows nothing, and a nurse may leave believing their trust-critical profile is saved. Also the uploaded avatar is only staged in local state; leaving without pressing 'save' discards it with no warning.
+ - evidence: line 44 `uploadAvatar.mutate(file, { onSuccess: ... })` and lines 54–64 `upsert.mutate(..., { onSuccess: () => enqueueSnackbar(...) })` — no onError anywhere
+- **[medium]** `client/src/app/[locale]/(private-routes)/nurse/services/MyServicesList.tsx` — Same error→false-empty pattern: a failed useMyVariants query renders the 'create your first service' empty state (isEmpty = !isLoading && variants.length === 0), telling an established nurse their offerings are gone / never existed.
+ - evidence: lines 37, 41–42: `const { data, isLoading } = useMyVariants(); ... const isEmpty = !isLoading && variants.length === 0;`
+- **[medium]** `client/src/app/[locale]/(private-routes)/nurse/services/VariantBuilder.tsx` — Step 2 treats a failed option-groups query as 'this category needs no options' (only isLoading and groups.length === 0 are handled), letting the nurse advance and submit a variant missing required option groups, which then dies with a generic create_error toast. The categories step handles isError with retry (lines 348–356) but the options step does not.
+ - evidence: lines 384–389: `optionGroupsQuery.isLoading ? : groups.length === 0 ? t('options_none') : ...` — no isError branch
+- **[medium]** `client/src/app/[locale]/(private-routes)/nurse/coverage/page.tsx` — The whole-city choice is encoded twice and contradicts itself: the scope ToggleButtonGroup (lines 198–210) selects whole-city vs districts, but when 'specific districts' is chosen, CascadingRegionSelect's district dropdown still offers its own 'whole city' empty MenuItem (CascadingRegionSelect.tsx line 152) — picking it then trips the 'district required' error on add. Also removeArea.mutate has no onError (lines 124–126): a failed removal leaves the chip with no feedback.
+ - evidence: coverage handleAdd line 89: `const districtInvalid = effectiveScope === 'districts' && region.districtId == null;` vs CascadingRegionSelect.tsx line 152: `{t('whole_city')} `
+- **[medium]** `client/src/layout/config.ts` — Sidebar anchoring is physical, not logical, and inconsistent per breakpoint: desktop drawer anchors 'left' while mobile anchors 'right', regardless of locale — in the default fa/RTL app the desktop nav sits on the trailing edge (unconventional for RTL) and switches sides between mobile and desktop. TopBarAndSideBarLayout offsets content with physical paddingLeft/paddingRight keyed to the anchor string (lines 53–60). Leftover starter comments ('// 'right';') confirm this was never decided for RTL.
+ - evidence: lines 8–9: `export const SIDE_BAR_MOBILE_ANCHOR = 'right'; // 'right';` / `export const SIDE_BAR_DESKTOP_ANCHOR = 'left'; // 'right';`
+- **[medium]** `client/src/components/common/AppButton/AppButton.tsx` — Starter-grade AppButton ships a default 8px margin on all sides (DEFAULT_SX_VALUES = { margin: 1 }), which every nurse-workspace call site individually fights with sx={{ m: 0 }} (profile, coverage, bank, services, builder — ~15 occurrences). Any forgotten override yields off-grid spacing; spacing should come from layout gaps, not the button.
+ - evidence: lines 9–11: `const DEFAULT_SX_VALUES = { margin: 1, ... }`
+- **[medium]** `client/src/app/[locale]/(private-routes)/nurse/profile/page.tsx` — Education level/field and specializations — trust-relevant credentials in a healthcare marketplace — exist in the data model but are not editable anywhere: the form silently round-trips initial values, so nurses can never present their qualifications. The page even shows a 'deferred_services' caption admitting the gap.
+ - evidence: lines 58–60: `educationLevel: initial?.educationLevel ?? '', educationField: initial?.educationField ?? '', specializationsJson: initial?.specializationsJson ?? '[]'`
+- **[medium]** `client/src/layout/TopBarAndSideBarLayout.tsx` — The content area gets a flat 8px gutter (paddingLeft/Right/Top: 1) and no container system, while every nurse page sets its own maxWidth (560 on profile/coverage/bank/builder, 640 on services list, none on earnings) and start-aligns — so on desktop the workspace reads as a narrow form column stuck in the corner of a mostly-empty page. Classic starter-dashboard composition, not a designed workspace.
+ - evidence: line 102: `sx={{ flexGrow: 1, justifyContent: 'space-between', paddingLeft: 1, paddingRight: 1, paddingTop: 1 }}` vs MyServicesList.tsx line 73 `maxWidth: 640` and coverage/page.tsx line 130 `maxWidth: 560`
+- **[low]** `client/src/app/[locale]/(private-routes)/nurse/services/VariantBuilder.tsx` — Option values render in a ToggleButtonGroup forced to wrap (sx={{ flexWrap: 'wrap' }}); MUI's grouped-button styling (collapsed borders/negative margins, first/last corner rounding) is designed for a single row, so wrapped rows show missing side borders and squared corners on mid-row buttons once a group has many values.
+ - evidence: line 424: `sx={{ flexWrap: 'wrap' }}` on ToggleButtonGroup
+- **[low]** `client/src/app/[locale]/(private-routes)/nurse/services/VariantBuilder.tsx` — The duplicate-listing warning sets body text color to var(--bal-warning) (amber) on the paper background — likely failing WCAG contrast in light mode; warning tokens elsewhere are used as chip backgrounds with a dedicated -contrast foreground. The price field also shows raw ungrouped digits (up to 12) while typing — a mis-typed extra zero is a 10× price error; grouping only appears in the estimate panel below.
+ - evidence: line 263: ``
+- **[low]** `client/src/components/PlaceholderScreen/PlaceholderScreen.tsx` — The single sparing terracotta accent (--bal-secondary) is spent on placeholder icons for unfinished screens — the brand accent's most prominent appearance in the nurse workspace is on an empty page, inverting its purpose.
+ - evidence: line 24: ` `
+- **[low]** `client/src/components/StepperHeader/StepperHeader.tsx` — The builder's progress header is a bare default MUI Stepper (default connector, default dot/check icons) — the most default-MUI element in the nurse flow, on the screen a nurse uses to define their core business offering. Page headers likewise are hand-repeated h5+body2 blocks on all five pages with no shared PageHeader component.
+ - evidence: lines 21–27: unstyled ``
+
+## Opportunities (8)
+
+- **Build the real nurse dashboard ('Today') — pure assembly, all data hooks exist** (impact: high, effort: medium) — Replace the placeholder with a working day-runner: (1) greeting header with name + TrustBadge; (2) 'needs your response' strip — pending requests from useNurseRequestInbox with per-card CountdownTimer, the single most time-critical thing a nurse can miss; (3) today's visits from the bookings/sessions queries with a check-in CTA (EVV lives at /nurse/visits already); (4) an earnings snapshot card from useNurseEarningsBalance (net payable + next weekly payout day) deep-linking to /nurse/earnings; (5) a verification/publish status card when not yet approved. Every widget is a read of an already-cached query — no new services needed.
+- **'Go live' setup checklist replacing the scattered warnings** (impact: high, effort: medium) — Verification banner (profile), PublishGate (services), empty-coverage warning (coverage), and bank empty state are four disconnected nags for one journey. Build one activation tracker — verified ✓, profile complete ✓, ≥1 active service ✓, ≥1 coverage area ✓, verified primary IBAN ✓ — with a progress meter, shown on the dashboard until complete and linked from each page's banner. This converts anxiety ('why am I not bookable?') into a guided funnel and directly drives supply-side activation.
+- **Public-profile preview: 'how families see you'** (impact: high, effort: small) — Nurses can't see their own listing as customers do. Add a preview mode composing the existing C3 public-profile pieces (avatar, TrustBadge, bio, ServicePriceRow list, coverage chips) from the nurse's own data, reachable from profile and services pages. On a trust-first marketplace this is both a confidence tool and the strongest motivator to complete bio/photo/credentials.
+- **Shared query error/empty boundary to kill the error→false-empty pattern** (impact: high, effort: small) — Earnings already has local ErrorPanel/EmptyPanel/retry (nurse/earnings/page.tsx lines 153–181). Extract them into shared components (or a small QueryStateGate wrapper) and apply across requests inbox, services list, bank, coverage, and the builder's options step. One small primitive fixes five misleading states at once and standardizes the retry affordance.
+- **Variant builder: live listing preview + smarter duplicate handling** (impact: medium, effort: small) — In step 3, render the actual VariantCard as a live 'this is what appears in search' preview (name, category, PriceDisplay) instead of only the price paper — the nurse is composing a listing, show the listing. On a 409 duplicate, offer 'edit the existing listing' (the list already knows it) rather than a dead-end warning. Consider chips instead of wrapped ToggleButtonGroups for option values, which also fixes the grouped-border artifact.
+- **Coverage map visualization** (impact: medium, effort: medium) — Coverage is text chips only, yet AddressMapPicker/Neshan tiles already exist in components/geography. Rendering covered city/district shapes (or even pins) on a small map makes 'where will I appear in search' tangible and catches mistakes (wrong city, forgotten district) instantly. Also collapse the double whole-city affordance: let the district dropdown's 'whole city' option BE the choice and drop the separate scope toggle.
+- **Mobile bottom navigation for the daily loop** (impact: medium, effort: medium) — Nurses on shift are on phones; today the 10-item nav hides behind a hamburger in a drawer that opens from the opposite side than on desktop. Give the nurse app a 4–5 tab bottom bar (Today, Requests, Visits, Earnings, More) and demote setup pages (profile/services/coverage/bank/verification) to the 'More' sheet — daily ops one thumb-tap away.
+- **Bank: add-account and change-IBAN flow** (impact: medium, effort: small) — Beyond the missing 'add another account' button, design the state properly: an 'accounts' section with a persistent add CTA, the pending poll surfaced as an explicit 'we are checking ownership, usually takes X' timeline, and a guarded flow for replacing the primary IBAN (new account → verify → make primary → optionally remove old). This is the nurse's paycheck; it should feel like a bank settings page, not a one-shot form.
+
+## Keep (do not regress)
+
+- Token discipline is genuinely excellent: zero hard-coded hexes anywhere in the nurse tree (grep-verified); all color flows through --bal-* semantic tokens with -contrast pairs (StatusChip, TrustBadge, BankStatusPanel, PublishGate), so dark mode switches for free.
+- RTL-safe logical properties for status accents — borderInlineStartWidth/borderInlineStartColor on every banner/panel (profile banner, coverage warning, BankStatusPanel, PublishGate, duplicate warning) — and LTR-pinned numeric inputs (IBAN, years, price) with textAlign:'start'.
+- Money correctness in the UI: PriceDisplay computes totals integer-safe via BigInt, never shows a total from rate alone, Toman entry converts to IRR at the field boundary, and the builder shows a live grouped-Toman estimate panel.
+- BankStatusPanel's three-state design (pending/verified/mismatch) with semantic accent edge, masked dir="ltr" IBAN, non-accusatory mismatch copy, and re-enter as the only action — exactly right for a money-trust surface.
+- TrustBadge honesty-by-construction (verified only when the aggregate is approved; expired visually distinct from never-verified) and its reuse from own-profile to search results.
+- Two-stage disclosure honored in the requests inbox (notes preview only, never address/clinical data) plus server-frozen CountdownTimer — the privacy model is visible in the UI code.
+- Soft deactivate semantics on VariantCard: no delete affordance at all, confirm dialog only for the destructive direction, instant reactivate, dimmed + neutral chip + 'can't be booked' hint on inactive rows.
+- Edit mode of the variant builder correctly locks identity fields (category + option set) with an explanatory caption — EAV identity semantics surfaced honestly instead of letting an edit silently create a different listing.
+- CascadingRegionSelect is production-grade: cached geo queries, out-of-range prefill guard against MUI Select warnings, per-level loading adornments, whole-city-only cities force the right affordance instead of dead-ending.
+- Duplicate coverage handling both belt (client-side areaExists pre-check) and braces (server 409 mapped to the same inline message).
+- Empty states with dashed border + icon + CTA (services list, bank, requests) and rounded skeletons on most lists — the vocabulary exists; it just needs error-state siblings.
+- Locale-aware digits everywhere (Intl fa-IR for counts/pagers) and Shamsi dates in the inbox.
diff --git a/dev/post-phase/ui/audit/shell-and-navigation.md b/dev/post-phase/ui/audit/shell-and-navigation.md
new file mode 100644
index 0000000..f45a689
--- /dev/null
+++ b/dev/post-phase/ui/audit/shell-and-navigation.md
@@ -0,0 +1,75 @@
+# App shell — header, sidebar, bottom bar, layouts, navigation
+
+## Current state
+
+The shell is a two-tier system. The root layout (client/src/app/[locale]/layout.tsx) is genuinely well-built: it owns , conditionally loads the Mikhak Persian font only on fa routes, seeds the color scheme from a cookie (no flash), and pairs direction-keyed themes (APP_THEME_RTL/LTR) with a stylis-plugin-rtl Emotion cache. Below it, route groups map to per-actor shells: (public-routes) → PublicLayout, (private-routes) → useSessionRoleSync + a pass-through PrivateLayout, then (customer) → RoleGuard+CustomerLayout, nurse/ → NurseLayout, admin/ → AdminLayout, partner/ → PartnerLayout. RoleGuard (client/src/components/auth/RoleGuard.tsx) is solid: brand splash while /me resolves, explicit error recovery, toast+redirect on role mismatch.
+
+The chrome itself is split. CustomerLayout (client/src/layout/CustomerLayout.tsx) is bespoke and closest to right: fixed TopBar (static "اپلیکیشن خانواده" title, support icon start, NotificationBell + dark toggle end), an 800px reading column with inner scroll, and a 5-tab BottomBar (Home/Bookings/Patients/Wallet/Profile). Everything else — NurseLayout (10 flat sidebar items), AdminLayout (12 capability-gated items via useAdminCapabilities), PartnerLayout (4 items), and PublicLayout (zero items) — reuses TopBarAndSideBarLayout.tsx, which is the untouched react-starter-kit engine: a stock MUI AppBar with a centered static app-label title, a 240px Drawer (persistent on desktop, temporary on mobile) whose toggle button is the starter's multicolor Twemoji PENCIL icon registered as `logo`, a SideBar containing a永-placeholder UserInfo card ("Current User" / "Loading..."), a default ListItemButton nav list, a dark-mode switch, and a logout icon. Nav items are defined inline in each *Layout.tsx via translated LinkToPage arrays; ROUTES constants live in client/src/constants/routes.ts.
+
+The "old MUI starter" complaint has a precise root cause: client/src/theme/theme.ts contains palette, typography, and shape only — there is not a single `components` override in the theme, so the AppBar, Drawer, ListItemButton selected state, Toolbar, and BottomNavigation all render stock MUI. On top of that, sidebar navigation is functionally degraded: SideBarNavItem compares unprefixed paths against the locale-prefixed pathname so the active item never highlights, links push unprefixed hrefs through raw next/link (middleware redirect hop), and the SSR mobile-first useIsMobile causes a 240px desktop layout jump after hydration. There is no brand mark, no page-title wayfinding, no back affordance, no locale switcher, and no way for a dual customer+nurse account to switch apps.
+
+## Problems (20)
+
+- **[high]** `client/src/components/common/AppIcon/config.ts` — The brand 'logo' is the starter kit's multicolor emoji-style pencil (client/src/components/common/AppIcon/icons/PencilIcon.tsx with hard-coded fills #EA596E, #FFCC4D, #D99E82). It is the brand mark on the auth screens (BrandMark.tsx:21 passes color="var(--bal-primary)" which is ignored because every path has its own fill) and the sidebar-toggle button in every sidebar shell (TopBarAndSideBarLayout.tsx:68-75). A trust-first healthcare product presents a cartoon pencil as its identity.
+ - evidence: config.ts:115 `logo: PencilIcon`; PencilIcon.tsx `fill="#EA596E"` / `fill="#FFCC4D"`
+- **[high]** `client/src/theme/theme.ts` — createAppTheme defines only cssVariables/colorSchemes/typography/shape/direction — zero `components` overrides. Every piece of chrome (AppBar shadow+solid primary, Drawer paper, grey ListItemButton selected state, BottomNavigation, Toolbar density) is stock MUI. This is the structural root cause of the 'default-MUI starter' look; no amount of per-layout tweaking fixes it without a theme components pass.
+ - evidence: theme.ts:20-35 — createTheme call has no `components` key
+- **[high]** `client/src/layout/components/SideBarNavItem.tsx` — Sidebar active-item highlight never triggers. `pathname` from next/navigation is locale-prefixed ('/fa/nurse/requests') while nav paths are unprefixed ('/nurse/requests'); defineRouting (client/src/i18n/routing.ts) uses the default localePrefix 'always', so startsWith always fails. Nurse, admin, and partner users get no 'where am I' signal in the sidebar. AppLink's activeClassName (AppLinkNextNavigation.tsx:95 `pathname == currentPath`) has the same bug.
+ - evidence: SideBarNavItem.tsx:28 `(path && path.length > 1 && pathname.startsWith(path))`
+- **[high]** `client/src/layout/components/SideBar.tsx` — The sidebar identity card renders a permanent placeholder: ` ` is never given a user, so every nurse/admin/partner sees an empty avatar, hard-coded English 'Current User', and an eternal 'Loading...' (UserInfo.tsx:34-36) — in the chrome of a product whose entire premise is verified identity. AuthState (context/auth/types.ts) has at least phone+roles, and profile services have name/avatar, but nothing is wired.
+ - evidence: SideBar.tsx:56 ` `; UserInfo.tsx:34 `{fullName || 'Current User'}`
+- **[high]** `client/src/layout/PublicLayout.tsx` — The login/first-impression screen carries starter junk: the TopBar title is the hard-coded English 'Unauthorized - Balinyaar' (line 9) shown even on the fa default locale; SIDE_BAR_ITEMS is [] yet the pencil button still opens a drawer containing only a dark-mode switch; and on mobile an EMPTY BottomBar (BOTTOM_BAR_ITEMS = [], line 19) renders as a bare elevated strip at the bottom of the login page (line 34).
+ - evidence: PublicLayout.tsx:9 `const TITLE_PUBLIC = 'Unauthorized - Balinyaar'`; line 34 `{bottomBarVisible && }`
+- **[high]** `client/src/hooks/layout.ts` — SERVER_SIDE_MOBILE_FIRST = true makes every desktop SSR paint the mobile shell first (no sidebar, 56px top bar), then TopBarAndSideBarLayout's stackStyles flip after hydration and content jumps 240px sideways on every nurse/admin/partner desktop load — visible CLS on the daily-driver backoffice screens.
+ - evidence: layout.ts:8 `SERVER_SIDE_MOBILE_FIRST = true`; TopBarAndSideBarLayout.tsx:49-63 paddings keyed on `onMobile`/`sidebarProps`
+- **[medium]** `client/src/layout/components/SideBarNavItem.tsx` — Sidebar links navigate via raw next/link with unprefixed paths (`to={path}`), forcing a middleware redirect hop on every click and risking a locale flip for /en users (cookie/accept-language re-detection). BottomBar and NotificationBell manually prefix with `/${locale}` instead. Three different locale-handling strategies exist in the chrome and there is no next-intl createNavigation wrapper.
+ - evidence: SideBarNavItem.tsx:34 `to={path}` vs BottomBar.tsx:14 `withLocale(locale, path)`
+- **[medium]** `client/src/layout/components/TopBar.tsx` — The header is a wasted, static surface: a centered app-label title ('نمای پرستار', 'اپلیکیشن خانواده') with whiteSpace:'nowrap' (overflow risk between icon groups on small screens), no page title, no breadcrumbs, no user identity/avatar, plus leftover starter comment '// boxShadow: none // Uncomment to hide shadow'. Users get zero wayfinding from the chrome on every screen.
+ - evidence: TopBar.tsx:28-38 centered nowrap Typography; line 20 commented starter code
+- **[medium]** `client/src/layout/AdminLayout.tsx` — Admin and Partner shells pass no headerActions: no notification bell (admin notifications is only a buried 12th sidebar item; partner has none at all), no admin identity or fine-grained-role chip in the header, no environment indicator. For a backoffice where a 'finance' vs 'support' admin see different consoles, the chrome never says who you are.
+ - evidence: AdminLayout.tsx:39-44 and PartnerLayout.tsx:29-36 — TopBarAndSideBarLayout called without headerActions
+- **[medium]** `client/src/layout/NurseLayout.tsx` — The nurse sidebar is a flat, ungrouped list of 10 items (dashboard, requests, profile, services, coverage, bank, verification, visits, earnings, support) in default ListItemText styling — no sections separating daily work (requests/visits) from setup (services/coverage/bank/verification) from money, and no verification-status cue in nav even though an unverified nurse's single most important task is finishing verification.
+ - evidence: NurseLayout.tsx:19-33 — one flat useMemo array
+- **[medium]** `client/src/layout/components/BottomBar.tsx` — No iOS safe-area handling — the Paper/BottomNavigation has no env(safe-area-inset-bottom) padding, so on iPhones the home indicator overlaps the tab labels of the customer app's primary navigation. The customer shell is explicitly the mobile-first primary experience.
+ - evidence: BottomBar.tsx:51-62 — sx only sets borderTop; no safe-area padding anywhere in globals.css either
+- **[medium]** `client/src/layout/CustomerLayout.tsx` — The shell offers no back affordance or contextual title for detail screens (nurse profile, booking detail, checkout): the TopBar startNode is always the support icon and the title is always 'Family app'. Only one page in the whole app (bookings/[id]/review/page.tsx:98) renders a back button, and there is no 'back'/'arrow' icon in the AppIcon registry at all — mobile users must rely on browser chrome mid-funnel.
+ - evidence: CustomerLayout.tsx:45-61 static TopBar; AppIcon/config.ts has no back/arrow icon; only review/page.tsx uses router.back()
+- **[medium]** `client/src/layout/components/SideBar.tsx` — The drawer-close handler is attached to the whole content Stack (onClick={handleAfterLinkClick} on line 52), so on mobile ANY tap inside the temporary drawer closes it — including toggling the dark-mode switch or a mis-tap on the divider, not just nav-link clicks.
+ - evidence: SideBar.tsx:49-53 `` wrapping UserInfo, nav list, and DarkModeFormSwitch
+- **[medium]** `client/src/layout/CustomerLayout.tsx` — The BottomBar renders unconditionally, so desktop customers get a full-width mobile tab bar pinned to the bottom of a wide viewport with a lone 800px column above it — a 'phone app stretched to desktop' effect with no desktop nav alternative (BOTTOM_BAR_DESKTOP_VISIBLE in config.ts is dead — only PublicLayout reads it).
+ - evidence: CustomerLayout.tsx:81 ` ` with no breakpoint gate
+- **[medium]** `client/src/layout/TopBarAndSideBarLayout.tsx` — Chrome strings hard-coded in English on a Persian-default product: 'Open Sidebar' tooltip (line 71), 'Logout Current User' (SideBar.tsx:76), 'Current User'/'Loading...' (UserInfo.tsx). Every other shell string goes through next-intl; these leak English into fa tooltips/labels.
+ - evidence: TopBarAndSideBarLayout.tsx:71 `title={sidebarProps.open ? undefined : 'Open Sidebar'}`
+- **[low]** `client/src/layout/TopBarAndSideBarLayout.tsx` — RTL correctness rests on a fragile double-flip coincidence: content offset uses physical paddingLeft/paddingRight keyed to the physical anchor string (lines 53-60), which only aligns with the drawer because MUI flips Drawer anchor under theme.direction='rtl' AND stylis-plugin-rtl flips the generated padding CSS. Any future inline style, non-Emotion CSS, or plugin removal silently breaks the fa desktop layout. Logical properties (marginInlineStart / paddingInlineStart) would make it robust.
+ - evidence: TopBarAndSideBarLayout.tsx:53-60 `paddingLeft: … anchor?.includes('left') ? SIDE_BAR_WIDTH : undefined`
+- **[low]** `client/src/layout/config.ts` — Starter residue: commented-out alternates ('right'; // 'right';) on the anchor constants and the dead BOTTOM_BAR_DESKTOP_VISIBLE=false // true; flag — config that documents the starter's indecision rather than Balinyaar's design.
+ - evidence: config.ts:8-9, 21
+- **[low]** `client/src/components/UserInfo/UserInfo.tsx` — Starter-typed component: `user?: any`, name/email fallback logic for a phone-OTP product with Persian names, 64px avatar with fontSize '3rem' initials. Needs replacing, not patching, when the sidebar identity card is wired to real profile data.
+ - evidence: UserInfo.tsx:6 `user?: any`; line 18 `user?.phone || (user?.email as string)`
+- **[low]** `client/src/app/globals.css` — Starter CSS reset sets max-height:100vh + overflow-x:hidden on html/body while the app runs two different scroll models (window scroll in TopBarAndSideBarLayout, inner overflowY:auto in CustomerLayout). The inner-scroll model also defeats Next.js scroll restoration — back-navigating from a nurse profile to search results loses the list position.
+ - evidence: globals.css `max-height: 100vh` on html,body; CustomerLayout.tsx:68 `overflowY: 'auto'` on main
+- **[low]** `client/src/layout/CustomerLayout.tsx` — No locale switcher exists anywhere in any shell (fa/en are both shipped), and a dual customer+nurse session — explicitly supported by RoleGuard ('passes either shell's guard and can move freely') — has no UI anywhere to switch between the family app and the nurse view, including the profile hub page.
+ - evidence: grep for LocaleSwitch/setLocale returns nothing; resolveRoleDestination used only in auth guards; (customer)/profile/page.tsx has no /nurse link
+
+## Opportunities (10)
+
+- **Design a real brand mark and put identity into the chrome** (impact: high, effort: medium) — Replace the Twemoji pencil with a proper Balinyaar logomark (teal/cream SVG that respects currentColor so dark mode works), register it as `logo` in AppIcon, and add a brand lockup to the chrome: wordmark in the customer Home header, a compact brand header at the top of the nurse/admin/partner sidebars, and a proper favicon/manifest icon. Auth screens (BrandMark) fix themselves for free since they already reference icon="logo".
+- **One theme `components` pass to de-starter every shell at once** (impact: high, effort: medium) — Add component overrides in theme.ts using existing --bal-* tokens: AppBar → cream/paper surface with a hairline divider instead of solid-teal + default shadow (calm, clinical-warm); Drawer paper → bg-default with inset border; ListItemButton → rounded 'pill' selected state using --bal-primary-soft with teal text/icon; BottomNavigation → teal selected color, medium label weight; Toolbar → consistent gutters. Because all four shells render through these primitives, one file transforms the entire chrome without touching layout logic.
+- **Contextual customer header: page title + back on detail routes** (impact: high, effort: medium) — Turn the customer TopBar from a static 'Family app' label into a contextual header: brand lockup on the 5 root tabs, and (title + back chevron) on pushed routes (nurse profile, booking detail, checkout steps, ticket thread). Implement via a tiny header context or a route-segment→title map; add a direction-aware back icon to the AppIcon registry. This is the single biggest mobile-UX upgrade available — the whole booking funnel currently has no in-app way back.
+- **Nurse workspace shell with grouped nav and a real identity card** (impact: high, effort: medium) — Restructure the nurse sidebar into labeled sections — امروز (dashboard, requests, visits), حرفه من (services, coverage, verification), مالی (earnings, bank), پشتیبانی — with subheaders and dividers; replace the placeholder UserInfo with a real card: avatar, name, TrustBadge verification state (the existing f5 component), and a small 'complete your verification' progress affordance for unverified nurses. Verification status in the chrome directly serves the trust-first premise.
+- **Dense admin backoffice chrome** (impact: medium, effort: large) — Give /admin real ops-console chrome: sectioned sidebar (Trust: verification/reviews · Money: payouts/refunds · Support: tickets/alerts · System: config/holidays/audit/roles/partners), a page-title + breadcrumb bar under the AppBar, the admin's fine-grained role chip (super_admin/finance/…) and a bell in the header, denser list typography, and full-width content (drop the 8px-gutter Stack for a proper content frame). Keep useAdminCapabilities gating exactly as is.
+- **App switcher for dual-role users + locale switcher** (impact: high, effort: small) — Add a compact actor switcher ('نمای پرستار ⇄ اپلیکیشن خانواده') to the customer profile hub and the nurse sidebar for sessions holding both roles (roles already live in AuthContext), and a fa/en locale switcher in the sidebar/profile. Also gives nurses-who-are-also-family a discoverable path that currently does not exist at all.
+- **Unify locale-aware navigation on next-intl createNavigation** (impact: high, effort: small) — Create src/i18n/navigation.ts (createNavigation from next-intl) and route ALL chrome navigation through its Link/usePathname/useRouter. This simultaneously fixes the never-highlighting sidebar selected state, the middleware redirect hop, the manual `/${locale}` prefixing scattered across BottomBar/NotificationBell/RoleGuard, and future-proofs against localePrefix changes.
+- **Mobile-native polish for the customer shell** (impact: medium, effort: medium) — Safe-area padding (env(safe-area-inset-bottom)) on the BottomBar, a max-width phone-frame or top-nav variant for desktop customers instead of a full-width bottom tab bar, hide-on-scroll app bar for long lists, and a slim route-transition progress indicator. Consider making the search results list restore scroll on back (window-scroll model or manual restoration) since it is the discovery workhorse.
+- **Trust cues woven into the chrome itself** (impact: medium, effort: small) — Beyond screens: a persistent 'پرداخت امن نزد بالینیار' escrow microcopy chip in the wallet tab header, verified-nurse badge treatment wherever a nurse identity appears in chrome, and an always-reachable emergency/support affordance in the nurse visit context (the customer support icon exists — mirror the guarantee on the nurse side). In a healthcare marketplace the shell, not just content, should keep signaling safety.
+- **Fix desktop SSR flash with a CSS-first responsive shell** (impact: medium, effort: medium) — Replace the JS useIsMobile branching in the shells with CSS breakpoints (sx display/breakpoint props render both variants and let media queries pick), or read a UA hint server-side, so desktop first paint already includes the persistent sidebar and correct paddings — eliminating the 240px post-hydration jump on every nurse/admin/partner load.
+
+## Keep (do not regress)
+
+- The design-token architecture: client/src/theme/tokens.css (--bal-* custom properties, light + dark schemes keyed on data-mui-color-scheme) mirrored 1:1 by colors.ts BRAND/LIGHT_PALETTE/DARK_PALETTE — the palette itself (deep teal, sparing terracotta, cream) is on-brand and already dark-mode-complete.
+- The root [locale] layout: correct lang/dir per locale, conditional Mikhak font loading only on fa routes, cookie-seeded color scheme with no flash, direction-keyed theme pair + stylis-plugin-rtl Emotion cache — this RTL/theming foundation is better than most production apps and must not be regressed.
+- The per-actor shell architecture: separate CustomerLayout / NurseLayout / AdminLayout / PartnerLayout mapped 1:1 to route groups, with RoleGuard's resolved-vs-pending hydration (brand splash, explicit /me error recovery, mismatch redirect with toast — never the wrong shell as a stand-in). Restyle the chrome, keep this structure.
+- AdminLayout's capability-gated sidebar via useAdminCapabilities — nav items filtered by fine-grained role codes with server-side enforcement acknowledged in comments; exactly the right display-convenience pattern.
+- The customer 5-tab bottom-nav IA (Home/Bookings/Patients/Wallet/Profile) and BottomBar's implementation details: locale-prefixed navigation and longest-prefix active matching so /patients/123 still highlights the Patients tab — the one nav component that handles locale correctly.
+- Performance-conscious chrome composition: DarkModeToggleButton/DarkModeFormSwitch are the only useColorScheme subscribers and NotificationBell isolates the polling unread count, so theme flips and bell updates never re-render the shells.
+- CONTENT_MAX_WIDTH reading-column constraint (800px) for customer content, ErrorBoundary wrapping every shell's main content, and the RTL typography setup (Mikhak across headings+body for full Persian glyph coverage, button textTransform:'none').
diff --git a/dev/post-phase/ui/audit/theme-and-brand.md b/dev/post-phase/ui/audit/theme-and-brand.md
new file mode 100644
index 0000000..3888ba1
--- /dev/null
+++ b/dev/post-phase/ui/audit/theme-and-brand.md
@@ -0,0 +1,66 @@
+# Design tokens, theme, typography, brand execution
+
+## Current state
+
+The theme lives in client/src/theme/ as a deliberate two-layer token system: tokens.css defines ~30 `--bal-*` CSS variables (primary/secondary + light/dark/contrast, soft tints, surfaces, text, divider, and four semantic feedback pairs) keyed on `[data-mui-color-scheme='light'|'dark']`, mirrored by colors.ts (`BRAND`, `LIGHT_PALETTE`, `DARK_PALETTE`) which feeds a MUI v9 cssVariables theme built in theme.ts (`createAppTheme` produces APP_THEME_LTR/APP_THEME_RTL once at module load). ThemeProvider.tsx wires a direction-aware Emotion cache with stylis-plugin-rtl, CssBaseline enableColorScheme, and a cookie sync so the server (app/[locale]/layout.tsx + lib/cookies/server.ts getThemeMode) stamps `data-mui-color-scheme` on before first paint for returning visitors. Fonts are loaded per-locale in app/[locale]/layout.tsx: Mikhak (400/500/700 woff2 via next/font/local, preload:false) attached only on fa routes; typography.ts declares a Space Grotesk variable for EN that is explicitly "not currently wired to a font loader". Adoption of the tokens in feature code is exceptionally disciplined — 331 `var(--bal-*)` usages across 103 component/page files, with raw hexes existing only in theme/colors.ts, one starter SVG, and one test.
+
+What the theme does NOT do is the story: createTheme receives only palette, typography (family+weight only), `shape.borderRadius: 10`, and direction — there is zero `components` key anywhere in src (the only styleOverrides grep hit is ErrorBoundary's unrelated text). Every Button, Card, TextField, Chip, Dialog, Table, Tab, and Alert renders stock Material Design with recolored primaries: default elevations, default densities, default focus treatment, Roboto-tuned type metrics. The MUI palette also omits success/error/warning/info, so the 29 files using ``/`color="success"` show stock MUI greens/reds while toasts (lib/toast/NotistackProvider.tsx) use the brand-harmonized `--bal-*` feedback colors — two visibly different feedback systems. Persian typography inherits MUI's Latin defaults wholesale (rem scale, tight heading line-heights, non-zero letter-spacing that is wrong for joined Persian script), and the "logo" registered in components/common/AppIcon/config.ts is still the starter's multicolor Twemoji pencil SVG, rendered at 56px on the auth screens by components/auth/BrandMark.tsx and as the TopBar logo button. The brand seed deck the tokens were extracted from (product/balinyaar.html) is referenced in tokens.css and .claude/skills/frontend-designer/SKILL.md but no longer exists in the repo. Starter residue persists: dead legacy themes light.ts/dark.ts (built on the deprecated LTR-only TYPOGRAPHY export, exported from theme/index.ts including `LIGHT_THEME as default` — never imported by the app), a raw starter globals.css, and starter-era comments in components/config.ts.
+
+## Problems (15)
+
+- **[high]** `client/src/theme/theme.ts` — The MUI theme has NO `components` customization at all — createTheme gets only cssVariables/colorSchemes/typography/shape/direction. No styleOverrides or defaultProps for Button, Card, Paper, TextField, Chip, Dialog, Table, Tabs, Skeleton, etc., so every surface renders stock Material Design (default elevations/shadows, ripple, densities, focus states) with recolored primaries. This is the single biggest reason the app reads as a default-MUI starter instead of the calm/warm brand.
+ - evidence: Lines 19-36: the entire createTheme call — a repo-wide grep for `styleOverrides|defaultProps` finds only an unrelated hit in ErrorBoundary.tsx
+- **[high]** `client/src/components/common/AppIcon/config.ts` — The registered brand `logo` is still the starter's Twemoji multicolor pencil SVG (icons/PencilIcon.tsx with hard-coded fills #D99E82/#EA596E/#FFCC4D/#CCD6DD). It is the logo button in the TopBar shell and is rendered at 56px on every auth screen by components/auth/BrandMark.tsx, where the passed `color="var(--bal-primary)"` prop is silently ignored by the hard-coded fills. The seed-deck logo (deep-teal square, cream glyph, terracotta dot) was never implemented — first-impression brand execution is a cartoon pencil.
+ - evidence: config.ts line 115: `logo: PencilIcon,`; BrandMark.tsx line 21: ` `
+- **[high]** `client/src/theme/colors.ts` — LIGHT_PALETTE/DARK_PALETTE never define success/error/warning/info, so all MUI severity surfaces (Alert, Chip color=success, etc. — used in 29 files including checkout, refund status, verification) render stock MUI #2e7d32 green / #d32f2f red, while notistack toasts use the brand-harmonized --bal-success #1f6b50 / --bal-error #a8392a. The same semantic state shows two different color systems depending on whether it arrives as a toast or an inline alert — directly against the skill rule 'Need success/error/warning/info → use --bal-* tokens, not MUI defaults'.
+ - evidence: colors.ts lines 29-75 define only primary/secondary/background/text/divider; AppAlert.tsx defaults `severity='error' variant='filled'` to stock MUI error red
+- **[high]** `client/src/theme/typography.ts` — TYPOGRAPHY_RTL sets only fontFamily and weights — the entire Persian type scale is MUI's Roboto-tuned Latin defaults: rem sizes, tight heading line-heights (e.g. h4 1.235) that clip Persian ascenders/descenders, and non-zero letterSpacing on body1/body2/button/caption/overline, which is typographically wrong for joined (cursive) Persian script — letter-spacing visually breaks glyph connections in Mikhak. There is no fa-specific size, line-height, or letter-spacing tuning anywhere, and no responsive heading sizes.
+ - evidence: Lines 43-52: TYPOGRAPHY_RTL contains only fontFamily + fontWeight per variant; no fontSize/lineHeight/letterSpacing overrides exist
+- **[medium]** `client/src/theme/typography.ts` — The theme requests fontWeight 600 (h6, button) but the Mikhak loader in app/[locale]/layout.tsx ships only 400/500/700 — CSS font-matching resolves 600 upward to 700, so all intended 'semibold' text renders full Bold in Persian. The same 600-vs-loaded-weights mismatch is repeated in ~68 `fontWeight: 600` sx usages across 42 component/page files, collapsing the weight hierarchy to regular-vs-bold; the loaded Medium 500 is barely used.
+ - evidence: typography.ts lines 38/51 (`fontWeight: 600`), layout.tsx lines 40-44 (weights 400/500/700 only); grep `fontWeight: 600` = 68 hits in 42 files
+- **[medium]** `client/src/theme/typography.ts` — The English brand font is vaporware: BRAND_FONT_VARIABLE_EN '--font-space-grotesk' is declared with a comment admitting it is 'Not currently wired to a font loader; the LTR stack falls back to the system fonts'. /en pages have no brand typeface at all — headings render in Segoe UI/Roboto, so the secondary locale has zero typographic identity.
+ - evidence: Lines 3-5 comment + line 22 DISPLAY_FONT_LTR referencing the never-populated variable
+- **[medium]** `client/src/theme/tokens.css` — The token system covers colors only. There are no spacing-scale tokens, no radius steps beyond the single shape.borderRadius:10, no elevation/shadow tokens (all shadows are MUI's default neutral-black stack — cold and grey against the warm cream surfaces), no motion/duration/easing tokens, and no focus-ring token. Focus styling exists exactly once in the whole app, hand-rolled in NurseResultCard.tsx (`'&:focus-visible': { outline: '2px solid var(--bal-primary)' }`) — keyboard focus everywhere else is MUI's faint default, a real accessibility + polish gap.
+ - evidence: tokens.css lines 20-99 define only color variables; grep `focus-visible|outline:` yields a single hit at NurseResultCard.tsx:67
+- **[medium]** `client/src/theme/index.ts` — Dead starter theme code is still exported as the public API: light.ts/dark.ts build legacy ThemeOptions on the deprecated LTR-only TYPOGRAPHY (system font, no colorSchemes, no cssVariables, no direction) and index.ts exports them plus `LIGHT_THEME as default` — importing the package default yields a broken, unbranded theme. Only ThemeProvider and getDirection are actually consumed (single import in app/[locale]/layout.tsx). Violates the repo's own no-dead-code rule and is a trap for future contributors.
+ - evidence: index.ts lines 8-17 export APP_THEME/LIGHT_THEME/DARK_THEME/`LIGHT_THEME as default`; grep shows the only consumer imports `{ ThemeProvider, getDirection }`
+- **[medium]** `client/src/lib/cookies/server.ts` — First-visit dark-mode flash: when no color-scheme cookie exists, getThemeMode returns `colorScheme: 'light'`, so SSR stamps data-mui-color-scheme="light" on ; an OS-dark first-time visitor paints the full light theme, then MUI flips to dark client-side (no InitColorSchemeScript/inline pre-paint script). On a cream-vs-deep-teal palette this flash is stark.
+ - evidence: Line 46: `return { colorScheme: 'light', defaultMode: 'system' };` combined with layout.tsx line 89 `data-mui-color-scheme={colorScheme}`
+- **[medium]** `client/src/theme/tokens.css` — The brand source of truth is missing: tokens.css says the palette was 'extracted from the seed-deck proposal (balinyaar.html)' and the frontend-designer skill describes the logo from 'product/balinyaar.html', but that file does not exist anywhere in the repo — the intended identity (logo lockup, imagery, tone) is unrecoverable from the codebase, so gaps between deck and implementation cannot even be checked.
+ - evidence: tokens.css line 4 references balinyaar.html; Glob `**/balinyaar*.html` across the repo returns no files
+- **[low]** `client/src/app/globals.css` — globals.css is an untouched starter reset with hazards: `max-width: 100vw; overflow-x: hidden; max-height: 100vh` on html/body (100vw invites scrollbar-width overflow that the hidden overflow then silently masks; max-height:100vh is fragile on mobile browsers vs dvh and makes body scrolling work only by accident) plus a global `a { color: inherit; text-decoration: none }` that strips native link affordance app-wide. Nothing brandful (selection color, scrollbar, focus) lives here.
+ - evidence: Lines 7-12 and 14-17 — the entire 17-line file
+- **[low]** `client/src/app/[locale]/layout.tsx` — Brand metadata is placeholder-grade and not locale-aware: a single static title 'Balinyaar | بالینیار' and description 'Balinyaar web application' serve both /fa and /en (no generateMetadata per locale), and viewport.themeColor is hard-coded to light teal #1d4a40 with no dark-scheme media entry, so dark-mode users get a light-teal browser chrome over a #0f1c19 page.
+ - evidence: Lines 50-58: `themeColor: BRAND.teal` and `description: 'Balinyaar web application'`
+- **[low]** `client/src/components/config.ts` — Starter residue in the component-defaults file: the comment `CONTENT_MIN_WIDTH = 320; // CONTENT_MAX_WIDTH - Sidebar width` is factually wrong (800 − 240 = 560) and the whole file keeps the starter's commented-out-alternatives style ('error' // 'error' | 'info' ...), signaling copy-paste config rather than owned design decisions.
+ - evidence: Line 5 comment; lines 10-35
+- **[low]** `client/src/components/common/AppIcon/config.ts` — The icon registry (~90 icons) mixes filled and outlined Material styles with no system: legacy starter entries are filled (Home, Settings, Star, AccountCircle, Dashboard, CheckCircle, Cancel, MedicalServices...) while everything added later is deliberately Outlined — adjacent nav/status icons visibly differ in visual weight, reinforcing the default-MUI feel the owner already dislikes. No custom/brand icon set exists (the only custom SVG is the Twemoji pencil).
+ - evidence: Lines 4-33 filled imports vs lines 35-98 `*Outlined` imports registered side-by-side in ICONS
+- **[low]** `client/src/layout/TopBarAndSideBarLayout.tsx` — Physical direction props in the shell (`paddingLeft`/`paddingRight` gated on `anchor.includes('left')`) instead of logical padding — currently rescued at runtime by the stylis-plugin-rtl Emotion cache flipping them, but it contradicts the skill's own RTL rule and couples shell correctness to the RTL cache implementation.
+ - evidence: Lines 53-60 and line 102
+
+## Opportunities (10)
+
+- **A single MUI components theming pass — the highest-leverage move in the whole app** (impact: high, effort: large) — Add a `components` block to createAppTheme encoding the brand once: Button (disableElevation, weight, comfortable padding), Paper/Card (hairline divider border + soft teal-tinted shadow instead of grey elevation), TextField (calmer outline, cream-tinted filled variant), Chip (soft --bal-primary-soft/secondary-soft fills), Dialog/Drawer (radius 16, cream surfaces), Tabs (thicker indicator), Table (relaxed density, tinted header), Skeleton (warm tint), Alert (severity colors mapped to --bal-* tokens). Every screen upgrades simultaneously with zero per-page edits — this is the escape hatch from the 'default-MUI starter' look.
+- **Design a real Persian type scale** (impact: high, effort: medium) — Replace inherited Roboto metrics in TYPOGRAPHY_RTL with an owned fa scale: letterSpacing: 0 on every variant (joined script), body line-height ≥ 1.7 and heading line-heights ~1.4-1.5 for Persian ascender/descender room, explicit responsive heading sizes (MUI's default 6rem h1 is unusable, which is why no page uses h1/h2 today — grep confirms zero usages), and a weight system built on the actually-loaded 400/500/700 (retire the phantom 600). Wire Space Grotesk via next/font for /en so the secondary locale gets its brand voice.
+- **Implement the real brand mark and kill the pencil** (impact: high, effort: medium) — Build the seed-deck logo (deep-teal rounded square, cream lowercase glyph, single terracotta dot) as a theme-aware SVG component with sizes for TopBar, auth lockup (BrandMark), favicon, and webmanifest/PWA icons. Since product/balinyaar.html is gone, first re-establish the brand source of truth as a product/brand.md (palette, logo construction, tone words, do/don'ts) so design decisions stop living only in a skill file.
+- **Unify semantic feedback into the MUI palette** (impact: high, effort: small) — Add success/error/warning/info (from the existing --bal-* values) into LIGHT_PALETTE/DARK_PALETTE so Alert, Chip, Badge, LinearProgress color props are automatically brand-harmonized, then delete per-component semantic styling. One feedback language across toasts, inline alerts, and status chips — important in a product where refund/verification/payment states are the emotional core.
+- **Codify a trust-signal design language at token level** (impact: high, effort: medium) — Trust IS the product, yet 'verified' has no dedicated visual identity — TrustBadge and verification chips borrow generic primary/success styling. Introduce a `--bal-trust`/`--bal-trust-soft` token pair (both schemes), a consistent shield/checkmark mark, and a defined 'verified nurse' card treatment (badge placement, tinted ring on avatar, tooltip explaining WHAT was verified: identity, license, Shahkar). This turns the platform's core differentiator into a recognizable, repeatable visual asset instead of an ad-hoc green chip.
+- **Extend tokens beyond color: shadows, radii, motion, focus** (impact: medium, effort: medium) — Add teal-tinted elevation tokens (e.g. shadows built on rgba(29,74,64,α) for light / black-teal for dark), a radius scale (4/10/16) documented next to shape.borderRadius, motion tokens (durations + easings) for consistent transitions, and a global :focus-visible ring (2px --bal-primary, offset 2) applied via theme so keyboard accessibility is uniform instead of existing in exactly one card.
+- **No-flash color-scheme boot + dark browser chrome** (impact: medium, effort: small) — Render MUI's InitColorSchemeScript (or a 3-line inline script) before paint so cookie-less OS-dark visitors never see the light flash, and switch viewport.themeColor to the media-query array form ({ media: '(prefers-color-scheme: dark)', color: BRAND.tealDeep }) so the browser UI matches the page in both schemes.
+- **Delete starter residue from the theme layer** (impact: medium, effort: small) — Remove light.ts, dark.ts, the deprecated TYPOGRAPHY export, and the APP_THEME/LIGHT_THEME/default exports from theme/index.ts (nothing imports them); rewrite globals.css as an intentional base (logical-property-safe reset, ::selection in brand teal/cream, dvh-safe heights, focus-visible fallback); fix the false CONTENT_MIN_WIDTH comment and prune the option-menu comments in components/config.ts.
+- **Commit to one icon style and consider a warmer set** (impact: medium, effort: medium) — Normalize the registry to a single style (the Outlined majority) by swapping the ~12 legacy filled starter icons, or go further and adopt a rounded/duotone set (e.g. Material Symbols Rounded or Phosphor) rendered through the existing AppIcon registry — rounded strokes read warmer and less 'admin dashboard', matching clinical-but-human. The name-registry architecture makes this a config-file-only swap.
+- **Systematize Persian numerals as a component** (impact: medium, effort: small) — The Intl plumbing (money.ts, date.ts, booking/format.ts) is correct but every call site must remember the locale parameter; add a tiny // component family (or a useFormatters() hook) so counts, pagination, phone numbers, and durations can never accidentally render Latin digits on fa — and typographic details like the Toman unit label and IRR→Toman display stay consistent.
+
+## Keep (do not regress)
+
+- The two-layer token architecture (tokens.css --bal-* variables scheme-keyed on data-mui-color-scheme, mirrored in colors.ts) with the sync rule documented in both file headers — a genuinely well-designed system, keep it as the foundation for any restyle.
+- Outstanding token discipline in feature code: 331 var(--bal-*) usages across 103 files and effectively zero hard-coded hexes outside the theme layer (only the starter PencilIcon and one test) — do not let a design pass reintroduce literals.
+- Correct MUI v9 RTL setup: dual prebuilt themes (APP_THEME_LTR/APP_THEME_RTL), direction-aware Emotion cache with stylis-plugin-rtl in ThemeProvider.tsx, and lang/dir sourced from the [locale] layout with the documented reasoning for why lives there.
+- Cookie-SSR color-scheme sync (lib/cookies/server.ts getThemeMode + data-mui-color-scheme stamped server-side) — returning visitors get zero dark-mode flash; also the documented colorSchemeSelector fix in theme.ts.
+- Per-locale font loading done right: Mikhak via next/font/local with preload:false and a conditional className so Persian woff2 never ships to /en, and the skill rule that fonts load only in the locale layout.
+- Brand-harmonized feedback tokens plus NotistackProvider styling toasts entirely from --bal-* variables, so toasts track scheme and direction for free through the portal.
+- The Persian correctness utility layer: money.ts (BigInt IRR, fa-IR digit grouping, Toman-at-the-boundary), date.ts (fa-IR-u-ca-persian Shamsi via Intl, no date library), text.ts toEnglishDigits for Persian-keyboard input, booking/format.ts locale clocks — this is rare-quality i18n plumbing.
+- The few global theme decisions already made are the right ones: shape.borderRadius 10, button textTransform 'none', dark palette that lifts teal to #6fc0ac on deep-teal surfaces instead of inverting to grey.
+- The frontend-designer skill (.claude/skills/frontend-designer/SKILL.md) as a written, enforceable design contract — extend it with whatever the design pass adds rather than replacing it.
+- The AppIcon name-registry pattern — exactly what makes a future icon-set swap a one-file change.
diff --git a/dev/post-phase/ui/ui-phase-0-design-language.md b/dev/post-phase/ui/ui-phase-0-design-language.md
new file mode 100644
index 0000000..638bcbf
--- /dev/null
+++ b/dev/post-phase/ui/ui-phase-0-design-language.md
@@ -0,0 +1,313 @@
+# UI Phase 0 — Design language & theme foundation
+
+> **Mission:** kill the default-MUI/starter look at the token + theme layer in one pass and establish the
+> brand's visual system — a `theme.components` pass, a semantic palette, a real Persian type scale, one icon
+> family, a designed Balinyaar logomark, extended tokens (elevation/motion/focus/rating/trust/money), and a
+> purge of starter residue — so every later phase composes on a system that already looks designed. **No page
+> redesigns here**: this phase changes what every screen inherits, not any screen's layout.
+>
+> **Track:** frontend · **Depends on:** — (first phase of the [UI chain](README.md)) · **Unlocks:**
+> [Phase 1](ui-phase-1-primitives-and-states.md), [Phase 2](ui-phase-2-shells-and-navigation.md), and every
+> later phase — they all inherit the de-startered look built here.
+> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md)
+> and invoke the frontend-designer skill — both are mandatory.**
+
+## 1. Context — where this sits
+
+Balinyaar's feature layer is unusually disciplined — 331 `var(--bal-*)` usages across 103 files, effectively
+zero hard-coded hexes, correct RTL/Shamsi/BigInt-money plumbing — but the *visual system underneath it is
+still the karpolan react-mui starter*. Diagnosed root causes (all verified in code):
+
+1. **`createTheme` has zero `components` overrides** — `client/src/theme/theme.ts:19-36` passes only
+ `cssVariables`/`colorSchemes`/`typography`/`shape`/`direction`; every Button/Card/TextField/AppBar/Chip/
+ Dialog/Alert renders stock Material with recolored primaries. The single biggest cause of the starter feel.
+2. **No semantic palette** — `theme/colors.ts` defines only primary/secondary/background/text/divider, so
+ inline `` surfaces render stock MUI green/red while toasts (`lib/toast/NotistackProvider.tsx`)
+ use the brand `--bal-*` feedback tokens — two feedback systems for one semantic state. Worse, a bare
+ `` defaults to a **filled stock-red error** (`components/config.ts:10-11`).
+3. **Persian typography is Roboto metrics** — `TYPOGRAPHY_RTL` (`theme/typography.ts:43-52`) sets only
+ `fontFamily` + weights: non-zero letter-spacing on a joined script, tight heading leading, and a requested
+ weight **600** (h6, button) that the Mikhak loader (`app/[locale]/layout.tsx:39-48`, weights 400/500/700)
+ renders as full Bold — repeated in 68 `fontWeight: 600` sx usages across 42 files (verified). Space
+ Grotesk (`--font-space-grotesk`) is declared for EN but never loaded.
+4. **The brand mark is the starter's Twemoji cartoon pencil** — `AppIcon/config.ts:115` registers
+ `logo: PencilIcon`; `components/auth/BrandMark.tsx:21` renders it at 56px on every auth screen and passes
+ `color="var(--bal-primary)"`, which the hard-coded fills (`#EA596E`, `#FFCC4D`, …) silently ignore.
+5. **The icon system is broken and incoherent** — `AppIcon.tsx:38-46` passes `size` as SVG `width`/`height`
+ *attributes*, which MUI `SvgIcon`'s `1em` class CSS overrides, so every `size={14..56}` call-site renders
+ 24px. The ~87-entry registry mixes filled starter icons against `*Outlined` feature icons, carries eight
+ dead entries (zero usages, verified), and has **no back/chevron icon at all** (only `expand`).
+6. **Starter residue everywhere else:** `AppButton`'s `DEFAULT_SX_VALUES = { margin: 1 }` neutralized by
+ **213 `m: 0` occurrences across 82 files**; dead `theme/light.ts`/`dark.ts` exported from `theme/index.ts`
+ incl. `LIGHT_THEME as default` (nothing else imports them); the 17-line starter `globals.css`; the wrong
+ `CONTENT_MIN_WIDTH` comment (`components/config.ts:5`); unused `AppImage` (all verified); a cookie-less
+ OS-dark first visit paints light first (`lib/cookies/server.ts:46` returns `'light'`, no pre-paint script
+ exists — acknowledged doc drift in `client/CLAUDE.md`); `viewport.themeColor` hard-coded to light teal
+ (`layout.tsx:50-52`); `public/site.webmanifest` still ships starter `#000000`/`#ffffff` and references
+ `img/favicon/*.png` files that don't exist.
+
+**What already exists (do not rebuild):**
+
+- The **two-layer token system** — `theme/tokens.css` (`--bal-*`, scheme-keyed on `data-mui-color-scheme`)
+ mirrored by `theme/colors.ts`, sync rule in both headers. Extend it; never bypass it.
+- The **MUI v9 RTL dual-theme setup** (`APP_THEME_LTR`/`APP_THEME_RTL` built once at module load,
+ `stylis-plugin-rtl` Emotion cache in `ThemeProvider.tsx`) and the **cookie-SSR color-scheme sync**
+ (`getThemeMode()` → `data-mui-color-scheme` stamped server-side; explicit `colorSchemeSelector`).
+- **Per-locale font loading** (Mikhak via `next/font/local`, `preload: false`, attached only on `fa`),
+ the **AppIcon string registry** with its snake_case domain names (the exact indirection that makes this
+ phase's icon swap a config-level change), **brand-styled notistack toasts**, and the correct global
+ decisions already made: `shape.borderRadius: 10`, `textTransform: 'none'`, the lifted dark-teal palette.
+
+## 2. Required reading (do this first)
+
+- The audits: [audit/theme-and-brand.md](audit/theme-and-brand.md),
+ [audit/component-primitives.md](audit/component-primitives.md),
+ [audit/cross-cutting-ux.md](audit/cross-cutting-ux.md) — the full file/line evidence and the
+ **Keep (do not regress)** lists this phase must honor.
+- [../../../.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md) —
+ the design contract you are extending (invoke the skill; this phase also *updates* it — see 3.10).
+- `client/CLAUDE.md` — "Golden rules", "Theme System" (the cookie/no-flash machinery and the MUI v9 traps:
+ explicit `colorSchemeSelector`, never `InitColorSchemeScript`, never `storageWindow`), and "Fonts".
+- Code, in this order: all of `client/src/theme/`, `components/config.ts`, `components/common/AppIcon/`
+ (+ `icons/PencilIcon.tsx`), `components/common/AppButton/AppButton.tsx`, `components/auth/BrandMark.tsx`,
+ `lib/toast/NotistackProvider.tsx`, `app/[locale]/layout.tsx`, `app/globals.css`, `lib/cookies/server.ts`
+ (`getThemeMode`), `public/site.webmanifest`.
+- `product/overview/platform-summary.md` — the tone (trust-first, calm, clinical-but-human) every visual
+ decision here must serve.
+
+## 3. Scope — build this
+
+### 3.1 The `theme.components` pass (the highest-leverage change in the app)
+
+Add a `components` block inside `createAppTheme` (`theme/theme.ts`) that encodes the brand once. Reference
+colors as `var(--bal-*)` tokens (or `theme.vars.palette.*`) so every override is scheme-correct — never a
+hex. Overrides must be direction-safe (logical properties only — both theme directions share them). Cover at
+least: **MuiButton** (`disableElevation`, comfortable `paddingInline`, weight per the 3.3 decision, keep
+`textTransform: 'none'`); **MuiPaper/MuiCard** (hairline `1px solid var(--bal-divider)` border + soft
+**teal-tinted** shadows from the 3.4 elevation tokens instead of MUI's grey stack on warm cream);
+**MuiAppBar** (paper/cream surface + hairline bottom divider, **not** solid primary — shells are rebuilt in
+phase 2, this stops the default bar screaming "starter" now); **MuiOutlinedInput/MuiTextField** (house
+radius, calmer resting border, brand focus ring); **MuiChip / MuiToggleButton(Group) / MuiListItemButton**
+(soft `--bal-primary-soft`/`--bal-secondary-soft` tonal fills; `selected` = primary-soft + primary text —
+tokens already exist in both schemes); **MuiDialog** (radius 16, sane mobile margins); **MuiTabs** (thicker
+indicator, comfortable min-height); **MuiAlert** (severities from the 3.2 semantic palette so inline alerts
+match toasts); **MuiStepper/MuiStepIcon** (brand-colored active/completed steps — the trust flows live on
+these); **MuiSkeleton** (warm tint per scheme); **MuiTooltip** (ink/cream inversion, house radius); and
+**MuiCssBaseline** carrying a global `:focus-visible` treatment off `--bal-focus-ring` (2px ring + offset —
+today exactly **one** `:focus-visible` style exists in the app, in `NurseResultCard.tsx`; after this,
+keyboard focus is uniform everywhere).
+
+### 3.2 Semantic palette — one feedback language
+
+- Add `success`/`error`/`warning`/`info` (main + contrastText, light **and** dark) to
+ `LIGHT_PALETTE`/`DARK_PALETTE` in `colors.ts`, sourced from the existing `--bal-*` feedback values in
+ `tokens.css` (light: `#1f6b50`/`#a8392a`/`#8a6418`/`#1d4a40`; dark: the lifted set). Keep the two files
+ mirror-synced. Alert, `Chip color="success"`, Badge, LinearProgress become brand-harmonized automatically.
+- Fix the `AppAlert` footgun in `components/config.ts`: first make severity explicit at any call-site that
+ genuinely means error, then change the defaults to a calm baseline (`severity="info"`,
+ `variant="standard"` on the new tinted severities — decide and document in the config file).
+
+### 3.3 A real Persian type scale
+
+In `theme/typography.ts` `TYPOGRAPHY_RTL`:
+
+- **`letterSpacing: 0` on every variant** — Persian is a joined script; tracking breaks Mikhak's glyph
+ connections (MUI's Latin defaults set non-zero spacing on body1/body2/button/caption/overline).
+- **Body line-height ≥ 1.7; headings ~1.4–1.5** (room for Persian ascenders/descenders) plus **responsive
+ heading sizes** (MUI's default 6rem h1 is unusable; grep confirms zero h1/h2 usages today).
+- **Resolve the weight-600 problem.** Mikhak loads 400/500/700 only, so every requested 600 silently renders
+ 700. Decide: (a) adopt **500/700 semantics** — map the theme's h6/button and the 68 `fontWeight: 600` sx
+ usages across 42 files to loaded weights (recommended: 700 headings/buttons, 500 in-text emphasis; the
+ sweep is mechanical) — or (b) add a genuine Mikhak DemiBold face to `src/app/fonts/` + the loader. Pick
+ one, do it fully, **document the decision in `typography.ts` and the skill (3.10)**.
+- **Settle the EN display font**: wire Space Grotesk via `next/font` in `app/[locale]/layout.tsx` (attached
+ only on `en`, mirroring the Mikhak pattern, `preload: false`) **or** delete the dead
+ `BRAND_FONT_VARIABLE_EN` variable and its fallback stack. Decide and do it — no vaporware comment left.
+
+### 3.4 Token extension — beyond color
+
+Extend `theme/tokens.css` (**both scheme blocks — always**), mirroring palette-level values in `colors.ts`:
+
+- **Elevation/shadow tokens** — 2–3 steps of teal-tinted shadows (light: `rgba(29,74,64,α)` stacks; dark:
+ black-teal), consumed by 3.1's Paper/Card/Dialog overrides.
+- **Radius scale** — document the house scale (e.g. 4 / 10 / 16: controls / cards / dialogs) as tokens or
+ constants next to `shape.borderRadius`; stop inventing radii per component.
+- **Motion tokens** — durations (e.g. 120/200/300ms) + easings; defined once here, consumed by phase 12 —
+ and **`--bal-focus-ring`**, the global focus ring used by 3.1's `:focus-visible` treatment.
+- **`--bal-rating` + `--bal-rating-empty`** — proper star gold + empty color per scheme (`RatingInput`'s
+ muddy `--bal-warning` stars are fixed in **phase 1** using these); **`--bal-money-emphasis`** — a
+ contrast-safe money-text emphasis per scheme, retiring terracotta as a small-text money color (`#d98c6a`
+ on white fails AA contrast); **`--bal-trust` + `--bal-trust-soft`** — a distinct trust identity for
+ verified marks (not generic primary/success), consumed by phases 1/4/8. All define-only here.
+- Update the stale `tokens.css` header (it cites `product/balinyaar.html`, which no longer exists in the
+ repo) to point at the frontend-designer skill as the brand source of truth (3.10).
+
+### 3.5 A real brand mark — kill the pencil
+
+- Design the Balinyaar logomark per the identity the skill records (the `product/balinyaar.html` seed deck
+ is **gone from the repo** — recreate from the tokens + the skill's description): deep-teal ground, cream
+ lowercase glyph, a single terracotta dot. Build two SVGs under `AppIcon/icons/`: a **monochrome
+ `currentColor` logomark** (so `color="var(--bal-primary)"` finally works and it recolors in dark mode for
+ free) and a **full lockup** (mark + wordmark) for `BrandMark`/auth.
+- Register the mark as `ICONS.logo` (replacing `PencilIcon`), update `components/auth/BrandMark.tsx` to the
+ new lockup, **delete `icons/PencilIcon.tsx`** (the last hard-coded-hex SVG in the app), regenerate
+ `src/app/favicon.ico` from the mark, and fix `public/site.webmanifest` — brand
+ `theme_color`/`background_color` (currently starter `#000000`/`#ffffff`) and icon entries that actually
+ exist (generate the referenced PNG sizes from the mark, or trim the manifest — today it points at missing
+ `img/favicon/*.png`).
+
+### 3.6 Icon system — one family, working sizes, full vocabulary
+
+- **Fix the size bug** in `AppIcon.tsx`: drive `fontSize` via `style`/`sx` instead of `width`/`height`
+ attributes (which MUI `SvgIcon`'s `1em` class CSS beats) — all ~40 existing `size={14..56}` call-sites
+ start working simultaneously. Stop spreading the invalid `size` attribute onto the DOM ``; make the
+ unknown-name `console.warn` **dev-only**; keep the custom-SVG path (the new logomark) scaling correctly.
+- **Normalize the ~87-entry registry to ONE visual family** — recommend the **Rounded** variants of
+ `@mui/icons-material` (zero new deps, warmer than the current filled/outlined mix, fits
+ clinical-but-human). A `config.ts`-only sweep thanks to the registry indirection. Delete the eight dead
+ starter entries (`daynight`/`night`/`day`/`visibilityon`/`visibilityoff`/`signup`/`login`/`settings` —
+ re-verify zero usages before each delete).
+- **Add the missing vocabulary**: `back`/`chevron_start` (no directional nav glyph exists today except
+ `expand`), `share`, `copy`, `phone` (non-emergency call), `navigate` (directions), `sort`, `attachment`,
+ `star_half`. Note `camera`, `calendar`, `wallet`, and `tune` (filter) are **already registered** — they
+ only need family normalization. **Directional icons auto-mirror**: register `back`/`chevron_start` as the
+ LTR glyph, mirrored via a `[dir="rtl"]` `scaleX(-1)` rule applied by `AppIcon` for a declared
+ `DIRECTIONAL_ICONS` set in `config.ts`; write the rule into the registry comment and the skill — later
+ phases must not hand-roll flips.
+
+### 3.7 AppButton de-startering
+
+- Remove `DEFAULT_SX_VALUES` (`margin: 1`), then sweep the now-no-op **213 `sx={{ m: 0 }}` neutralizations
+ across 82 files** (mechanical: remove the key; drop `sx` when it becomes empty). Outer spacing becomes the
+ parent's job (Stack/Box gaps). Clean the starter prop cruft: duplicate `label`/`text` props (pick one,
+ migrate the loser's call-sites), the false "Box around to specify margins" JSDoc, the `// Missing props`
+ comment, and the `underline` spread onto non-link buttons (`AppButton.tsx:85`).
+- **Do not break the public API**: `to`/`href` auto-link composition, icon-name `startIcon`/`endIcon`, and
+ non-MUI-color-becomes-text-color all stay (238 call-sites). Leave the `color='inherit'` default as-is —
+ changing every unspecified button's color is a per-screen decision for later phases. Update
+ `AppButton.test.tsx` (and every other touched shared component's test) in the same change.
+
+### 3.8 Starter residue purge
+
+- Delete `theme/light.ts` + `theme/dark.ts` and their exports from `theme/index.ts` (verified: nothing else
+ imports them; drop `LIGHT_THEME`/`DARK_THEME`/`APP_THEME`/`LIGHT_THEME as default` — keep `ThemeProvider`,
+ `getDirection`, `APP_THEME_LTR/RTL`). Delete the deprecated `TYPOGRAPHY` alias in `typography.ts` once
+ nothing imports it.
+- Rewrite `app/globals.css` as an intentional minimal base: keep the `box-sizing` reset, **drop
+ `max-height: 100vh`** and the `max-width: 100vw; overflow-x: hidden` mask, add `::selection` in brand
+ colors, and a commented decision on the `a { color: inherit }` reset (AppLink owns link affordance).
+- Fix the wrong `CONTENT_MIN_WIDTH` comment (`components/config.ts:5`), prune the starter
+ commented-out-alternatives style there into owned decisions, and delete the unused `AppImage` + its test +
+ barrel export (verified: zero product usages).
+
+### 3.9 No-flash color-scheme boot
+
+- A cookie-less OS-dark first visit paints light, then flips. Implement the **`ColorSchemeScript`** that
+ `client/CLAUDE.md`'s Theme System section already documents (acknowledged doc drift): an inline ``
+ script in `app/[locale]/layout.tsx` that reads the `color-scheme` cookie, falls back to
+ `matchMedia('(prefers-color-scheme: dark)')` when absent, and sets `data-mui-color-scheme` before first
+ paint — plus the documented `Storage.prototype` patch. **Never** MUI's `InitColorSchemeScript` (banned in
+ `client/CLAUDE.md` — it reads localStorage, which diverges from the cookie). The script must agree with
+ `getThemeMode()` on every path. Switch `viewport.themeColor` (`layout.tsx:50-52`) to the media-query array
+ form: light → `BRAND.teal`, `(prefers-color-scheme: dark)` → `BRAND.tealDeep`, so browser chrome matches
+ the page in both schemes.
+
+### 3.10 Keep the design contract honest
+
+Update `.claude/skills/frontend-designer/SKILL.md` **in the same change**: the new tokens (elevation, radius,
+motion, focus, rating, trust, money-emphasis) and when to use each; the type-scale + weight decision (its
+"buttons weight 600" rule becomes wrong the moment 3.3 lands); the normalized icon family + directional-
+mirroring rule + a corrected registered-icons note (it lists ~19 of ~87 icons and still names the dead
+starter entries); and the brand-mark construction as the written source of truth now that
+`product/balinyaar.html` is gone (fix its reference; `tokens.css`'s header points here too).
+
+**(DEFERRED → phase 1):** all new shared primitives — EmptyState/ErrorState, PageHeader, ``, Jalali
+picker, StatusChip v2, RatingInput's star fix, skeleton twins, route-level `loading.tsx`/`error.tsx`/404.
+**(DEFERRED → phase 2):** the shells — TopBar/SideBar chrome, `UserInfo`, nav grouping, SSR mobile-first flash.
+**(DEFERRED → phase 12):** the app-wide motion pass that consumes 3.4's motion tokens.
+
+## 4. Mocks & seams in this phase
+
+None. This phase is pure client theming — no service seams, no mock flags. The chain's REQ posture: if a
+backend gap surfaces (none is expected here), append a REQ entry to
+[../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
+— REQ-001…038 are taken (verified: the tracker ends at REQ-038); number onward from **REQ-039**. UI stays
+mock-tolerant behind the existing `services/{domain}` seams; never edit `server/`.
+
+## 5. Critical rules you must not get wrong
+
+- **The two-layer token sync is law** — every color added/changed lands in `tokens.css` (**both** scheme
+ blocks) *and* `colors.ts` in the same commit — and **zero hex regressions**: feature code has effectively
+ no hard-coded hexes; the theme pass must not reintroduce literals outside the theme layer (deleting
+ `PencilIcon` removes the last offender).
+- **Do not break the RTL dual-theme/Emotion setup** (`APP_THEME_LTR`/`APP_THEME_RTL` stay module-load-built;
+ `createTheme()` never in a component; overrides use logical properties only) **or the cookie-SSR
+ color-scheme sync** (`colorSchemeSelector` stays the explicit `'data-mui-color-scheme'`, never `'data'`;
+ no `InitColorSchemeScript`; no `storageWindow`).
+- **The AppIcon registry indirection stays** — keep the `` API, the snake_case domain
+ names, the lowercase keys. **AppButton's composition API stays** (`to`/`href` → AppLink, icon-name
+ `startIcon`/`endIcon`) — 238 call-sites; the margin removal is a default change, not an API change.
+- **Keep the keep-lists.** Notably: `textTransform: 'none'`, radius 10 as the house radius, the lifted
+ dark-teal palette, per-locale font loading (Mikhak never ships to `/en`; mirror that discipline if you
+ wire Space Grotesk), and `NotistackProvider` staying token-driven.
+- **Design-contract non-negotiables:** any new user-facing string (e.g. the logo's aria/alt) goes in
+ **both** `messages/en.json` and `messages/fa.json`; MUI v9 API only; co-located `*.test.tsx` updated for
+ every touched shared component (`AppIcon`, `AppButton`, `BrandMark`, …); fetch/cookies rules untouched.
+- **Update the skill and `client/CLAUDE.md` in the same change** (3.10 + §8) — stale design docs are how the
+ next phase reintroduces the starter.
+
+## 6. Definition of Done
+
+On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):
+
+- [ ] `npm run check` green; `npm run test:ci` green incl. updated tests for every touched shared component;
+ `en.json`/`fa.json` in sync.
+- [ ] `theme.ts` has a `components` block covering at least the 3.1 list; no override uses a raw hex or a
+ physical direction property.
+- [ ] `LIGHT_PALETTE`/`DARK_PALETTE` define success/error/warning/info; an inline
+ `` and a success toast are visibly the same color family in both schemes.
+- [ ] `TYPOGRAPHY_RTL` has `letterSpacing: 0` everywhere + the new line-heights and responsive heading
+ sizes; no `fontWeight: 600` remains in `src` (or a real 600 face is loaded — per the documented
+ decision); the Space Grotesk question is resolved (wired or deleted).
+- [ ] `tokens.css` (both blocks) carries elevation, radius, motion, focus-ring, rating, money-emphasis, and
+ trust tokens, mirrored in `colors.ts` where palette-level.
+- [ ] `ICONS.logo` is the new Balinyaar mark; `PencilIcon.tsx`, `light.ts`, `dark.ts`, `AppImage`, and the
+ eight dead icon entries are deleted; favicon + webmanifest rebuilt with brand colors and only real
+ files referenced.
+- [ ] ` ` actually renders 48px; the registry is one visual family;
+ `back`/`chevron_start` mirror correctly under `dir="rtl"`; `AppButton` has no default margin and
+ **zero** `sx={{ m: 0 }}` neutralizations remain in `src`.
+- [ ] First visit with OS dark preference and no cookie paints dark with no light flash; browser
+ `theme-color` matches the scheme in both modes.
+- [ ] Visual verification on the four axes — `/fa` + `/en` × light + dark — and mobile + desktop widths on
+ the §7 walk, with screenshots in the report; skill + `client/CLAUDE.md` updated to match reality.
+
+## 7. How to test (what a human can verify after this phase)
+
+1. Open `/fa/login` (private window, OS set to dark, no cookies): the page paints **dark immediately** — no
+ light flash — and shows the **new Balinyaar logomark** (not a pencil), recoloring correctly in dark mode.
+2. On the customer home `/fa`: buttons are flat (no elevation) with comfortable padding; cards show hairline
+ borders + soft teal-tinted shadows (not grey); chips are soft-tinted; nothing renders stock-MUI grey/blue.
+ Tab through: every focusable element shows the same 2px brand focus ring.
+3. Visit `/fa/nurse` and `/fa/admin` (seeded accounts): the top bar is a cream/paper surface with a hairline
+ divider — **not** a solid teal slab; sidebar icons are one visual family (all Rounded); the selected nav
+ item uses the soft-primary fill.
+4. Trigger an inline alert (e.g. a form error state) and a toast on the same screen: both use the same
+ brand-harmonized semantic colors, in light and dark.
+5. On `/fa`, inspect Persian text: no letter-spacing gaps inside joined words, headings don't clip
+ ascenders, bold shows a real weight hierarchy (not everything Bold). Compare `/en`: headings render per
+ the 3.3 decision (Space Grotesk or the documented system stack).
+6. Check the browser tab: the favicon is the new mark; toggling OS dark mode flips the browser chrome
+ (`theme-color`) to the deep-teal value.
+
+## 8. Hand off & document (close the phase)
+
+- Update `client/CLAUDE.md`: the Theme System section (`ColorSchemeScript` now real — remove the doc-drift
+ note; new tokens; the weight decision), the theme entries in "Project Structure" (`light.ts`/`dark.ts`
+ removed, new icon assets), and the Fonts table if Space Grotesk was wired. Update
+ `.claude/skills/frontend-designer/SKILL.md` per 3.10 (part of the phase, not optional).
+- Write the frontend report at `dev/shared-working-context/reports/ui-phase-0-report.md`: what changed at the
+ theme layer, the type-scale + weight + icon-family decisions, before/after screenshots on the four axes,
+ what later phases must know (new token names, directional-icon rule), and any REQs filed (expected: none).
+- Save a memory note per operating-rules §8: the theme/token/icon/brand decisions, the size-bug fix, the
+ margin-default removal, and the no-flash boot mechanism — later UI phases build on all of them.
diff --git a/dev/post-phase/ui/ui-phase-1-primitives-and-states.md b/dev/post-phase/ui/ui-phase-1-primitives-and-states.md
new file mode 100644
index 0000000..f4d567a
--- /dev/null
+++ b/dev/post-phase/ui/ui-phase-1-primitives-and-states.md
@@ -0,0 +1,296 @@
+# UI Phase 1 — Shared primitives & app-wide states
+
+> **Mission:** build the shared component kit the pages keep hand-rolling — state views, page header, confirm
+> dialog, card anatomy, ``, locale formatting, a Jalali date picker, status/timeline/countdown/rating
+> upgrades, skeleton twins — give the app real route-level chrome (loading / error / 404 / per-route metadata),
+> and fix the error→false-empty defects the missing kit caused. This phase **owns every shared primitive**
+> (see the [README ownership rules](README.md)); phases 3–11 only consume what ships here.
+>
+> **Track:** frontend · **Depends on:** [Phase 0](ui-phase-0-design-language.md) (theme pass, icon family,
+> `--bal-money-emphasis` / `--bal-rating*` / elevation / motion / focus tokens) · **Unlocks:** the kit every
+> area redesign (phases 3–11) composes from
+> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md) and invoke the `frontend-designer` skill — both are mandatory.**
+
+## 1. Context — where this sits
+
+Balinyaar's feature layer is disciplined (tokens, RTL, four-state data pattern), but the *page primitives*
+were only ever built for the backoffice. Everything user-facing is hand-rolled per page, so the app reads as
+a starter and every restyle is a 20-file change. Verified in code:
+
+- **29 hand-rolled dashed-border `Paper` empty/error blocks across 23 files** (grep `border: '1px dashed'`),
+ while `AdminEmptyState`/`AdminErrorState` sit unused outside `components/admin/`.
+- **24 hand-assembled `formatIrrToToman(x) + t('currency_toman')` sites across 19 files**; `PriceBreakdown.tsx:62`
+ colors the grand total terracotta (`--bal-secondary`, ≈2.7:1 on white — a WCAG failure on the most
+ trust-critical number), a pattern repeated across five sibling money components.
+- **23 copies of the `locale === 'fa' ? 'fa-IR' : 'en-US'` ternary across 15 files**, already drifting
+ (`admin/partners/[id]/page.tsx` passes the raw locale); **no relative-time formatting anywhere**; the OTP
+ resend clock renders Latin digits (`components/auth/OtpStep.tsx:27`).
+- **Zero `loading.tsx` / `error.tsx` / `not-found.tsx` / `global-error.tsx`** under `client/src/app`; the only
+ crash UI is the starter `ErrorBoundary` (English ``, raw `componentStack` dump); one static ``
+ serves ~60 routes; **no Jalali date input** — every date input is a native Gregorian `type="date"`.
+- **The missing kit caused real defects.** A failed query renders as *success-with-no-data*:
+ - `(customer)/page.tsx:57–68` — the page gate destructures only `data` from `usePatients()`; on error it
+ returns ` ` forever (spinner-hang). The `CategoryGrid` in the same file branches all four
+ states correctly — the discipline exists, the gate doesn't.
+ - `(customer)/patients/page.tsx:83` (`isEmpty = !isLoading && …length === 0` → error shows «no patients»);
+ `nurse/requests/page.tsx:19` (error = empty inbox); `nurse/services/MyServicesList.tsx:37,42` (error =
+ «add your first service» CTA).
+ - `(customer)/profile/page.tsx:16` — an errored profile renders a **blank form** whose save could overwrite
+ server truth; `nurse/services/VariantBuilder.tsx:384` — `optionGroupsQuery` has no error branch, so failed
+ option groups are treated as "category has none", letting a nurse skip required options.
+- **Silent mutations:** `nurse/profile/page.tsx:44` (avatar upload) and `:54–64` (profile save) pass only
+ `onSuccess` — a failure gives zero feedback.
+
+**What already exists (do not rebuild):**
+
+- The admin primitives to *promote*, not rewrite (all in `client/src/components/admin/`, all tested):
+ `AdminPageHeader` (title/subtitle/actions, caller-owned i18n), `AdminEmptyState`/`AdminErrorState`,
+ `ConfirmDialog` (required-reason gating + busy-disable).
+- `utils/money.ts` (BigInt IRR↔Toman, fa digits) and `utils/date.ts` (Shamsi via `Intl` `fa-IR-u-ca-persian`,
+ no date library); `CountdownTimer`'s architecture — server-frozen UTC deadline prop, self-owned 1s tick
+ (only it re-renders), single `onElapsed` fire, `dir="ltr"` tabular-nums locale digits (`CountdownTimer.tsx:84`).
+- `StatusChip` as the single status color+icon source seven components delegate to; the exhaustive typed
+ enum→chip mappings; `TrustBadge`'s honest three states; the four-state pattern itself (~95 `isError`
+ branches, ~124 retries) — restyle and fill gaps, don't rebuild; the `dispatchToast → notistack` pipeline.
+- Phase 0's output: `theme.components` pass, one icon family, the new tokens. Build on them.
+
+## 2. Required reading (do this first)
+
+- [audit/component-primitives.md](audit/component-primitives.md) (kit inventory, hand-roll counts),
+ [audit/feature-components.md](audit/feature-components.md) (card-anatomy drift, rating/terracotta/timeline
+ defects), [audit/cross-cutting-ux.md](audit/cross-cutting-ux.md) (route chrome / metadata / formatting) —
+ each carries the full evidence and a keep-list.
+- [.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md) — the design
+ contract; and `client/CLAUDE.md` — Golden rules, Project Structure, Unit Testing, Toast Notifications, i18n.
+- Code to read before writing: `client/src/components/admin/{AdminPageHeader,AdminEmptyState,AdminErrorState,ConfirmDialog}.tsx`;
+ `client/src/components/{StatusChip,CountdownTimer,RatingInput,StepperHeader,RefundStatusCard,PriceBreakdown}/`;
+ `client/src/components/booking/BookingStatusTimeline/`; `client/src/utils/{money,date}.ts`;
+ `client/src/lib/api/client.ts` (which errors the fetch layer already toasts — 3.14 builds on it);
+ `client/src/theme/tokens.css` as phase 0 left it. Product framing: `product/overview/platform-summary.md`
+ (money display and Shamsi correctness are product rules, not preferences).
+
+## 3. Scope — build this
+
+New shared components live under `client/src/components/common//` (component + `index.tsx` barrel +
+co-located `.test.tsx`), exported from `@/components`, presentational with **caller-owned i18n** (already-
+translated strings in, like `AdminPageHeader` documents). Strings a component *does* own (route chrome,
+default retry labels) go in **both** `messages/en.json` and `messages/fa.json`.
+
+### 3.1 State kit — `EmptyState`, `ErrorState`, `QueryStateGate` + the false-empty fixes
+
+- Promote/generalize `AdminEmptyState`/`AdminErrorState` into shared `EmptyState` and `ErrorState` with a
+ branded look (phase 0 surface/radius tokens; registry icon slot; calm — retire the dashed border).
+ `EmptyState`: icon, title, body, optional CTA. `ErrorState`: **`onRetry` is required** («تلاش دوباره»).
+ `components/admin` keeps thin wrappers or its call sites migrate — either way no duplicated implementation
+ remains. (Custom illustration set: DEFERRED → phase 12.)
+- Add a small `QueryStateGate` (props: `isLoading`, `isError`, `onRetry`, `isEmpty`, `skeleton`, `empty`,
+ `children`) rendering skeleton → error → empty → data in that order, **plus** write the convention into
+ `client/CLAUDE.md`: *an errored query must never render as empty; every error branch has a retry.* Pages
+ with naturally inline branching (infinite lists) may follow the convention without the component.
+- Fix the six defects from §1 with it. The profile fix must render `ErrorState` instead of the blank form;
+ VariantBuilder's option-group error must block progression, not pretend "no options".
+- Mechanical sweep: replace the 29 dashed-`Paper` blocks in 23 files with `EmptyState`/`ErrorState`. Copy
+ stays per-page (existing keys); only the shell unifies.
+
+### 3.2 `PageHeader` + `ConfirmDialog` promotions
+
+- Generalize `AdminPageHeader` into a shared `PageHeader`: `title`, `subtitle?`, `actions?`, plus a `backTo?`
+ affordance (RTL-flippable chevron via the phase 0 icon family; if phase 0 shipped no `back` icon, extend
+ `AppIcon/config.ts` minimally and note it — ownership rule). Adopt it **mechanically** where pages already
+ hand-roll the identical h5/h1 + subtitle block (patients, addresses, bookings, earnings, …); per-page header
+ design is each area phase's job (DEFERRED → 3–11). `AdminPageHeader` becomes a re-export or its call sites migrate.
+- Move `components/admin/ConfirmDialog` to `components/common/ConfirmDialog` **preserving its contract**:
+ required-reason gating, busy state disabling both buttons, caller-owns-the-mutation. Migrate the admin
+ imports and the hand-rolled confirm flows only where mechanical (patients archive, addresses delete); the
+ remaining raw `` confirm sites are area-phase work (DEFERRED → 3–11).
+
+### 3.3 Card kit — `SurfaceCard` + `AccentCard`
+
+Encode the de-facto anatomy once: `SurfaceCard` = flat `Paper` (`elevation={0}`), 1px `divider` border, house
+radius, a padding scale (`sm`/`md`/`lg` ≈ today's p:2 / 2.5 / 3); `AccentCard` = `SurfaceCard` + a
+`borderInlineStart` accent taking a **semantic token name**, at one standardized width (pick one; end the
+3px/4px drift). Migrate the ~12 hand-rollers mechanically — `EarningsRow`, `EarningsBalanceHeader`,
+`PayoutHistoryRow`, `PatientCard`, `VariantCard`, `VisitNoteCard`, `InstallmentScheduleRow`, `PriceBreakdown`,
+`BankStatusPanel`, `DocumentUpload`'s panels, `BookingRequestSummaryCard`, the customer-home `NudgeCard` —
+visual unification only, zero content/behavior change.
+
+### 3.4 `` primitive
+
+One component for every displayed amount: takes a **served IRR digit-string**, renders Toman + unit label via
+`utils/money.ts` (never computes); `size`/`tone` variants with emphasis driven by `--bal-money-emphasis`
+(phase 0) — **not terracotta**; deduction rendering (muted/error tone with an explicit visual sign treatment,
+never a bare minus inside RTL text); optional strikethrough. Replace the 24 hand-assembled sites in 19 files —
+this fixes the `PriceBreakdown` total-contrast defect and the sibling terracotta-money sites (`RefundStatusCard`,
+`BnplPlanCard`, `InstallmentScheduleRow`, `CancellationPolicyDisclosure`) in one pass. `PriceDisplay` (catalog
+unit rates) stays; refactor it onto `` internals only if trivial — its contract is tested and correct.
+
+### 3.5 Formatting utils — `utils/number.ts`
+
+- `localeTag(locale)` → `'fa-IR' | 'en-US'` and `formatNumber(value, locale, options?)` — kill the 23
+ copy-pasted ternaries in 15 files (including the drifted `admin/partners/[id]/page.tsx` raw-locale call).
+- `formatRelativeTime(iso, locale)` via `Intl.RelativeTimeFormat`; **rule (document in JSDoc): relative up to
+ ~7 days («۲ ساعت پیش»، «دیروز»), then decay to the absolute Shamsi date** via `formatShamsiDate`. Consumers
+ land in phases 10/11; ship the util + tests now.
+- `formatClock(totalSeconds, locale)` — locale-digit `mm:ss`/`hh:mm:ss`; migrate `CountdownTimer`'s inline
+ `Intl` pad onto it and fix `OtpStep.tsx:27`'s Latin-digit resend clock.
+
+### 3.6 `JalaliDatePicker` + `JalaliDateField`
+
+Shamsi-native date selection that **emits ISO Gregorian** (the wire stays Gregorian; display stays Shamsi):
+a month-grid calendar (Persian month/weekday names, today marked, min/max props), a compact **day-chip-strip**
+variant for near dates (next N days as tappable chips — the C4 booking form's shape), and `JalaliDateField`
+(read-only input + picker popover) to replace native `type="date"` inputs. **Investigate and DECIDE the
+arithmetic layer:** prefer `Intl` `fa-IR-u-ca-persian` (already proven in `utils/date.ts`) for display + a
+minimal conversion for month math; a tiny, well-maintained jalali lib (e.g. `jalaali-js`, ~2 KB) is acceptable
+if `Intl`-only arithmetic gets awkward — document the choice and measured bundle cost in the report; no
+heavyweight date library. Locale-aware (`fa` Persian calendar, `en` may show Gregorian — same ISO output),
+keyboard-navigable, RTL-correct, both schemes. Consumed by phases 4, 5, 8, 11 — consumer migration is theirs
+(DEFERRED → 4/5/8/11).
+
+### 3.7 `StatusChip` v2
+
+Soft-tint default: `-soft` token backgrounds + strong text (extend `tokens.css` + `colors.ts` with any missing
+`-soft` pairs, both schemes); solid fill reserved for high-alarm states; a documented status weight hierarchy
+(neutral < info < progress < success < warning < alarm). `StatusChip` is the delegation point, so this is
+mostly one file; update direct chip consumers mechanically and keep the exhaustive enum→chip mappings intact.
+
+### 3.8 `StatusTimeline`
+
+A designed vertical timeline: node = label + optional timestamp + optional note; states completed / current
+(animated within the phase 0 motion tokens, reduced-motion-safe) / pending / terminal-failure. Migrate the
+two StepperHeader-as-status-display misuses: `RefundStatusCard.tsx:76` (3-step refund progress) and
+`components/booking/BookingStatusTimeline` (7-status booking lifecycle). **StepperHeader remains for real
+wizards only** (onboarding, verification, BNPL) — write that into its JSDoc.
+
+### 3.9 `CountdownTimer` v2
+
+Keep the do-not-regress architecture (server-frozen deadline, isolated tick, single `onElapsed`, `dir="ltr"`
+tabular digits). Add: a **progress-ring variant** (given `windowStart`); **urgency tiers** — calm teal →
+amber → terracotta — with threshold props, replacing the binary `urgent`; **humanized coarse mode** above
+~10 minutes («۲۵ دقیقه مانده», minute-grain updates) switching to the ticking clock below it; keep the `label`
+slot; `aria-live="polite"` at coarse intervals (never per second). Swap the hourglass `pending` glyph for the
+clock; drop the hard-coded `1.5rem` for size variants. Existing consumers (C5 tracker, nurse inbox, checkout,
+EarningsRow dispute window) keep working unchanged — new props optional.
+
+### 3.10 `RatingInput` v2
+
+Switch fill colors to phase 0's `--bal-rating` / `--bal-rating-empty`; **fractional fill** (dual-layer or clip)
+for read-only averages so 4.5 renders as 4.5 — then remove the `Math.round` workaround at
+`search/nurse/[nurseId]/page.tsx:260` (it shows a 4.5 average as 5 stars — an honesty bug on the core trust
+surface); size variants; keep the radiogroup/readOnly `role="img"` semantics and `data-*` hooks. Swap the star
+color in `NurseResultCard` and `BookingRequestSummaryCard` to the same tokens.
+
+### 3.11 Skeleton twins + `AppLoading` rebrand
+
+Co-located ` ` statics matching real anatomy (avatar disc, chip row, price line) for
+`NurseResultCard`, `EarningsRow`, `PayoutHistoryRow`, `PatientCard`, `VisitNoteCard`, `TicketListCard`; adopt
+at the loading branches that hand-guess heights (e.g. `search/results/page.tsx:75`). Rebrand `AppLoading` into
+a branded splash (brand mark + subtle motion, reduced-motion-safe) used **only** for true full-page waits
+(auth splash, gateway return). Of the 18 screens using it today, switch the ones with a shaped skeleton
+available to skeletons; the rest keep the splash until their area phase.
+
+### 3.12 Route chrome — loading / error / 404
+
+Per-route-group `loading.tsx` with shell-shaped skeletons (`(customer)` header + card stack; `nurse`/`admin`/
+`partner` sidebar-shell content; `(public-routes)` auth card). Branded `error.tsx` (per `[locale]` segment)
+and `not-found.tsx` + the next-intl catch-all 404 pattern — calm Persian-first copy, brand mark, retry
+(`reset()`) and «بازگشت به خانه» CTAs, keys in both catalogs. `global-error.tsx` (sits above `[locale]`,
+renders its own `` — it cannot use next-intl): minimal static **bilingual** fa+en copy — the one
+sanctioned hard-coded-string exception; note it in `client/CLAUDE.md`. Rewrite `components/common/ErrorBoundary`
+to the same design: i18n'd, branded, retry affordance, **stack/componentStack rendered only in development**
+(logged, never shown, in prod).
+
+### 3.13 Per-route metadata
+
+Title template via one `generateMetadata` in `app/[locale]/layout.tsx`: `'%s | بالینیار'` (fa) /
+`'%s | Balinyaar'` (en) + localized description. Establish and document the client-page pattern — `page.tsx`
+becomes a thin RSC exporting `generateMetadata` and rendering the co-located `'use client'` body — and apply
+it to the main landing pages now (customer home, `/login`, `/search`, `/bookings`, `/nurse`, `/admin`,
+`/partner`). Full per-page adoption continues in the area phases (DEFERRED → 3–11).
+
+### 3.14 Mutation-error convention
+
+Read `lib/api/client.ts` to confirm exactly which failures the fetch layer already toasts; then: **every
+mutation whose failure is not surfaced inline or by the fetch layer gets an `onError` toast.** Fix the two
+known silent ones — nurse profile save (`nurse/profile/page.tsx:54–64`) and avatar upload (`:44`) — and write
+the convention into `client/CLAUDE.md` (Toast Notifications section).
+
+## 4. Mocks & seams in this phase
+
+None — pure UI over existing `services/{domain}` seams; no new service, mock, or flag. Expected REQ count:
+zero. If a genuine backend gap surfaces anyway, append it to
+[../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
+numbered **REQ-039 onward** (001–038 are taken), keep the UI mock-tolerant behind the seams, never touch `server/`.
+
+## 5. Critical rules you must not get wrong
+
+1. **Presentational purity + caller-owned i18n stays.** Shared primitives take already-translated strings and
+ stable codes; no data-fetching, no wire-derived labels inside this layer.
+2. **Money invariants stay.** Amounts are served IRR digit-strings through `utils/money.ts` (BigInt); ``
+ formats, never computes; `PriceBreakdown`'s dev reconciliation guard survives the card-kit migration.
+3. **CountdownTimer architecture stays** — server-frozen deadline, isolated self-tick, single `onElapsed`;
+ deadlines never recomputed client-side. **ConfirmDialog's contract stays** through the move. **An error is
+ never an empty** — the defect class this phase kills; every error state keeps a retry.
+4. **Don't break what delegates.** `StatusChip` is the single status source and the enum→chip mappings are
+ exhaustively typed — keep both; `StepperHeader` keeps serving the real wizards untouched; honest-copy
+ components (EscrowNotice verbatim fa, RefundStatusCard's no-success-framing-on-failure,
+ EarningsBalanceHeader's "owed back") keep their copy and state logic — restyle shells only.
+5. **Design contract non-negotiables:** tokens not hexes (extend `tokens.css` + `colors.ts` together, both
+ schemes); logical/RTL-safe props only (keep the deliberate `dir="ltr"` islands for clocks/IBANs/phones);
+ both message catalogs in sync; MUI v9 API only; co-located tests for every shared component;
+ fetch/cookies/toast rules per `client/CLAUDE.md` untouched.
+6. **Never add a layout above `[locale]`** — `global-error.tsx` is a special file, not a layout, and is the
+ only file allowed static copy.
+
+## 6. Definition of Done
+
+On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):
+
+- [ ] `npm run check` green; `npm run test:ci` green (many shared components are touched — run it); every new
+ shared component has a co-located `.test.tsx`; `en.json`/`fa.json` in sync.
+- [ ] All six error→false-empty/spinner defects from §1 render `ErrorState` with a working retry (verify by
+ forcing the query to fail); the profile error never shows an editable blank form.
+- [ ] Sweep greps clean: `border: '1px dashed'` under `client/src/app` → zero; `currency_toman` outside
+ `utils/money.ts` + ``/`PriceDisplay` internals → zero; `locale === 'fa' ? 'fa-IR'` → only
+ `utils/number.ts`. The checkout total renders in `--bal-money-emphasis`, not terracotta.
+- [ ] `loading.tsx`, `error.tsx`, `not-found.tsx`, `global-error.tsx` exist and render branded, localized
+ (404/error verified on `/fa` and `/en`); ErrorBoundary shows no stack in a production build; browser
+ tab titles differ per section using the `%s | بالینیار` template.
+- [ ] `JalaliDatePicker`/`JalaliDateField` render Shamsi, emit ISO Gregorian, pass keyboard/RTL tests; the
+ arithmetic decision + bundle cost is documented in the report.
+- [ ] A 4.5 rating average renders a half-filled fifth star on the nurse profile (no `Math.round`);
+ `RefundStatusCard` and the booking timeline render `StatusTimeline` (wizards still use `StepperHeader`).
+- [ ] Nurse profile save and avatar upload toast on failure; the convention is in `client/CLAUDE.md`.
+- [ ] Visual verification on the four axes — `/fa` + `/en` × light + dark — and mobile + desktop for the state
+ kit, card kit, route chrome, and the Jalali picker.
+
+## 7. How to test (what a human can verify after this phase)
+
+1. Stop the backend (or force queries to reject via devtools offline mode) and load `/fa`: customer home shows
+ a branded error state with «تلاش دوباره» — not an infinite spinner. Restore, retry → home renders. Repeat on
+ `/patients`, `/profile`, `/nurse/requests`, `/nurse/services`: an error state, never «empty» copy or a blank form.
+2. Visit a garbage URL (`/fa/xyz`) → branded Persian 404 with a home CTA. Throw inside a page in dev → branded
+ error screen with retry; build production and confirm no stack is rendered.
+3. Navigate between sections and watch the tab title change («جستجو | بالینیار», «رزروها | بالینیار», …).
+4. Open search results while loading: skeletons match the card anatomy — no layout jump when data lands. Open
+ checkout (mock path): the total renders in the money-emphasis color with correct contrast in both schemes;
+ a deduction row (earnings commission) reads visually as a deduction.
+5. Open the C5 request tracker: progress ring, coarse «X دقیقه مانده» above 10 minutes, ticking Persian-digit
+ clock below, escalating tint as thresholds pass; the `/login` OTP resend clock shows Persian digits on `/fa`.
+6. Open a nurse profile with a fractional average: stars render the fraction. Open a refund status page:
+ vertical timeline with node states, not a form stepper.
+7. Mount `JalaliDateField` (the phase's test page or the first consumer): pick «۱۵ مرداد» → the emitted value
+ is the correct ISO Gregorian date; keyboard navigation works; `/en` shows the Gregorian view.
+8. Kill the network mid-save on the nurse profile → an error toast appears; the form keeps the draft.
+
+## 8. Hand off & document (close the phase)
+
+- Update `client/CLAUDE.md`: Project Structure (new `components/common/*` primitives, route-chrome files,
+ `utils/number.ts`), the QueryStateGate/error-never-empty convention, the mutation-error toast convention,
+ the metadata pattern, and the `global-error.tsx` exception.
+- Write the report at `dev/shared-working-context/reports/ui-phase-1-report.md`: what shipped, the Jalali
+ decision + bundle cost, icon-registry additions (if any), the sweep counts, any REQ entries appended to
+ [for-backend.md](../../shared-working-context/frontend/requests/for-backend.md) (REQ-039+; expected none),
+ and any foundation gaps left for phase 2+.
+- Save a memory note per operating-rules §8: the primitives kit now exists and later phases must consume it
+ (never fork local variants), the error-never-empty and mutation-toast conventions, and the Jalali decision.
diff --git a/dev/post-phase/ui/ui-phase-10-messaging-and-notifications.md b/dev/post-phase/ui/ui-phase-10-messaging-and-notifications.md
new file mode 100644
index 0000000..24a0728
--- /dev/null
+++ b/dev/post-phase/ui/ui-phase-10-messaging-and-notifications.md
@@ -0,0 +1,280 @@
+# UI Phase 10 — Messaging & notifications
+
+> **Mission:** make support/coordination feel like a messaging app, not a form list. The optimistic-send
+> plumbing underneath is excellent — but the surfaces betray it: a thread opens at the **oldest** message
+> and never live-updates, the inbox cannot page past 20 tickets, the unread signals are dead on the real
+> API (REQ-028 gap), every bubble carries a full Shamsi date-time, an alarm-red emergency banner with no
+> phone number sits permanently on every inbox, and notifications are a flat absolute-timestamp list.
+> This phase turns the platform's **only sanctioned communication channel** into a conversation.
+>
+> **Track:** frontend · **Depends on:** [Phases 0–2](ui-phase-2-shells-and-navigation.md) ·
+> **Unlocks:** support/coordination feels like a messaging app, not a form list
+> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md) and invoke the frontend-designer skill — both are mandatory.**
+
+## 1. Context — where this sits
+
+Messaging is ticket-based by product design (no free chat, no phone directory — see
+[product/business/12-messaging-and-emergencies.md](../../../product/business/12-messaging-and-emergencies.md)).
+A coordination ticket is auto-created for every confirmed booking, so an active nurse's inbox outgrows one
+page quickly. Both systems are functionally complete and real-API-wired (`USE_TICKETS_MOCK = false`) —
+what's missing is the messaging-app layer. Diagnosed, all verified in code:
+
+1. **The inbox can't page or filter.** `TicketInboxScreen.tsx:35` calls `useMyTickets({})` — page 1 /
+ pageSize 20 forever, no load-more, no status chips — even though the hook
+ (`services/tickets/hooks/useMyTickets.ts:14`) already accepts `page`/`pageSize`/`status` and keys the
+ cache on the filter object.
+2. **Unread signals are dead on the real path.** `unreadCount`/`lastMessageAt` on `TicketSummary` are
+ documented mock-only (`services/tickets/types.ts:56-59`, "REQ-028 gap"), so on the real API the unread
+ pill (`TicketListCard.tsx:44`) never shows and times silently fall back to `createdAt`. There is no
+ last-message preview in the type at all.
+3. **Threads never live-update and open at the top.** `useTicket.ts:15-21` has staleTime/gcTime but no
+ `refetchInterval` (contrast the polling `useUnreadCount.ts:21`); `TicketMessageList.tsx:51-63` has no
+ scroll logic — a long thread opens at the oldest message; a reply never appears until blur/refocus.
+4. **Zero chat typography.** Every bubble renders a full `formatShamsiDateTime` stamp
+ (`TicketMessageList.tsx:58`); no date separators, no same-author grouping, system messages render as
+ ordinary bubbles. `MessageBubble.tsx:70` forces `direction: 'ltr'` on a Persian Shamsi string — bidi
+ visually reorders the date/time segments.
+5. **The composer mis-handles mobile and failure.** `MessageComposer.tsx:46-51` — Enter always sends,
+ even on touch keyboards (no way to type a newline on mobile); on failure the optimistic bubble rolls
+ back and the only trace is a small caption, no retry, no aria-live (`MessageComposer.tsx:55-59`). The
+ `send` icon (`AppIcon/config.ts:85,187`) is never RTL-mirrored — stylis flips CSS, not SVG glyphs — so
+ in fa the paper plane points back into the text field.
+6. **Emergency affordance is wrong-sized.** `TicketInboxScreen.tsx:57` renders `` on
+ every inbox with no `contactPhone` — and `EmergencyBanner.tsx:58` only renders the call button when a
+ phone exists, so the inbox banner tells users to "call the emergency contact" on a surface that can
+ never show a number. The tel: contact only exists on the nurse's post-confirmation booking read.
+7. **Notifications are a flat list.** `NotificationCenter.tsx:103` stamps every row with an absolute
+ Shamsi date-time; no day grouping; non-navigable rows (deepLink → null) are still ButtonBase cards that
+ ripple and appear to do nothing (`NotificationCenter.tsx:46-50`). The bell is navigation-only
+ (`NotificationBell.tsx:31`) even on desktop; the support entry has no unread badge
+ (`CustomerLayout.tsx:48-53`); admin notifications is a dead `PlaceholderScreen`
+ (`admin/notifications/page.tsx:4-8`) the admin nav links to.
+
+**What already exists (do not rebuild):**
+
+- The full messaging component set in `client/src/components/messaging/` (`TicketInboxScreen`,
+ `TicketListCard`, `TicketThreadScreen`, `TicketMessageList`, `MessageBubble`, `MessageComposer`,
+ `ContactSupportDialog`, `EmergencyBanner`, `BookingSupportEntry`), all four-state, all tokenized.
+- The optimistic-send architecture: `usePostMessage` with clientMessageId reconciliation, draft cleared
+ only on server confirm, composer remount-keyed per ticket (`TicketThreadScreen.tsx:117-119`).
+- The notifications system: polled auth-gated `useUnreadCount` (only the bell container re-renders),
+ `NotificationCenter` (unread-first, optimistic mark-read, mark-all, load-more), role-aware null-safe
+ `services/notifications/deepLink.ts`, per-kind icons in `components/notifications/notificationIcon.ts`.
+- The seams: `services/tickets` and `services/notifications` (hooks/apis/keys/constants/types), the
+ is_internal-free user types, `ticketKeys`/`notificationKeys` cache-key factories.
+- Foundation from phases 0–2: theme/token system + icon registry (0), shared primitives + relative time +
+ state kits (1), the per-actor chrome with its header/nav slots (2).
+
+## 2. Required reading (do this first)
+
+- [audit/messaging-notifications.md](audit/messaging-notifications.md) — the 16-problem inventory with
+ file/line evidence, the opportunities this scope is drawn from, and the keep-list §5 restates.
+- Code, in this order: `client/src/components/messaging/*` (all nine components),
+ `client/src/services/tickets/{types.ts,constants.ts,keys.ts,hooks/*}`,
+ `client/src/components/notifications/*` + `client/src/services/notifications/*`,
+ `client/src/layout/CustomerLayout.tsx` + `NurseLayout.tsx` (the phase-2 chrome slots), and
+ `client/src/utils/date.ts` (you will add a time-only sibling to the two Shamsi formatters).
+- [.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md) — invoke
+ the skill; §2 tokens, §6 icon registry, §7 non-negotiables all bite here.
+- [product/business/12-messaging-and-emergencies.md](../../../product/business/12-messaging-and-emergencies.md)
+ (ticket-only channel, tel:-only emergency) and [product/business/14-notifications-and-admin.md](../../../product/business/14-notifications-and-admin.md).
+- REQ-028 in [../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
+ — the existing inbox-enrichment request you will extend (see §4).
+
+## 3. Scope — build this
+
+### 3.1 Ticket inbox → a real inbox
+
+- **Pagination + filters.** Expose what `useMyTickets` already accepts: status filter chips (همه / باز /
+ بسته — map to the `TicketStatus` values) and load-more/paging past pageSize 20. `keepPreviousData` is
+ already set, so chip switches must not flash.
+- **`TicketListCard` redesigned around unread + recency:** bold subject + unread pill (existing behavior)
+ **plus** a one-line last-message preview and a relative last-activity time (phase-1 relative-time
+ formatter, decaying to Shamsi). `unreadCount`/`lastMessageAt` are mock-only and `lastMessagePreview`/
+ last-author-role don't exist at all — extend the contract via REQ (§4) and build **mock-tolerant
+ fallbacks**: when the enrichment fields are absent (the real path today), the card degrades gracefully to
+ subject + status chip + `createdAt` time — never an empty slot, never a fake "0 unread". `referenceCode`
+ stays prominent.
+- **Support-entry unread badge in the chrome.** Add a `useSupportUnreadTotal()` (or equivalent) read
+ behind the `services/tickets` seam — the mock sums its `unreadCount`s; the real implementation returns
+ nothing until the REQ lands, and the badge renders **only when a signal exists**. Mount it as a `Badge`
+ on the phase-2 customer TopBar support entry (`CustomerLayout.tsx:48-53`) and the nurse nav's support
+ item — a minimal touch to phase-2-owned `layout/` files; note it in your report per the ownership rules.
+
+### 3.2 Live thread
+
+- **Poll while mounted.** Give `useTicket`/`useTicketThread` a `refetchInterval` (new
+ `TICKET_THREAD_REFETCH_INTERVAL` constant in `services/tickets/constants.ts`; ~15s is proportionate) —
+ TanStack Query only polls while the query has active observers, so this is automatically scoped to the
+ mounted thread screen. Do **not** set `refetchIntervalInBackground`. SSE replaces this later; the seam
+ stays. The global polling posture (§5) is unchanged.
+- **Scroll orchestration.** Thread opens scrolled to the **newest** message. On send: always scroll to
+ the new bubble. On receive: auto-scroll only when the user is already near-bottom (~120px); otherwise a
+ floating «پیام جدید ↓» pill that scrolls-to-newest on tap and dismisses on reaching bottom. Build this
+ as a reusable hook (e.g. `useThreadScroll`) — phase 11's admin thread has the inverse bug (a 520px
+ scrollbox that also opens at the top) and will consume it (§3.7).
+- **Chat typography.** In `TicketMessageList`: centered Shamsi date separators (امروز / دیروز / ۲۵ تیر —
+ derive day labels from the existing `Intl` `fa-IR-u-ca-persian` plumbing in `utils/date.ts`; add a
+ time-only `formatShamsiTime` there as a minimal foundation extension); consecutive same-author messages
+ group under one author label; bubbles show **hh:mm only** (the full date lives on the separator);
+ `authorRole === 'system'` renders as a centered neutral event line (chip-style), not a bubble — a
+ coordination thread must read as a timeline, not a stranger's messages.
+- **Fix the bidi timestamp bug.** Remove `direction: 'ltr'` from the bubble time label
+ (`MessageBubble.tsx:70`) — with hh:mm-only Persian-digit stamps no forced direction is needed. Keep the
+ forced LTR **only** on the Latin `referenceCode`.
+- **Sticky composer separation.** The sticky strip (`TicketThreadScreen.tsx:100-107`) is a bare bgcolor
+ block — give it a real top hairline (`divider`) or a phase-0 elevation token so bubbles no longer scroll
+ flush into the input. While there, align the messaging surface widths (inbox 640 / thread 720) to one.
+
+### 3.3 Composer
+
+- **Enter semantics per input modality.** Enter=send + Shift+Enter=newline **on desktop only**; on touch
+ (coarse pointer — `matchMedia('(pointer: coarse)')` or `useIsMobile()`), Enter inserts a newline and the
+ explicit send button is the only send path. The customer shell is mobile-first; touch keyboards have no
+ Shift+Enter.
+- **Retry-in-place on failure.** Today `usePostMessage` rolls the optimistic bubble back, leaving only
+ caption text. Instead: keep the failed bubble in place with `sendStatus: 'failed'` (the
+ `MessageSendStatus` union already includes it — `types.ts:40`), error-token accented, with «تلاش مجدد»
+ (re-mutates with the **same** `clientMessageId`) and a delete affordance that restores the text to the
+ composer. Announce the failure via `role="alert"`/`aria-live` — screen readers are currently never told.
+ The invariant that survives any mechanism change: **a failure never loses typed text**, and
+ clientMessageId reconciliation never double-renders (§5). Update the co-located tests to prove both.
+- **Attachment affordance — designed, gated.** Refund/coordination tickets need photo evidence; the
+ object-storage seam exists server-side (verification docs). Design the composer attachment button +
+ pending-upload chip now, but **render it only when the contract lands** (REQ in §4) — behind a
+ capability flag in `services/tickets/constants.ts`, default off. No dead buttons in production.
+- **Mirror the send icon in RTL.** stylis-plugin-rtl flips CSS, not SVG glyphs; Material's own RTL list
+ names Send as must-mirror. Use the phase-0 auto-mirroring icon strategy for the `send` registry entry
+ (`AppIcon/config.ts:85,187`); if phase 0 shipped no such mechanism, add a minimal registry-level mirror
+ (`scaleX(-1)` under `dir="rtl"`) and note the foundation extension in your report.
+
+### 3.4 Emergency affordance right-sizing
+
+- Keep the **full** `EmergencyBanner` (error accent + tel: click-to-call) **only where the phone exists**:
+ the nurse post-confirmation booking read (`BookingSupportEntry`). That placement is untouched.
+- In both ticket inboxes, replace the permanent banner (`TicketInboxScreen.tsx:57`) with a **compact,
+ neutral «موارد اضطراری» row** (collapsed by default, expands to the playbook copy + "open a ticket").
+ Rewrite the customer-side copy so it no longer instructs calling a number the customer can never see.
+ tel:-only stays the law — no VoIP, no phone directory, nothing new out-of-band (§5).
+
+### 3.5 Notification center
+
+- **Day grouping:** section headers امروز / دیروز / این هفته, then Shamsi date headers for older items.
+- **Relative timestamps** via the phase-1 relative-time formatter («۵ دقیقه پیش»), decaying to Shamsi for
+ older rows — replacing the absolute `formatShamsiDateTime` on every row (`NotificationCenter.tsx:103`).
+- **Per-kind visual identity:** soft-tinted icon containers — booking teal, payout success, alert warning
+ — from `--bal-*` tokens only (add `-soft` tokens in **both** scheme blocks + `colors.ts` mirror if
+ phase 0 didn't ship them; note the extension).
+- **Non-navigable rows rendered non-interactive:** when `notificationDeepLink` returns null, render a
+ plain surface (no ButtonBase, no ripple, no pointer cursor) that still supports mark-read; navigable
+ rows get a trailing chevron (registry icon from phase 0/1 — register one if missing) and a visible
+ `:focus-visible` style. **Keep the mark-read UX as is** — per-row mark-read-on-open and mark-all-read.
+
+### 3.6 Bell behavior
+
+- **Desktop popover preview** on the nurse shell (and the admin shell once phase 11 gives it a feed —
+ §3.7): the bell opens a `Popover` with the 5 most recent notifications, mark-all-read, and «مشاهده همه»
+ linking to the full center. The popover fetches the list **on open** (reusing the `notificationKeys`
+ cache), never on the poll tick.
+- **Mobile keeps direct navigation** to the notification center — no popover on the customer shell.
+- Optional polish: a one-shot badge pulse when the count increases (respect `prefers-reduced-motion`).
+- **Do not regress the isolation:** ONLY the bell container subscribes to the polled count
+ (`useUnreadCount`) — the shell and the popover contents never do.
+
+### 3.7 Admin notifications placeholder — phase 11 handshake
+
+`admin/notifications/page.tsx` is a `PlaceholderScreen` the admin nav links to.
+[Phase 11](ui-phase-11-admin-and-partner-console.md) owns the admin tree and decides whether to build an
+admin alert feed. **This phase's job:** if phase 11 hasn't shipped that feed when this runs, hide the dead
+admin nav entry (a "coming soon" page in a staff backoffice erodes trust) and record the handshake in your
+report. Export `useThreadScroll` (§3.2) and the bell popover as consumables — phase 11's admin thread
+scrollbox needs the same scroll fix. Do not build admin surfaces here.
+
+## 4. Mocks & seams in this phase
+
+**No new mocks or seams.** Everything stays behind the existing `services/tickets` and
+`services/notifications` seams; the mock ticket store keeps supplying the enrichment fields so the full
+inbox design is demonstrable offline, and the real path degrades gracefully per §3.1.
+
+Backend gaps become REQ entries appended to
+[../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md).
+REQ-001…038 are taken — **check the tracker's highest number at execution time** and number onward:
+
+- **REQ-039 (indicative) — Ticket inbox enrichment, extension of REQ-028:** re-assert
+ `unreadCount` + `lastMessageAt` on `TicketSummaryDto`, **add** `lastMessagePreview` (first ~80 chars,
+ internal notes excluded server-side) + the last message's author role, plus an unread-**total** read for
+ the chrome badge. Cross-reference REQ-028 rather than duplicating its rationale.
+- **REQ-040 (indicative) — Ticket message photo attachments:** upload + serve via the existing
+ object-storage seam, message-attachment linkage, size/type limits. Gates the §3.3 attachment affordance.
+
+## 5. Critical rules you must not get wrong
+
+- **The optimistic-send architecture stays.** clientMessageId reconciliation (never a double bubble), no
+ typed text ever lost on failure, composer remount-keyed per ticketId so drafts/in-flight state never
+ cross threads. §3.3 changes the failure *presentation*, not these invariants — tests must prove them.
+- **`is_internal` NEVER appears in user-app types or UI.** The user-side `services/tickets` types don't
+ model it and no component renders it; the REQ you file must keep internal notes excluded from
+ `lastMessagePreview` server-side. Airtight — do not regress.
+- **referenceCode prominence stays** — inbox card, thread header, creation-success dialog, LTR-forced.
+- **Polling stays polite.** The 60s auth-gated count poll remains the only global poll; the thread poll is
+ strictly while-mounted; lists refetch on focus/invalidation, never on an interval.
+- **Emergency is tel:-only, nurse-post-confirmation-only.** No VoIP, no new phone surfaces, no contact
+ directory — the anti-disintermediation rule is product law.
+- **Design-contract non-negotiables:** every new string in **both** catalogs (ICU plurals for unread
+ counts); colors from `--bal-*` tokens / palette keys, never hexes; logical properties only (the
+ bubble-tail `borderStartEndRadius` pattern is the house style); verify dark mode on every new tint;
+ MUI v9 API only; icon registry, not raw imports; co-located `*.test.tsx` for every touched/new shared
+ component; `clientFetch`/cookie rules untouched.
+
+## 6. Definition of Done
+
+On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):
+
+- [ ] `npm run check` green; `npm run test:ci` green including updated messaging/notification component
+ tests; `en.json`/`fa.json` in sync.
+- [ ] Inbox: status chips filter, paging reaches ticket #21+, cards show preview/unread/relative-time from
+ the mock and degrade gracefully (no empty slots) on the real API.
+- [ ] Thread: opens at the newest message; a reply arriving while mounted appears within the poll interval;
+ scrolled-up + new message shows «پیام جدید ↓»; date separators + grouping + hh:mm stamps + centered
+ system events render; the fa timestamp no longer bidi-scrambles.
+- [ ] Composer: touch Enter newlines, desktop Enter sends; a failed send leaves a retry-able failed bubble
+ (aria-live announced) and retry never duplicates; the send icon points out of the field in fa.
+- [ ] Inboxes show the compact emergency row (no permanent red banner); the nurse booking-detail tel:
+ banner is unchanged.
+- [ ] Notification center is day-grouped with relative times and per-kind tints; null-deepLink rows don't
+ ripple; mark-read/mark-all still work. Nurse desktop bell opens the popover; customer mobile bell
+ still navigates.
+- [ ] Admin nav no longer links to a placeholder (hidden, or phase 11's feed exists).
+- [ ] Visual verification on the four axes — `/fa` + `/en` × light + dark — mobile **and** desktop.
+- [ ] REQs filed in the tracker with correct next numbers; no `server/` edits.
+
+## 7. How to test (what a human can verify after this phase)
+
+1. Flip `USE_TICKETS_MOCK = true`, open `/fa` customer → پشتیبانی: inbox shows unread pills, previews,
+ relative times; filter by بسته; page past 20 tickets; the TopBar support entry shows the unread badge.
+2. Flip the mock off (real API): cards show subject + status + Shamsi time — nothing broken or blank; the
+ badge simply doesn't render.
+3. Open a long thread → opens at the newest message of a date-separated, author-grouped conversation with
+ centered system events. Scroll up, have the other side reply → «پیام جدید ↓»; tap → scrolls to newest.
+4. Send a message → bubble appears instantly, hh:mm stamp on confirm. Kill the API and send → failed
+ bubble with «تلاش مجدد»; restore the API, retry → exactly one bubble.
+5. On a touch viewport (devtools emulation), Enter in the composer inserts a newline; on desktop, Enter
+ sends. In `/fa`, the send arrow points out of the field.
+6. Inbox shows a compact «موارد اضطراری» row that expands to the playbook; the red click-to-call banner
+ appears **only** on the nurse's confirmed-booking detail.
+7. Notification center (`/fa` + `/en`, light + dark): امروز/دیروز groups, «۵ دقیقه پیش» decaying to
+ Shamsi, tinted per-kind icons; a null-deepLink row doesn't ripple; navigable rows show a chevron.
+8. Nurse desktop: bell opens the popover (5 recent + mark-all + «مشاهده همه»); customer mobile: bell
+ navigates. In devtools: only the count endpoint polls, plus the thread endpoint while a thread is open.
+9. Admin shell: no dead "notifications — coming soon" nav entry.
+
+## 8. Hand off & document (close the phase)
+
+- Update `client/CLAUDE.md` "Project Structure" if you added components/hooks folders (thread scroll hook,
+ popover, emergency row) — same change, per the working agreements.
+- Write the report at `dev/shared-working-context/reports/ui-phase-10-report.md`: what shipped, the REQ
+ numbers filed (with the REQ-028 cross-reference), the foundation files minimally extended (layout badge
+ slots, `formatShamsiTime`, icon mirror, `-soft` tokens — per the README ownership rules), the phase-11
+ handshake state, and the mock-tolerant degradations that light up when REQ-039 lands.
+- Save a memory note per operating-rules §8: messaging/notifications are now chat-grade; the surviving
+ invariants (optimistic-send, is_internal boundary, polite polling, tel:-only emergency); open REQ gates.
diff --git a/dev/post-phase/ui/ui-phase-11-admin-and-partner-console.md b/dev/post-phase/ui/ui-phase-11-admin-and-partner-console.md
new file mode 100644
index 0000000..1e1bd71
--- /dev/null
+++ b/dev/post-phase/ui/ui-phase-11-admin-and-partner-console.md
@@ -0,0 +1,284 @@
+# UI Phase 11 — Admin & partner console
+
+> **Mission:** the backoffice has a real primitive layer — `AdminDataTable`, `ConfirmDialog`, `AdminPager`,
+> the four-state list pattern — under daily-use ergonomics gaps: no URL-synced state (back/refresh loses
+> the queue), audited actions targeting people by hand-typed numeric ID, a trust queue you can't search,
+> a ticket console that can never close a case, Gregorian date inputs in a Shamsi product, and a partner
+> portal showing raw English `snake_case` statuses to Persian center staff. Make the console **fast and
+> safe** for the ops desk and the portal **professional** — while keeping the density of a work tool.
+>
+> **Track:** frontend · **Depends on:** [Phases 0–2](ui-phase-2-shells-and-navigation.md) ·
+> **Unlocks:** the ops desk gets speed and safety, partners get a professional portal
+>
+> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md) and invoke the frontend-designer skill — both are mandatory.**
+
+## 1. Context — where this sits
+
+Trust, money, and dispute resolution all converge on `/admin`; the partner portal is the only surface an
+external business ever sees. The audit found the composite layer unusually good (one table, one pager, one
+confirm pattern, zero hard-coded hexes, Shamsi/Toman formatting throughout) and everything *around* it
+thin. Phase 2 replaced the starter shell (dense chrome, working sidebar active-state, locale-aware nav);
+phase 1 shipped PageHeader, JalaliDatePicker, StatusChip v2. This phase is the **workflow layer**. All
+defects below were re-verified in code on 2026-07-16:
+
+- **No URL-synced list state anywhere.** `admin/tickets/page.tsx:39-41` holds applied filters + page
+ in `useState` (every queue page does); detail pages hand-roll `router.push` "back" buttons that land
+ on page 1 (`tickets/[id]/page.tsx:90`).
+- **Rows beyond page 1 are unreachable on config and holidays.** `admin/config/page.tsx:63`
+ (`usePlatformConfigs(1)`), `:182` (history drawer), and `admin/holidays/page.tsx:38`
+ (`useHolidays({}, 1)`) are hard-wired to page 1 with no pager — later rows are invisible/uneditable.
+- **Raw-ID targeting on audited actions.** `admin/roles/page.tsx:176-182` grants roles via a bare
+ `type="number"` TextField; same pattern for partner-center admin and sponsored-nurse assignment
+ (`partners/page.tsx`, `partners/[id]/page.tsx`). One mistyped digit grants `super_admin` to a
+ stranger — and `admin/users/page.tsx` is a `PlaceholderScreen`, so there's nowhere to look an ID up.
+ Related: alert assign-to-self silently falls back to user #1 (`admin/alerts/page.tsx:36` —
+ `const meId = authState.currentUser?.id ?? 1;`).
+- **The flagship trust queue can't search or prioritize.** `AdminVerificationQueueFilters` is
+ `{ status?: 'pending' | 'in_review' }` (`services/verification/types.ts:177-179`) — no name/phone
+ search, no counts, no age signal; `AdminDataTable.tsx` has no sort affordance at all.
+- **A resolved ticket can never leave the queue.** `services/tickets/hooks/` has **no
+ close/reopen/assign mutation**; the thread header (`tickets/[id]/page.tsx:118-127`) shows read-only
+ chips. The message list (`:150`) opens scrolled to the oldest message; internal-note mode (`:163-190`)
+ is only a small toggle — composer and send button look identical in both modes.
+- **UTC off-by-one on payout window defaults.** `admin/payouts/page.tsx:56` uses
+ `d.toISOString().slice(0, 10)` — near Tehran midnight the prefilled window is yesterday. The final
+ run confirm (`:365-371`) is generic copy with no money-movement summary.
+- **Partners see raw wire codes.** `partner/bookings/page.tsx:17,45,66-69` renders
+ `pending_payment`/`in_progress` literally in the filter menu and table chip — untranslated,
+ un-StatusChip'd, shown to external Persian-speaking staff.
+- Small verified cleanups: dead ternary `config/page.tsx:152` (both branches `'text'`); misleading
+ `holidays/page.tsx:106` (`TODAY_ISO = ''` claims to be seeded — the field starts blank); static
+ chevron, no `aria-expanded` on `AuditLogRow.tsx`; pager indicator has no total (callers pass only
+ `{ page }`, e.g. `partner/bookings/page.tsx:101`, though every caller computes `pageCount`).
+
+**What already exists (do not rebuild):**
+
+- The composite layer in `client/src/components/admin/` — `AdminDataTable`, `AdminPageHeader`,
+ `AdminPager`, `AdminEmptyState`, `AdminErrorState`, `ConfirmDialog` — used by every console and
+ unit-tested. **Restyle/extend these; never fork per-page markup.**
+- The domain composites: `RefundPanel`, `DocumentViewer` (signed URLs + expired→re-request),
+ `AdminMessageBubble`, `SupportAlertCard`, `ConfigRow`, `AuditLogRow`, `PartnerSettlementRow`.
+- The draft-vs-applied filter pattern on tickets/audit (typing never refetches; Apply commits the key)
+ and the four-state list pattern (skeleton → error-with-retry → empty → table) on every list page.
+- Phase 1's `PageHeader`, `JalaliDatePicker`, `StatusChip`; phase 2's admin chrome + locale-aware nav.
+- The URL-as-filter-carrier pattern proven in `client/src/services/search/filterParams.ts` (C1 writes
+ the URL, C2 reads it back into the query-key object) — the model for 3.1.
+
+## 2. Required reading (do this first)
+
+- [audit/admin-partner.md](audit/admin-partner.md) — the full evidence: 20 problems, 11 opportunities,
+ the keep-list §5 restates.
+- [../../../.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md) —
+ invoke the skill; its token/wrapper/icon/RTL rules bind every deliverable below.
+- Code, in this order: `client/src/components/admin/` (all of it); `admin/tickets/page.tsx` +
+ `tickets/[id]/page.tsx` (draft-vs-applied + the thread); `admin/verification/page.tsx` +
+ `verification/[nurseId]/page.tsx` (the case-view bones: StepCard, DocumentViewer, credential form);
+ `admin/{payouts,roles,partners,alerts,config,holidays,audit}/…`; the three `partner/*` pages;
+ `services/search/filterParams.ts`.
+- Service seams you'll touch: `services/{tickets,verification,payouts,admin,partnerCenter}` — note
+ which are mock-primary (`USE_*_MOCK` in each domain's `constants.ts`) before wiring anything.
+- `client/CLAUDE.md` "Golden rules" + Project Structure; `product/business/` for verification/payout
+ business rules (never infer them from code).
+- [../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
+ — REQ-028's delivery note matters for 3.4 (admin queue unread is explicitly served as `0`).
+
+## 3. Scope — build this
+
+### 3.1 `useAdminListState` — URL-synced worklist state, adopted everywhere
+
+Build `client/src/hooks/useAdminListState.ts` (+ co-located test): a small generic hook that mirrors
+**applied** filters + page into `searchParams` (read on mount → initial state; `router.replace` on
+apply/page-change) the way `services/search/filterParams.ts` already proves. Draft filter state stays
+local — only *applied* state hits the URL. Adopt it on **all** admin queue pages — tickets, audit,
+verification, reviews, payouts, partners, alerts, holidays, config — and the three partner lists.
+Detail-page "back" becomes a real `router.back()` (falling back to the list route), so queue position
+survives back/refresh/share-a-link. While you're in there, **fix the hard-wired page-1 reads**: give
+config, its history drawer, and holidays real page state + `AdminPager` (the hooks already accept a
+page argument — callers pass `1`).
+
+### 3.2 Kill raw-ID targeting — `UserPicker` / `NursePicker`
+
+Build `client/src/components/admin/UserPicker/` (and a thin `NursePicker` variant), an async MUI
+`Autocomplete` that searches by name/phone and renders **name + masked phone + `#id`** per option. Drop
+it into role grants (`roles/page.tsx`), partner-center admin assignment (`partners/page.tsx`),
+sponsored-nurse assignment (`partners/[id]/page.tsx`), and alert assignment. The resolved **name** is
+echoed into the `ConfirmDialog` body (e.g. «اعطای نقش مدیر مالی به مریم احمدی (۰۹۱۲***۴۵۶۷)؟») — never
+just `#42`. `services/admin` has **no user-search endpoint** (verified) — file the REQ (§4) and back
+the picker with a `mockApi` implementation behind the existing seam.
+
+**Fix the alert assign-to-self fallback:** `alerts/page.tsx:36` must never default to user `1`. Disable
+"assign to me" until `authState.currentUser?.id` is hydrated (tooltip: «در حال بارگذاری حساب شما…»).
+
+### 3.3 Verification desk — the flagship trust queue
+
+- **Status tabs with counts** («در انتظار (۱۲)» / «در حال بررسی (۴)») replacing the lone select — counts
+ need the queue read extended (REQ, §4); render the tabs without counts until it lands.
+- **Name/phone search** — `AdminVerificationQueueFilters` has only `status`; add the search field behind
+ the draft-vs-applied pattern and file the REQ for the query param.
+- **Waiting-time column** — client-computed age from the served `submittedAt` (display-only): relative
+ Shamsi age with SLA coloring (`--bal-warning` past 48h, `--bal-error` past 96h; named constants).
+- **Keyboard-friendly next-case flow** — from `verification/[nurseId]`, «پرونده بعدی»/«پرونده قبلی»
+ affordances (+ arrow-key bindings) walking the current queue order via the 3.1 URL state — a reviewer
+ never round-trips to the list between cases. A full split-pane case view is **(DEFERRED —
+ post-chain)**: next/prev delivers the throughput win at a fraction of the layout risk.
+- `DocumentViewer`'s signed-URL flow (on-demand fetch, expired→re-request) is **do-not-regress**.
+
+### 3.4 Ticket console — lifecycle + a safe composer
+
+- **Close/reopen + assign-to-me on the thread header.** The b15 contract exposes no close mutation
+ (verified: `services/tickets/hooks/` has none). Add `useCloseTicket`/`useReopenTicket`/
+ `useAssignTicket` hooks with full `mockApi` implementations and `clientApi` mapped to the proposed
+ routes; file the REQ (§4). On the real path, gate the controls behind a `TICKET_LIFECYCLE_ENABLED`
+ constant in `services/tickets/constants.ts` — never show a button that can only 404. Close is
+ `ConfirmDialog`-guarded.
+- **Queue columns: unread + last-activity.** REQ-028 delivered `unreadCount`/`lastMessageAt` for the
+ *user* list, but the admin queue is explicitly served `unreadCount = 0` (tracker delivery note).
+ [Phase 10](ui-phase-10-messaging-and-notifications.md) owns filing the admin-side extension —
+ **reference its REQ, don't double-file**; render the columns when present, fall back to `createdAt`.
+- **Scroll-to-latest on open** — anchor the `:150` message Box to the newest message on thread load.
+- **Internal-note mode made unmistakable:** when `internal` is active, the composer Paper gets an
+ amber surface (`--bal-warning` soft, both schemes) and the send button label swaps to
+ «ثبت یادداشت داخلی» — the safety cue lives *on the action*, not only in the toggle above.
+
+### 3.5 Money-desk safety
+
+- **Payout run confirm shows the movement summary:** batch total (Toman via the shared formatter),
+ nurse count, and processing date — all from the server preview the dialog already fetched — plus a
+ **typed confirmation** (type «تایید» or the exact amount) enabling the final-run confirm button: the
+ standard guard for an irreversible money movement.
+- **Fix the UTC off-by-one:** replace `payouts/page.tsx:56`'s `toISOString().slice(0,10)` with
+ local-date formatting (and adopt phase 1's `JalaliDatePicker` for the window inputs — 3.6).
+- **Reconcile/retry polish:** transfer-reference entry stays `dir="ltr"`; failed payout rows get a
+ visible failure reason and a retry affordance through `useRetryPayout`; localize skipped-reason
+ strings if served as codes (raw free-text reasons stay `dir="ltr"` as today).
+
+### 3.6 Primitives v2 (extend, never fork)
+
+- **`AdminDataTable`:** optional per-column server-param sort (a `sort` callback + direction chevron —
+ the filter object is already the query key, so sort is just another applied param through 3.1);
+ sticky header for long pages; per-column `minWidth`; a footer line «نمایش ۱–۲۰ از ۱۲۴» — callers
+ already hold `total`. Keep `align: 'inherit'` (RTL-safe) and the horizontal-scroll container.
+- **`AdminPager`:** take `total`, restore «صفحه {page} از {total}» in the admin i18n namespace (the
+ non-admin namespace kept it), locale digits via the existing formatting utils. Update both tests.
+- **Detail headers:** unify the four divergent patterns (verification case = `AdminPageHeader` + back;
+ ticket thread = h6-in-Paper; payout batch = raw h5; partner-center detail = h5 + a back button
+ misusing the `partners` icon) onto phase 1's shared `PageHeader` (title, back affordance, chips
+ slot). If `PageHeader` lacks a slot, extend it minimally and note it (ownership rule — no fork).
+- **Jalali date inputs everywhere:** adopt phase 1's `JalaliDatePicker` (Shamsi display, ISO-Gregorian
+ wire value) for audit from/to, payout windows, holiday date, and credential issued/expires
+ (`verification/[nurseId]/page.tsx`). No native `type="date"` remains under `/admin`.
+- **`AuditLogRow`:** rotate the chevron on expand, add `aria-expanded` + button semantics; resolve
+ actor IDs to names via 3.2's batch id→label lookup (same REQ), falling back to `#id` until it lands.
+
+### 3.7 Partner portal — professional, light-touch
+
+- **Localize booking statuses** (the worst partner-facing defect, verified at
+ `partner/bookings/page.tsx:17,45,66-69`): map the seven codes to `StatusChip` kinds + fa/en labels in
+ both the filter menu and the table chip. No raw wire code ever reaches a partner's screen again.
+- **Scoped read-only booking detail:** link each sponsored-booking row to a summary detail (dates,
+ status timeline, patient display name — no clinical data). `services/partnerCenter` has only the
+ list read — file the REQ; build the detail mock-tolerant behind the seam.
+- **CSV export on settlement** — client-side, current result set, UTF-8 **with BOM** (so Excel renders
+ Persian correctly) + CRLF; a small `toCsv` util with a co-located test — for the center's accountant.
+- **Portal identity in the chrome:** carry the center's name + MoR state persistently across portal
+ pages (home already shows the MoR `StatusChip`; lift a compact identity block via `useMyPartnerCenter`).
+ If this touches `layout/`, extend phase 2's shell minimally and note it.
+
+### 3.8 Dead ends & honest nav
+
+- **Users console:** replace `admin/users/page.tsx`'s placeholder with a **read-first directory**
+ (search by phone/name via the 3.2 endpoint, role chips, links into audit/tickets) **only if** the
+ user-search REQ is granted during this phase. Otherwise **hide the nav entry** (same rule phase 10
+ applies to admin notifications) and say so honestly in the report — a production nav ships no
+ placeholder dead-ends.
+- Small verified cleanups: delete the dead ternary at `config/page.tsx:152`; fix the misleading
+ `TODAY_ISO` constant/comment at `holidays/page.tsx:106` (seed today's local date or drop the lie).
+
+## 4. Mocks & seams in this phase
+
+No new seams. Every backend gap becomes a REQ appended to
+[../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
+— **REQ-001…038 are taken; check the tracker's current tail (other UI phases may have appended) and
+number onward.** File, at minimum: **(1) admin user lookup** — search by name/phone →
+`{ id, displayName, maskedPhone, roles }` + a batch id→label endpoint (powers 3.2 and 3.6);
+**(2) verification queue enrichment** — name/phone search + per-status counts (3.3), optionally a
+`submittedAt` sort param; **(3) ticket lifecycle mutations** — close/reopen + assign (3.4);
+**(4) partner scoped booking detail** — read-only summary (3.7). For each: build the UI complete against
+the domain's `mockApi`, map `clientApi` to the proposed route, and gate real-path controls that would
+otherwise 404. Do **not** re-file phase 10's admin ticket-queue enrichment — reference it.
+
+## 5. Critical rules you must not get wrong
+
+- **Server-authority posture stays.** `useAdminCapabilities` flags only *hide* controls; money is never
+ recomputed client-side (payout eligibility and the holiday shift come from the server; the 3.5
+ summary re-renders served preview numbers, never sums).
+- **`ConfirmDialog` on every audited/irreversible action** — approve/reject, moderate, revoke,
+ run/retry payout, resolve alert, close ticket — with required-reason gating where it exists today,
+ loading that disables both buttons, destructive color. The 3.2 pickers make its body *human*.
+- **`is_internal` never enters user-facing types** — the admin-only typing boundary from f14/f15 stays;
+ 3.4's composer work touches presentation only.
+- **Draft-vs-applied filtering stays** — typing never refetches; Apply commits the query key (and now
+ the URL). 3.1 syncs *applied* state only.
+- **Density is a feature.** Keep `size="small"`, dense tables, tight vertical rhythm — no consumer-app
+ whitespace or hero moments in a worklist tool.
+- **PII discipline stays:** write-then-masked settlement IBAN, never-echoed credential numbers, signed
+ document URLs, non-leaking partner access-denied state, masked phones in the new pickers.
+- Design contract: i18n in **both** catalogs; tokens not hexes (`--bal-*` in both scheme blocks); RTL
+ logical props (`borderInlineStart`, `align: 'inherit'`, `dir="ltr"` islands for IBANs/references);
+ dark mode by construction; MUI v9 API only; co-located tests for every shared component/hook touched;
+ `clientFetch`/cookie rules untouched.
+
+## 6. Definition of Done
+
+On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):
+
+- [ ] `npm run check` green; `npm run test:ci` green including updated tests for `AdminDataTable`,
+ `AdminPager`, `UserPicker`, `useAdminListState`, and the CSV util; `en.json`/`fa.json` in sync.
+- [ ] Filter + page state on every admin/partner queue survives refresh, browser back from a detail
+ page, and a pasted URL; config and holidays can page past page 1.
+- [ ] No `type="number"` user-ID input remains on any audited action; every confirm body names the
+ resolved person; alert assign-to-me can never target user #1.
+- [ ] Verification queue: search field + SLA-colored waiting-time column render; next/prev case
+ navigation works. Ticket thread: opens scrolled to the latest message; internal mode shows the
+ amber composer and «ثبت یادداشت داخلی» send label; close/reopen works on the mock path.
+- [ ] Payout run confirm shows batch total/count/date and requires the typed confirmation; window
+ defaults are correct at local midnight (no UTC drift).
+- [ ] Partner bookings show localized `StatusChip` statuses in filter and table; settlement exports a
+ CSV that opens correctly in Excel with Persian text.
+- [ ] No placeholder screen is reachable from the admin nav; REQs filed with correct sequential
+ numbers, no duplicate of phase 10's REQ.
+- [ ] Visual verification on all four axes (`/fa` + `/en` × light + dark), desktop-first (this is a
+ desk tool) with a mobile sanity pass on the partner portal.
+
+## 7. How to test (what a human can verify after this phase)
+
+1. `/fa/admin/tickets`: apply a status filter + go to page 2 → open a ticket → browser back → filter
+ and page intact; paste the list URL into a new tab → same view.
+2. `/fa/admin/config`: page past page 1 (mock or seeded rows) → edit a row from page 2 → saves; the
+ history drawer pages too.
+3. `/fa/admin/roles` → «اعطای نقش»: type a partial name → options show name + masked phone + id →
+ pick one → the ConfirmDialog names the person, not a number.
+4. `/fa/admin/alerts` before `/me` hydrates (throttle the network): "assign to me" is disabled.
+5. `/fa/admin/verification`: search a seeded nurse by name → row found; waiting-time column shows
+ amber/red for old cases; open a case → «پرونده بعدی» walks the queue without returning to the list.
+6. Open a long ticket thread → scrolled to the newest message; internal mode → composer turns amber,
+ send button reads «ثبت یادداشت داخلی»; close (mock path) → it leaves the open queue; reopen restores.
+7. `/fa/admin/payouts`: window defaults match today's local date; preview → run → the confirm shows
+ total/count/date and stays disabled until «تایید» (or the amount) is typed.
+8. `/fa/admin/audit`: pick from/to with the Jalali picker (no Gregorian native input); expand a row →
+ chevron rotates, `aria-expanded` toggles, actor shows a name (or `#id` fallback).
+9. `/fa/partner/bookings`: statuses render as Persian `StatusChip`s in table and filter;
+ `/fa/partner/settlement` → «خروجی CSV» opens in Excel with correct Persian.
+10. Every pager/footer reads «صفحه ۲ از ۷» / «نمایش ۱–۲۰ از ۱۲۴» with locale digits; verify the four
+ axes; no admin nav item leads to a placeholder.
+
+## 8. Hand off & document (close the phase)
+
+- Update `client/CLAUDE.md` (Project Structure) for new files: `useAdminListState`, `UserPicker`/
+ `NursePicker`, the CSV util, any `PageHeader` extension, hidden/added routes.
+- Write the report at `dev/shared-working-context/reports/ui-phase-11-report.md`: what shipped, the
+ exact REQ numbers filed, which controls are gated awaiting delivery, any minimal foundation
+ extensions to phase 1/2 files, and the users-console decision (built vs nav hidden).
+- Save a memory note per operating-rules §8: URL-synced admin list state, the picker pattern replacing
+ raw IDs, the ticket-lifecycle gating constant, the REQ numbers — phase 12 (copy/motion) sweeps these
+ surfaces.
diff --git a/dev/post-phase/ui/ui-phase-12-copy-motion-and-polish.md b/dev/post-phase/ui/ui-phase-12-copy-motion-and-polish.md
new file mode 100644
index 0000000..9d8f468
--- /dev/null
+++ b/dev/post-phase/ui/ui-phase-12-copy-motion-and-polish.md
@@ -0,0 +1,287 @@
+# UI Phase 12 — Copy, motion & final polish
+
+> **Mission:** the closing sweep of the UI chain — make the product read and move as one: a checked-in
+> Persian style guide enforced across both catalogs, the verified copy defects on trust-critical strings
+> fixed, a restrained motion language behind a single reduced-motion gate, an accessibility pass, and a final
+> four-axes QA of everything the chain touched. Copy is the cheapest trust lever Balinyaar has — and today
+> the brand name is spelled two ways, a nurse-facing EVV error reads as "there is no game-entrance", and one
+> BNPL sentence states the inverse of the intended risk allocation.
+>
+> **Track:** frontend · **Depends on:** all prior phases — [0](ui-phase-0-design-language.md) →
+> [11](ui-phase-11-admin-and-partner-console.md) (run this last) · **Unlocks:** ships the chain — one voice, one motion language, verified on all axes.
+> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md) and invoke the frontend-designer skill — both are mandatory.**
+
+## 1. Context — where this sits
+
+Balinyaar is a trust-first product in a market where written register carries weight: families decide whether
+to let a stranger into a parent's home partly on whether the product *sounds* careful. The catalogs are
+already hand-written — the weaknesses are craft, not tone. Verified in `client/messages/fa.json` / `en.json`
+(line numbers spot-checked 2026-07-16; earlier phases may shift them — grep the key, not the line):
+
+- **The brand name is spelled two ways:** «بالین یار» (plain space) in 5 keys — `common.brand` (53),
+ `auth.customer_title` (625), `auth.select_role_title` (645), `auth.account_error_body` (654),
+ `verification.start_body` (663) — vs «بالینیار» (ZWNJ) in 17: login and escrow copy disagree on the name.
+- **تأیید — the most frequent word in a verification product — appears 38× without hamza and 26× with**, no
+ pattern (`bank.status_verified_title` vs `admin.ver_pass`); ~64 occurrences to sweep.
+- **Two grammar bugs read as nonsense:** `booking.evv_no_open_check_in` (542) «ورود بازی برای ثبت خروج وجود
+ ندارد» and `admin.alert_empty` (1360) «هشدار بازی وجود ندارد» — the indefinite ی attached to the wrong word.
+ And a typo sits on a trust chip: `bank.status_verified_chip` (180) reads «تاییدشد» (missing final ه) — the
+ chip a nurse stares at while IBAN ownership verification runs.
+- **One en arrow points the wrong way:** `booking.continue_payment` (en 445) is "Continue to payment ←" while
+ its sibling `payment.cta_pay` (577) is "…→". Five keys per catalog embed ←/→ literally (445, 577, 632, 633,
+ 1337) — directional glyphs inside translatable strings are how this bug happens.
+- **fa lacks ICU plurals where en has them:** `search.cta_view_results` (fa 350) is «مشاهده {count} پرستار» —
+ the primary search CTA can render «مشاهده ۰ پرستار»; en has ICU but its `=0` case renders "View no nurses".
+- **BNPL copy is banker's language with an ambiguous pronoun:** `bnpl.ownership_note` (857) «…ریسک نکول مشتری
+ کاملاً با اوست…» — «نکول» is credit-desk jargon and «با اوست» leaves the reader unsure whether the
+ *customer* carries the default risk (the intended meaning: the provider does).
+- **Policy numbers are hard-coded into legally-sensitive copy** the admin config panel can change: the ۷۲-hour
+ dispute window (`payouts.explainer_point_2`, 958), the ۲۴-hour cancellation tiers (`refunds.lead_gt_24h/
+ lead_lt_24h`, 789–790), the ۷–۱۰-day refund ETA (`refunds.eta_business_days`, 850). One config edit makes
+ the UI lie — and **no public config read exists** (`platform_config/*` in `services/admin/` is admin-only).
+- **Motion and live-region a11y are absent, app-wide:** zero `prefers-reduced-motion` handling and zero
+ `aria-live` anywhere in `client/src`; `AppIconButton` names icon-only actions solely via Tooltip `title`,
+ dropped entirely when disabled (`AppIconButton.tsx:89–95`).
+
+**What already exists (do not rebuild):**
+
+- Phase 0's token extension (motion/focus/elevation tokens in `theme/tokens.css`) and single-weight icon
+ registry with a mirrored chevron — this phase *applies* motion tokens, it does not invent them.
+- Phase 1's shared primitives: `utils/number.ts` `formatNumber` (replacing the 25+ inline `fa-IR` ternaries),
+ the state-view kit, `CountdownTimer` v2, route-level `loading.tsx`/`error.tsx`.
+- Full key parity between `fa.json` and `en.json` (28 namespaces) with complete loading/empty/error/confirm/
+ toast coverage per namespace, and the trust copy that is already right (keep-list, §5):
+ `payment.escrow_notice`, `payouts.explainer_point_1–3`, calming refund-status vocabulary, two-stage
+ disclosure copy, the specific verification-failure reasons.
+- Phases 4/6/8/9 may already have fixed items this phase lists conditionally (3.2 address hint, 3.5 pipeline
+ naming, 3.7 cities, 3.10 desktop layouts) — **read their reports first** and skip what is done.
+
+## 2. Required reading (do this first)
+
+- [audit/microcopy.md](audit/microcopy.md) — the 16 copy problems + 7 opportunities this phase executes; its
+ keep-list is binding. Then [audit/cross-cutting-ux.md](audit/cross-cutting-ux.md) — motion/a11y/responsive
+ findings (AppIconButton, motion absence, desktop phone-column) and its keep-list.
+- Prior phase reports in `../../shared-working-context/reports/` (`ui-phase-0` … `ui-phase-11`) — to learn
+ which conditional items are already done and what naming phase 8 chose for the verification journey.
+- `client/messages/fa.json` + `en.json` — read the namespaces you will sweep (booking, verification, payment,
+ auth, common, bnpl, search, refunds, payouts, admin, shell, tickets, bank, address).
+- Code (under `client/src/`): `components/common/AppIconButton/AppIconButton.tsx`,
+ `components/CountdownTimer/`, `components/EscrowNotice/EscrowNotice.tsx`, `theme/tokens.css` (phase-0
+ motion/focus tokens), `utils/number.ts` (phase-1 `formatNumber`), and for 3.10
+ `app/[locale]/(private-routes)/(customer)/bookings/checkout/` + `…/(customer)/search/results/page.tsx`.
+- Product: [../../../product/overview/platform-summary.md](../../../product/overview/platform-summary.md)
+ §"Glossary" — consult it **before** deciding پرستار/مددجو/بیمار; the cancellation/refund docs under
+ [../../../product/business/](../../../product/business/index.md) before touching policy-number copy.
+- The design contract: [../../../.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md)
+ (§7 non-negotiables) and `client/CLAUDE.md` "Golden rules". REQ tracker:
+ [../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
+ — find the tail (REQ-038 at authoring time; earlier UI phases may have appended) and number onward.
+
+## 3. Scope — build this
+
+### 3.1 The Persian style guide — `client/messages/STYLE.md` + enforcement
+
+Write a one-page style guide the catalogs are then swept against, fixing these decisions:
+
+- **Brand = «بالینیار» (ZWNJ), always.** Sweep the 5 plain-space keys.
+- **Hamza form «تأیید»** everywhere (~64 occurrences to normalize — the 38 hamza-less ones change).
+- **One form of جستجو.** Standardize on «جستجو» (the 9-key majority; fold in the 2 «جستوجو» keys).
+- **ZWNJ rules:** می + verb, plural ها, compound adjectives («تأییدشده»); codify with examples.
+- **Punctuation & quotes:** Persian «،» / «؛» / guillemets; en uses straight apostrophes (the admin
+ namespace's curly 'don’t' normalizes to the file's straight-quote majority).
+- **Domain glossary:** پرستار / بیمار vs مددجو — decide against the product glossary; «مددجو» appears exactly
+ once (`booking.patient_label`), so adopt it consistently or drop the one-off. One status vocabulary shared
+ by nurse-facing and admin-facing keys (see 3.5).
+- **Shell naming system:** one metaphor per audience class instead of today's four (`shell.*`, fa 56–61:
+ اپلیکیشن/نما/کنسول/پرتال). Recommended: end-user shells = «اپلیکیشن» (خانواده، پرستار), back-office
+ shells = «کنسول» (مدیریت، همکار).
+- **Register:** formal شما with polite imperatives (کنید) — already consistent; codify so it cannot drift.
+- **Digits policy:** Persian digits in fa for both literals and interpolations (mechanics in 3.3).
+
+**Enforcement:** add a ~30-line Node script (`client/scripts/check-copy.mjs`) that greps `fa.json` for the
+banned variants (space-brand, hamza-less تایید, the «بازی» word-boundary trap, جستوجو, میگردد) and exits
+non-zero; wire it as `npm run lint:copy` and into `npm run check`. 60+ scattered findings become a one-time
+fix that cannot regress.
+
+### 3.2 Verified copy-bug fixes (each independently shippable)
+
+1. `booking.evv_no_open_check_in` → «ورودِ ثبتشدهای برای این ویزیت وجود ندارد؛ ابتدا ورود را ثبت کنید.»
+ and `admin.alert_empty` → «هشداری برای رسیدگی نیست.»
+2. `address.line_hint` «…برای یافتن در نیاز دارد» → «هر جزئیاتی که پرستار برای پیدا کردن منزل شما لازم
+ دارد.» — **only if phase 9 didn't**. And `bank.status_verified_chip` «تاییدشد» → «تأییدشده».
+3. Archaic passive «میگردد» → «میشود»: `refunds.confirm_restate` (814), `admin.mod_confirm_publish` (1314),
+ `admin.cfg_save_confirm_body` (1333) — three occurrences, not one as the audit counted.
+4. `tickets.thread_empty_body` comma splice → «هنوز پیامی نیست. هماهنگی را شروع کنید.»
+5. en normalization: `auth.nurse_subtitle` "licence" → "license" (American throughout — the file already uses
+ "center" 25×); straighten the admin namespace's curly apostrophes.
+6. Introduce EVV in Persian once, then abbreviate: a first-occurrence key «ثبت حضور الکترونیکی (EVV) — ورود و
+ خروج شما ثبت میشود تا ویزیت بدون اختلاف تأیید شود» on the nurse's visits surface (`booking.evv_*`, fa
+ 528–535), short chips («ثبت ورود») thereafter — EVV is why the nurse gets paid without arguments; sell it.
+
+### 3.3 ICU plurals + designed zero cases
+
+Every `{count}` key gets an ICU plural form in **both** catalogs with a designed `=0` case: fa
+`search.cta_view_results` (350), `results_count` (352), `reviews_count` (364), `booking.session_count` (515)
+plus a grep sweep for the rest. Zero states are copy, not numerals — «پرستاری یافت نشد» / "No nurses found";
+also fix en's awkward `=0` label ("View no nurses"). Digits policy: fa interpolations render Persian digits —
+use `{count, number}` / ICU `#` (locale-formatted by next-intl) in message keys, and phase 1's `formatNumber`
+where a raw value is interpolated at a call site; verify one of each renders «۳» not "3".
+
+### 3.4 Arrows out of strings
+
+Remove the five literal ←/→ glyphs per catalog (`booking.continue_payment`, `payment.cta_pay`,
+`auth.nurse_switch`, `auth.customer_switch`, `admin.cfg_history_change` — fa/en lines 445/577/632/633/1337)
+and fix en 445's wrong-direction arrow by construction: direction moves into auto-mirroring `endIcon` slots
+on the buttons involved (phase 0's chevron via the `AppIcon` registry; the RTL Emotion cache mirrors it).
+`admin.cfg_history_change` «{old} ← {new}» becomes two interpolations joined by a component-rendered mirrored
+arrow icon. Touch each call site; the strings become text-only.
+
+### 3.5 Trust-moments copy pass
+
+- **OTP screen reassurance:** `auth.customer_subtitle` (626) is just «با شماره موبایل خود وارد شوید» — add one
+ warm trust line (verified nurses + escrow, e.g. «پرستاران تأییدشده، پرداخت امن نزد بالینیار»).
+- **Checkout «چرا امن است»:** one line near the pay CTA linking escrow to the dispute process —
+ *supplementing*, never replacing, `payment.escrow_notice` (§5). And **post-payment what-happens-next:** the
+ confirmation states the next steps in order (nurse notified → coordination ticket → visit-day EVV).
+- **BNPL de-jargon:** rewrite `bnpl.ownership_note` (857) reader-first, plainly allocating risk to the
+ provider — e.g. «قسطها را مستقیماً به {provider} میپردازید؛ بالینیار مبلغ کامل را همان ابتدا دریافت
+ میکند و اگر قسطی پرداخت نشود، مسئولیت آن با ارائهدهنده است، نه شما و نه پرستار.» «نکول» disappears from
+ the catalog.
+- **Disambiguate the verification names:** «احراز هویت» names both the 7-step pipeline (`nav.verification`
+ 13, `verification.title` 658) *and* the KYC step inside it (`step_identity_kyc` 676, admin 1470). Rename
+ the pipeline «تأیید صلاحیت»; the KYC step keeps «احراز هویت». **Coordinate with phase 8's journey naming**
+ — if its report already renamed, adopt its term and only sweep stragglers.
+- **One rejected-status vocabulary:** unify رد شد / ناموفق / ردشده into one nurse-facing and one
+ admin-facing form (decide in STYLE.md; sweep `verification.*` and `admin.*` together — they cross-reference).
+
+### 3.6 Config-served policy numbers (REQ + client single-sourcing)
+
+Parameterize the hard-coded policy numbers: `payouts.explainer_point_2` takes `{hours}` (72),
+`refunds.lead_gt_24h`/`lead_lt_24h` take `{hours}` (24), `refunds.eta_business_days` takes `{minDays}`/
+`{maxDays}` (7–10). File a REQ (next free number) for a **public/authenticated policy-config read**
+(dispute-window hours, cancellation tiers + percentages, refund ETA) — verify first that no existing endpoint
+serves it (at authoring time only admin-scoped `platform_config/*` exists). Until served, single-source the
+constants in `client/src/constants/policy.ts` with a comment linking the REQ, and feed the interpolations
+from there. One config edit must never again silently make copy lie.
+
+### 3.7 Honest search empty state
+
+Replace `search.empty_suggest_city` (361, both catalogs) — «شهر نزدیک دیگری مانند مشهد، اصفهان یا شیراز را
+امتحان کنید» is nonsense advice for a Tehran-launch user — with honest relaxations already next to it (clear
+the district, widen filters) or interpolated actually-covered cities. **Only if phase 4 didn't.**
+
+### 3.8 Motion pass — one restrained language
+
+Apply phase 0's motion tokens app-wide; introduce nothing springy — this is a calm clinical product:
+
+- Page/content transitions: 150–200ms fade + small slide on route-group content; skeleton → content
+ crossfade (the phase-1 skeleton twins).
+- Dialog/bottom-sheet enter/exit via `theme.components` defaults (one place, not per-dialog); list-entrance
+ restraint — at most a subtle stagger on first paint, never re-animating on refetch.
+- **`prefers-reduced-motion` is respected via a single media-query gate in one place** (a token/theme-level
+ switch that zeroes durations) — today the app has zero handling; do not scatter per-component checks.
+
+### 3.9 A11y sweep
+
+- `AppIconButton`: add an `aria-label` passthrough that defaults from `title` and survives `disabled` (today
+ the Tooltip — the only name source — is dropped when disabled; `AppIconButton.tsx:89–95`). Update its test.
+- `aria-live` on the moments that change without focus: `CountdownTimer` (polite), async error/retry regions,
+ payment-status polling. Today `aria-live` appears **nowhere** in `client/src`.
+- Disclosure semantics (`aria-expanded` + `aria-controls`) on collapsibles — the earnings `ExplainerCard`
+ pattern and any phase-built equivalents.
+- Form label association audit (MUI does this when `label` is used — audit the hand-rolled ones); focus-visible
+ coverage check (phase 0 tokenized the ring — verify interactive elements show it); contrast re-check on
+ remaining terracotta *text* usages in both schemes.
+
+### 3.10 Desktop-aware layouts — the two worst phone-column offenders (scope-boxed)
+
+**Only if phases 4/6 didn't already:** above ~900px, (a) checkout (`(customer)/bookings/checkout/`) becomes
+two-column with a sticky order summary; (b) search results (`(customer)/search/results/page.tsx`) uses the
+width (wider cards or list + detail). Nothing else — a full responsive pass is (DEFERRED → post-chain).
+
+### 3.11 Final QA — the closing walkthrough
+
+Write a per-shell walkthrough checklist (customer funnel, nurse day-of + verification, admin desks, partner
+portal, auth/first-run) × `/fa`+`/en` × light+dark × mobile+desktop. Run it end-to-end; fix what it finds
+(anything non-trivial becomes a report note, not silent scope expansion). Attach the completed checklist to
+the phase report — it is the chain's shipping evidence.
+
+## 4. Mocks & seams in this phase
+
+None introduced. UI stays mock-tolerant behind the existing `services/{domain}` seams. The one backend gap is
+3.6's policy-config read: a REQ appended to
+[for-backend.md](../../shared-working-context/frontend/requests/for-backend.md) (REQ-001…038 taken at
+authoring time — check the tail for numbers earlier UI phases claimed, and number onward). Until delivered,
+the client single-sources the numbers in `constants/policy.ts`; no mock flag changes.
+
+## 5. Critical rules you must not get wrong
+
+- **`payment.escrow_notice` and product-mandated strings change only with a flagged product note.** The
+ escrow sentence, `payouts.explainer_point_1–3` (especially point 3's BNPL-fee-never-deducted guarantee),
+ the two-stage disclosure copy (`booking.notes_hint` / `booking.disclosure_note`), and the verification-failure
+ reasons are model trust writing — sweeps may normalize their spelling, never their meaning. If a rewrite
+ seems needed, flag it in the report instead.
+- **Don't regress the keep-lists** (both audit files): calming money-status vocabulary («در راه»، «نیازمند
+ بررسی»), culturally-tuned gender copy, formal شما, complete per-namespace state strings, admin
+ confirm-dialog operational candor, token discipline, RTL `dir="ltr"` islands, Shamsi-first formatting.
+- **en stays hand-written-idiomatic** — no machine-translation tone ("Queue clear — nothing to review" is the
+ bar). **Key parity fa/en stays absolute:** every add/rename lands in both catalogs in the same change.
+- **Don't rename stable message keys casually.** A key rename must grep and update every `t('…')` usage; if a
+ key's meaning is unchanged, keep its name and change only the value.
+- **Design-contract non-negotiables** that bite here: tokens not hexes (motion durations/easings are tokens,
+ not inline magic numbers); RTL logical props (arrows mirror by construction); dark mode on every touched
+ surface; MUI v9 API only; co-located tests for touched shared components (`AppIconButton`,
+ `CountdownTimer`, `EscrowNotice` all have `.test.tsx`); fetch/cookies rules untouched — no data-layer changes.
+- **Reduced motion is a hard gate, not a nice-to-have** — every animation collapses to no-motion under
+ `prefers-reduced-motion: reduce` via the single gate (3.8). **Frontend lane only:** the policy-config gap
+ is a REQ, never a `server/` edit.
+
+## 6. Definition of Done
+
+On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):
+
+- [ ] `npm run check` green (now including `lint:copy`); `npm run test:ci` green for touched shared
+ components (`AppIconButton`, `CountdownTimer`, any collapsible/state component touched).
+- [ ] `client/messages/STYLE.md` exists; `scripts/check-copy.mjs` passes with **zero** banned variants —
+ no plain-space brand, no hamza-less تایید, no «بازی» trap, no میگردد, one جستجو form.
+- [ ] `en.json`/`fa.json` in key parity; every `{count}` key has ICU plural + designed `=0` in both catalogs;
+ no literal ←/→ remains in either catalog (grep proves it).
+- [ ] `bnpl.ownership_note` plainly allocates default risk to the provider; «نکول» absent from the catalog.
+- [ ] Policy numbers interpolate from `constants/policy.ts`; the policy-config REQ is filed with a number.
+- [ ] `prefers-reduced-motion: reduce` disables all chain-added motion; icon-only buttons have accessible
+ names including when disabled; countdown/async-status regions have `aria-live`; touched collapsibles
+ expose `aria-expanded`.
+- [ ] Visual verification on the four axes — `/fa` + `/en` × light + dark — and mobile + desktop for 3.8/3.10
+ surfaces; the completed 3.11 walkthrough checklist is attached to the phase report.
+
+## 7. How to test (what a human can verify after this phase)
+
+1. `cd client && npm run check` — passes, including the new copy lint. Temporarily add «بالین یار» to any fa
+ value → `lint:copy` fails; revert.
+2. Log in as a customer (`/fa`): login, checkout escrow copy, and refund screens all spell «بالینیار»
+ identically; the OTP screen shows the new reassurance line.
+3. Search with filters that match nothing → «پرستاری یافت نشد» (not «۰ پرستار»; not "View no nurses" on
+ `/en`); counts render Persian digits on `/fa`.
+4. As a nurse, attempt EVV check-out without a check-in → the corrected sentence (no «ورود بازی»); the bank
+ screen's verified chip reads «تأییدشده»; the visits screen introduces «ثبت حضور الکترونیکی (EVV)» once.
+5. On `/en` checkout, "Continue to payment" carries a mirrored end-icon pointing forward (→ in LTR, mirrored
+ on `/fa`); grep both catalogs for `←|→` → zero hits.
+6. Open the BNPL comparison → the ownership note reads plainly, no «نکول», risk clearly on the provider.
+ Grep the catalogs for «۷۲ ساعته» → none; the keys take `{hours}` from `constants/policy.ts`; the REQ
+ exists in the tracker.
+7. Navigate between routes → 150–200ms calm fade/slide, skeletons crossfade into content; enable reduced
+ motion in DevTools → everything appears instantly.
+8. Inspect the a11y tree on a screen with icon-only buttons: each has a name, including a disabled one; a
+ running countdown announces politely (`aria-live`).
+9. Run the 3.11 checklist across all shells on the four axes + both widths; every row checked or fixed.
+
+## 8. Hand off & document (close the phase)
+
+- Update `client/CLAUDE.md`: mention `messages/STYLE.md` + `lint:copy` in the i18n/Golden-rules sections and
+ the reduced-motion gate location; update "Project Structure" if `scripts/` or `constants/policy.ts` are new.
+- Write the frontend report at `../../shared-working-context/reports/ui-phase-12-report.md`: the style-guide
+ decisions taken (hamza form, جستجو form, shell naming, status vocabulary, digits policy), swept-key counts,
+ conditional items skipped because an earlier phase did them, the completed QA checklist, and anything the
+ walkthrough found but deferred. List the REQ(s) filed with their numbers.
+- Save a memory note per operating-rules §8: the chain is complete — style guide + copy lint now enforce
+ Persian orthography; motion and a11y gates exist; record the STYLE.md decisions so future copy follows them.
diff --git a/dev/post-phase/ui/ui-phase-13-public-front-door.md b/dev/post-phase/ui/ui-phase-13-public-front-door.md
new file mode 100644
index 0000000..57dd6e6
--- /dev/null
+++ b/dev/post-phase/ui/ui-phase-13-public-front-door.md
@@ -0,0 +1,276 @@
+# UI Phase 13 — Public front door (optional)
+
+> **Mission (this phase is OPTIONAL — the app is complete without it):** today the entire anonymous web
+> surface of a trust-first marketplace is `/login`. A family evaluating Balinyaar cannot see a single
+> service, trust signal, or explanation of escrow before creating an account; there is no marketing page,
+> no per-page metadata, and the `robots.txt` is starter junk. This phase builds the public front door —
+> a landing at `/` for unauthenticated visitors, honest how-it-works and trust sections, and a real
+> SEO/metadata surface — and it starts by **framing the guest-browse product decision** (how deep the
+> anonymous experience goes) so scope is decided, not drifted into.
+>
+> **Track:** frontend · **Depends on:** [Phases 0–2](ui-phase-2-shells-and-navigation.md) · a product
+> decision on guest browse (§3.1) · **Unlocks:** marketing, SEO, and an acquisition funnel that starts
+> before login
+> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md) and invoke the frontend-designer skill — both are mandatory.**
+
+## 1. Context — where this sits
+
+Balinyaar's product *is* trust — verified nurses, escrow-held payments, payout only after a confirmed
+check-out — yet none of that story exists outside an authenticated session. Diagnosed current state
+(all verified in code):
+
+1. **There is no public surface.** `client/src/app/[locale]/(public-routes)/` contains exactly two
+ files: `layout.tsx` and `login/page.tsx`. Home, search, results, and nurse profiles are all wrapped
+ in `RoleGuard(customer)` via `(customer)/layout.tsx` ([audit/customer-storefront.md](audit/customer-storefront.md), problem #1).
+2. **The middleware sends every guest to `/login`.** `client/middleware.ts:23-30` checks
+ `PUBLIC_PATHS.some((p) => pathWithoutLocale.startsWith(p))` and `client/src/constants/routes.ts:169`
+ defines `PUBLIC_PATHS: string[] = [ROUTES.LOGIN]` — so an unauthenticated hit on `/` 307s to
+ `/{locale}/login`. Note the **`startsWith` matching**: `'/'` can never be added to `PUBLIC_PATHS`
+ (every path starts with `/`, which would silently un-gate the whole app).
+3. **`/` belongs to the customer app.** The `(customer)` route group has no URL segment, so
+ `(customer)/page.tsx` *is* the root route. A second `page.tsx` cannot also resolve to `/` (Next
+ parallel-page collision), so the landing must be served by a **middleware rewrite**, not a sibling page.
+4. **One static `` for ~60 routes.** The only metadata export in the app is
+ `src/app/[locale]/layout.tsx:54-58` (`'Balinyaar | بالینیار'` + placeholder description). No
+ `generateMetadata`, no OpenGraph, anywhere ([audit/cross-cutting-ux.md](audit/cross-cutting-ux.md)).
+5. **`client/public/robots.txt` exists but is contradictory starter content** — two `User-agent: *`
+ blocks, `Disallow: /private/` (a path that does not exist in this app) followed by `Allow: /`. There
+ is no sitemap.
+
+**What already exists (do not rebuild):**
+
+- The **brand mark, theme pass, and de-startered public shell** from
+ [Phase 0](ui-phase-0-design-language.md) and [Phase 2](ui-phase-2-shells-and-navigation.md); the
+ primitives kit + per-route metadata groundwork from [Phase 1](ui-phase-1-primitives-and-states.md).
+- `CategoryTile` (`client/src/components/CategoryTile/CategoryTile.tsx`) — takes `label` + `iconKey`
+ props with a safe icon fallback (`KNOWN_CATEGORY_ICONS`: `elderly`, `post_surgery`, `infant`,
+ `chronic`, `companionship`); it works with **static i18n labels**, no API needed.
+- `EscrowNotice` (`client/src/components/EscrowNotice/EscrowNotice.tsx`) — the **product-mandated
+ verbatim escrow copy** («مبلغ بهصورت امانی نزد بالینیار میماند…»), and `TrustBadge`'s three
+ honest states.
+- The brand tagline is already a key: `common.brand_tagline` = «مراقبت مطمئن در خانه»
+ (`client/messages/fa.json:54`).
+- The `[locale]` root layout already renders honest `lang`/`dir` per locale and loads Mikhak fa-only
+ (`src/app/[locale]/layout.tsx`) — this phase builds **on top of** it, never above it.
+- The auth machinery: middleware gate, `RoleRouter`/`resolveRoleDestination`
+ (`client/src/services/auth/routing.ts` — a customer resolves to `ROUTES.HOME` = `/`), `RoleGuard` on
+ the four shells (refinement phase 2). None of it changes semantics in this phase.
+- If [Phase 3](ui-phase-3-auth-and-first-run.md) has already run: the terms/privacy pages and the
+ login `returnUrl` capture. If [Phase 4](ui-phase-4-customer-storefront.md) has run: the
+ verification-explainer content (whatever component name its report gives it). Reuse both; see §3.2.
+
+## 2. Required reading (do this first)
+
+- [audit/customer-storefront.md](audit/customer-storefront.md) — problem #1 (no public storefront) and
+ the "Public landing + guest browse" opportunity this phase implements; the **Keep** list you must not
+ regress (token discipline, four data states, trust honesty, money handling).
+- [audit/cross-cutting-ux.md](audit/cross-cutting-ux.md) — the metadata/404/route-chrome findings and
+ the "Public landing + public nurse profiles" opportunity; its **Keep** list (RTL habits, Mikhak
+ fa-only loading, locale/dir wiring reasoning).
+- Code: `client/middleware.ts` (the whole file — the i18n 307/308 early-return, the `PUBLIC_PATHS`
+ check, the locale header), `client/src/constants/routes.ts` (`ROUTES`, `PUBLIC_PATHS`),
+ `client/src/app/[locale]/layout.tsx` (root metadata + the "why no layout above [locale]" comment),
+ `client/src/app/[locale]/(public-routes)/layout.tsx` (client layout wrapping `PublicLayout` — RSC
+ children still render server-side), `(customer)/page.tsx` (the authenticated home that owns `/`),
+ `client/src/services/auth/routing.ts` + `client/src/components/auth/RoleRouter.tsx`,
+ `client/src/components/CategoryTile/CategoryTile.tsx`, `client/src/components/EscrowNotice/EscrowNotice.tsx`.
+- [../../../.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md) —
+ the design contract (invoke the skill; §7 non-negotiables all apply here).
+- Product: [../../../product/overview/platform-summary.md](../../../product/overview/platform-summary.md)
+ (the four ground truths — everything the landing claims must trace to them) and
+ [../../../product/business/index.md](../../../product/business/index.md) (verification + escrow rules,
+ so marketing copy is honest). [../../../product/notes/open-questions.md](../../../product/notes/open-questions.md)
+ is where §3.1's decision gets recorded.
+- The REQ tracker: [../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
+ — REQ-001…038 existed before this chain; earlier UI phases may have appended more. **Check the file
+ for the next free number before filing.**
+
+## 3. Scope — build this
+
+### 3.1 The product decision — do this FIRST, and write it down
+
+Frame guest-browse depth as three tiers and get an explicit decision before building:
+
+- **(a) Landing only** — a marketing page at `/`; zero data, zero new endpoints.
+- **(b) Landing + static public pages** — (a) plus category/how-it-works content pages; still zero
+ data (the category grid is static i18n content, not the catalog API).
+- **(c) Guest search + public nurse profiles** — read-only anonymous variants of the phase-4 search
+ results and nurse-profile screens. **Requires public read endpoints that do not exist** (all search
+ and profile reads sit behind the auth middleware and cookie-bearing `clientFetch`), so tier (c) is
+ REQ-gated backend work plus a privacy review of which nurse fields may be exposed logged-out.
+
+**Recommendation this phase encodes: ship (a)+(b) now; frame (c) as a REQ-gated follow-up** (file the
+REQs as proposals, §3.4 — do not build guest search against endpoints that don't exist). Record the
+decision (chosen tier, rationale, what tier (c) would need) in this phase's report **and** append it to
+[../../../product/notes/open-questions.md](../../../product/notes/open-questions.md) (edit the `.md`;
+the `.html` view is generated — `cd product && node build-docs.mjs`).
+
+### 3.2 Public landing at `/` for unauthenticated visitors
+
+**Routing mechanics (get this exactly right):**
+
+- Create the landing as an **RSC** at `client/src/app/[locale]/(public-routes)/welcome/page.tsx`
+ (the client `(public-routes)/layout.tsx` is fine — RSC children of a client layout still render on
+ the server).
+- In `client/middleware.ts`, after the i18n 307/308 early-return: if the request is **unauthenticated
+ and `pathWithoutLocale === '/'`**, `NextResponse.rewrite()` to `/{locale}/welcome` — the visitor sees
+ the landing **at the URL `/`** (good for SEO canonical). Do **not** redirect.
+- Add `/welcome` (a named constant in `ROUTES`) to `PUBLIC_PATHS`. **Never add `'/'`** — the
+ `startsWith` match would make every route public (§1.2).
+- An **authenticated** user hitting `/welcome` directly → redirect to `/` (a signed-in customer lands
+ on their home, exactly as today). Authenticated `/` is untouched — `(customer)/page.tsx` keeps
+ serving it, `RoleGuard`/`RoleRouter` behavior unchanged.
+- Preserve the middleware's existing behavior for every other path: the locale-header injection, the
+ next-intl `Link: alternate` hreflang headers it copies through, and the redirect-to-login for private
+ paths (including the `returnUrl` capture if Phase 3 has added it — test that flow after your change).
+
+**Landing sections (mobile-first, in order):**
+
+1. **Hero** — brand mark (phase 0), the existing tagline `common.brand_tagline`
+ («مراقبت مطمئن در خانه»), one supporting sentence, and a primary CTA to `/login` (label e.g.
+ «شروع کنید»). No carousel, no stock photography of fake nurses.
+2. **Category grid** — reuse `CategoryTile` with **static i18n labels** keyed to the five
+ `KNOWN_CATEGORY_ICONS` keys (elderly / post_surgery / infant / chronic / companionship). Each tile
+ links to `/login` (tier a+b: intent capture, not guest search). This section needs a small client
+ wrapper only if tiles navigate via router — prefer plain `AppLink`-wrapped tiles to keep the page RSC.
+3. **How it works — 3 steps:** «جستجوی پرستار تاییدشده» → «پرداخت امن امانی» → «مراقبت با خیال راحت».
+ Step 2's supporting text reuses the `EscrowNotice` verbatim copy (same i18n key or the component
+ itself) — never a paraphrase.
+4. **Verification/trust explainer** — the "what we verify" story (identity, nursing license, INO
+ membership). If Phase 4 has shipped its verification-explainer content, reuse those keys/components;
+ if this phase runs before Phase 4, write the section from
+ [../../../product/business/index.md](../../../product/business/index.md)'s pipeline and note in the
+ report that Phase 4 should fold its explainer into the same keys.
+5. **Nurse recruitment** — «پرستار هستید؟ به بالینیار بپیوندید» with a secondary CTA to `/login` with
+ the nurse intent the login screen already supports (the A1/B1 role switch from f1/phase 3).
+6. **Footer** — links to the terms/privacy pages if Phase 3 has shipped them (omit gracefully and note
+ it in the report if not), a contact affordance, and the locale switcher from the phase-2 public shell.
+
+### 3.3 SEO & metadata for the public surface
+
+- `generateMetadata` on the landing (and login + any phase-3 public pages if present): **localized**
+ title/description via `getTranslations`, a title template `%s | بالینیار`, and
+ `alternates.canonical` pointing at `/{locale}` for the landing (the rewrite means `/welcome` and `/`
+ serve the same content — canonicalize on `/`).
+- **OpenGraph:** `og:title`/`og:description` per locale + one static OG image (1200×630) at
+ `client/public/og/balinyaar-og.png`, built from the phase-0 brand mark on the brand teal/cream. Set
+ `metadataBase` from an env constant (e.g. `NEXT_PUBLIC_SITE_URL`) — never a hard-coded origin.
+- **Replace** the contradictory `client/public/robots.txt` with an app-router `src/app/robots.ts`
+ (delete the static file), allowing the public routes and disallowing the private roots
+ (`/*/nurse`, `/*/admin`, `/*/partner`, `/*/bookings`, …); add `src/app/sitemap.ts` listing **public
+ routes only** (`/fa`, `/en`, login, terms/privacy when they exist) with locale alternates.
+- `lang`/`dir` per locale is already correct in `src/app/[locale]/layout.tsx` — **do not regress it,
+ and NEVER add a layout (or robots/sitemap-driven layout tricks) above `[locale]`** (golden rule #1;
+ `robots.ts`/`sitemap.ts` are metadata routes, not layouts — they are safe at `src/app/`).
+
+### 3.4 Tier (c): guest search + public nurse profiles (DEFERRED unless explicitly approved in §3.1)
+
+If — and only if — the §3.1 decision approves tier (c): build guest search results and a public nurse
+profile as **read-only variants of the phase-4 screens** (no booking CTA past login, no address/PII),
+behind new public endpoints. Either way, **file the REQs now as proposals** in
+[for-backend.md](../../shared-working-context/frontend/requests/for-backend.md) (next free numbers —
+check the tracker, ≥ REQ-039):
+
+- **Public search read** — anonymous, rate-limited variant of the nurse search (verified-only invariant
+ preserved; no customer-context fields).
+- **Public nurse profile read** — privacy-reviewed field set (display name, photo, verified badge
+ state, rating aggregate, service/price rows; **never** phone, exact areas, or document data).
+
+Everything else about tier (c) — routes, guest-to-login handoff at the «درخواست رزرو» tap — is
+**(DEFERRED → a follow-up phase once the REQs are delivered)**.
+
+### 3.5 Performance sanity
+
+The landing is the one page where first paint is the product. **RSC-first, zero client data fetching
+above the fold** (the whole page needs no API at tier a+b), no new client-side libraries, images through
+`AppImage`/`next/image` with explicit dimensions, and the per-locale font strategy untouched (Mikhak is
+already fa-only, `preload: false` — do not "optimize" it into loading for `/en`). Any interactive island
+(e.g. a locale switcher) stays a leaf client component.
+
+## 4. Mocks & seams in this phase
+
+**None.** Tiers (a)+(b) are static content — no service calls, no new `services/{domain}` seams, no mock
+flags. The REQ posture: backend gaps become REQ entries appended to
+[../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
+(REQ-001…038 pre-date this chain; check the tracker for the next free number). This phase files the two
+tier-(c) proposals in §3.4 and builds nothing against them.
+
+## 5. Critical rules you must not get wrong
+
+- **This phase is optional and must stay reversible.** If it is skipped or aborted mid-way, the app must
+ behave exactly as today (guests → `/login`). Keep the middleware change small and additive.
+- **Auth boundaries untouched.** Private routes stay private; `RoleGuard`/`RoleRouter`/
+ `resolveRoleDestination` semantics unchanged; the middleware remains the auth gate. The only new
+ public surface is what §3.2/§3.3 name.
+- **The `PUBLIC_PATHS` `startsWith` trap:** never add `'/'` — root goes public via the exact-match
+ rewrite branch, not the list (`client/middleware.ts:23`, `routes.ts:169`).
+- **Do not break the login flow:** the phase-3 `returnUrl` capture (if shipped) and next-intl locale
+ detection/normalization (the 307/308 early-return and the copied `Link` hreflang headers) must survive
+ the middleware edit.
+- **No marketing claims the product can't honor.** Escrow copy is the `EscrowNotice` verbatim string;
+ "verified" copy describes the real pipeline; **no invented numbers** («۵۰۰۰ پرستار», fake ratings,
+ fake testimonials). Trust-first means the landing is honest-first.
+- **Copy discipline:** every string in **both** `messages/en.json` and `messages/fa.json`; new strings
+ use the ZWNJ brand spelling «بالینیار» (phase 12 canonicalizes the legacy spaced form — don't add
+ more of it); formal شما register.
+- **Design contract:** tokens not hexes (`--bal-*` / palette keys), terracotta stays the single sparing
+ accent, RTL logical props only, dark mode verified, MUI v9 API only, icons via the `AppIcon` registry,
+ `App*` wrappers before raw MUI. Any new **shared** component gets a co-located `*.test.tsx`.
+- **Never add a layout above `[locale]`** — `lang`/`dir` would freeze on the default locale (the root
+ layout's comment explains why). `robots.ts`/`sitemap.ts` at `src/app/` are fine; a layout is not.
+- **Fetch/cookies rules untouched** — no raw `fetch()`, no cookie reads outside `@/lib/cookies/*`
+ (at tier a+b you should need neither).
+
+## 6. Definition of Done
+
+On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):
+
+- [ ] The §3.1 decision is written down (report + `product/notes/open-questions.md`) **before** the
+ landing was built, and the built scope matches it.
+- [ ] `npm run check` green; `npm run test:ci` green for any touched/added shared components;
+ `en.json`/`fa.json` in sync.
+- [ ] An **unauthenticated** visit to `/` renders the landing at the URL `/` (rewrite, not redirect),
+ in both locales; an **authenticated** customer at `/` still gets the customer home; `/welcome`
+ while authenticated redirects to `/`.
+- [ ] Every other unauthenticated private path still redirects to `/login` (spot-check `/bookings`,
+ `/nurse`), and login → role routing works exactly as before (including `returnUrl` if phase 3 ran).
+- [ ] Visual verification on the four axes — `/fa` + `/en` × light + dark — and on mobile + desktop
+ widths for the landing (it is the page strangers judge the product by).
+- [ ] View-source of the landing shows the localized ``/description, OG tags with an absolute
+ image URL, and correct `lang`/`dir`; `/robots.txt` and `/sitemap.xml` serve the §3.3 content
+ (starter `robots.txt` deleted).
+- [ ] The landing performs no client-side data fetching (Network tab: no `/api/v1/*` calls while
+ logged out) and no marketing string contradicts `EscrowNotice`/`TrustBadge` semantics.
+- [ ] Tier-(c) REQs filed as proposals in the tracker with the next free numbers.
+
+## 7. How to test (what a human can verify after this phase)
+
+1. In a private/incognito window, open `http://localhost:3000` → normalized to `/fa`, the **landing**
+ renders (URL stays `/fa`, no `/login` redirect, no `/welcome` in the address bar): hero + tagline,
+ 5 category tiles, 3-step how-it-works with the escrow sentence, trust explainer, nurse CTA, footer.
+2. Switch to `/en` → LTR landing, English copy, system font (Network tab: no Mikhak woff2). Toggle dark
+ mode → all sections stay token-correct.
+3. Still logged out, visit `/fa/bookings` → redirected to `/fa/login` exactly as before this phase.
+4. Tap the hero CTA → `/login`; complete the seeded customer OTP login → land on `/` and see the
+ **customer home** (not the landing). Manually revisit `/fa/welcome` while logged in → redirected to `/fa`.
+5. Tap «پرستار هستید؟…» while logged out → login screen in its nurse-intent mode (B1 switch preselected).
+6. View source on the logged-out landing: localized `` (`… | بالینیار` on `/fa`), meta
+ description, `og:image` absolute URL, `lang="fa" dir="rtl"` (and `lang="en" dir="ltr"` on `/en`).
+7. Open `http://localhost:3000/robots.txt` → the new rules (private roots disallowed) and the sitemap
+ reference; `http://localhost:3000/sitemap.xml` → public routes only, both locales.
+8. Confirm in the report/tracker: the §3.1 decision recorded, the two tier-(c) REQs filed, and (if
+ phase 3 hadn't run) the noted footer-legal-links gap.
+
+## 8. Hand off & document (close the phase)
+
+- Update `client/CLAUDE.md` **Project Structure**: the new `(public-routes)/welcome` route (and the
+ middleware's rewrite behavior in the routing/middleware description), plus `robots.ts`/`sitemap.ts`.
+- Append the §3.1 decision to [../../../product/notes/open-questions.md](../../../product/notes/open-questions.md)
+ and regenerate the docs HTML (`cd product && node build-docs.mjs`).
+- Write the frontend report at `dev/shared-working-context/reports/ui-phase-13-report.md`: the tier
+ decision + rationale, the middleware change (with the `PUBLIC_PATHS` trap called out for future
+ agents), sections shipped, REQ numbers filed, and any graceful omissions (footer legal links,
+ verification-explainer reuse status).
+- List the REQs filed (tier-(c) public search read + public nurse profile read) with their final numbers.
+- Save a memory note per operating-rules §8: the public front door exists, `/` now forks by auth via a
+ middleware **rewrite** (never `'/'` in `PUBLIC_PATHS`), tier (c) is REQ-gated and unbuilt.
diff --git a/dev/post-phase/ui/ui-phase-2-shells-and-navigation.md b/dev/post-phase/ui/ui-phase-2-shells-and-navigation.md
new file mode 100644
index 0000000..21d48bb
--- /dev/null
+++ b/dev/post-phase/ui/ui-phase-2-shells-and-navigation.md
@@ -0,0 +1,296 @@
+# UI Phase 2 — Shells & navigation
+
+> **Mission:** replace the starter chrome with per-actor shells — consumer-app chrome for customers, a
+> workspace for nurses, a dense console for admin/partner — and fix the navigation-correctness defects the
+> audit verified: the sidebar active state that never fires, the middleware redirect hop on every sidebar
+> click, the fragile RTL anchoring, and the desktop SSR mobile-first flash. Phases 0–1 gave the app a design
+> language and primitives; this phase is where every actor finally gets chrome that *looks like Balinyaar and
+> navigates correctly*. **This phase owns `client/src/layout/`.**
+>
+> **Track:** frontend · **Depends on:** [Phase 0](ui-phase-0-design-language.md),
+> [Phase 1](ui-phase-1-primitives-and-states.md) · **Unlocks:** every actor gets branded, correct chrome —
+> the area redesigns (phases 3–11) compose inside these shells
+> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md) and invoke the frontend-designer skill — both are mandatory.**
+
+## 1. Context — where this sits
+
+The feature layer is disciplined, but the chrome around it is the untouched react-starter-kit. Diagnosed
+current state (all verified in code):
+
+1. **The sidebar active highlight never fires.** `SideBarNavItem.tsx:28` compares
+ `pathname.startsWith(path)` where `pathname` (from `next/navigation`) is locale-prefixed
+ (`/fa/nurse/requests`) and `path` is unprefixed (`/nurse/requests`) — `defineRouting` uses
+ `localePrefix: 'always'`, so the comparison always fails. Nurse/admin/partner users get zero "where am I"
+ signal. `AppLink`'s `activeClassName` (`AppLinkNextNavigation.tsx:95`, `pathname == currentPath`) has the
+ same bug.
+2. **Three locale strategies coexist in the chrome.** Sidebar links push unprefixed hrefs through raw
+ `next/link` (`SideBarNavItem.tsx:34` via `AppLink`) — a middleware redirect hop on every click and a
+ locale-flip risk for `/en` users; `BottomBar.tsx:14` and `NotificationBell` manually prefix with
+ `/${locale}`; `CustomerLayout.tsx:52` does the same inline. There is no `createNavigation` wrapper —
+ `src/i18n/` holds only `routing.ts` + `request.ts`.
+3. **The chrome is starter junk on a trust-first product.** `PublicLayout.tsx:9` titles the login screen
+ `'Unauthorized - Balinyaar'` in English; `SideBar.tsx:56` renders ` ` with no user —
+ an eternal English "Current User" / "Loading..." (`UserInfo.tsx:34-36`, prop typed `user?: any`);
+ `TopBarAndSideBarLayout.tsx:71` tooltips `'Open Sidebar'`; `SideBar.tsx:76` says `'Logout Current User'`.
+4. **Structural defects:** `SERVER_SIDE_MOBILE_FIRST = true` (`hooks/layout.ts:8`) makes every desktop SSR
+ paint the mobile shell, then content jumps 240px after hydration; content offset uses physical
+ `paddingLeft/Right` keyed to anchor strings (`TopBarAndSideBarLayout.tsx:53-60`) — RTL correctness by
+ double-flip coincidence; the drawer-close handler sits on the whole content `Stack` (`SideBar.tsx:52`) so
+ any tap inside closes it; `BottomBar` has no safe-area padding and renders unconditionally on desktop
+ (`CustomerLayout.tsx:81`); the customer shell has **no sign-out, no back affordance, no contextual
+ title**; no shell has a locale switcher or an actor switcher for dual-role sessions.
+
+**What already exists (do not rebuild):**
+
+- **[Phase 0](ui-phase-0-design-language.md):** the brand mark (registered as `logo`), the
+ `theme.components` pass (AppBar/Drawer/ListItemButton/BottomNavigation already restyled), the single icon
+ family incl. a direction-aware back chevron, elevation/motion/focus tokens.
+- **[Phase 1](ui-phase-1-primitives-and-states.md):** PageHeader, state views, route-level
+ `loading.tsx`/`error.tsx`/`not-found.tsx`, per-route metadata, formatting utils.
+- **RoleGuard + role hydration** (refinement phase 2): `RoleGuard` wraps all four shells with
+ resolved-vs-pending `/me` hydration, error recovery, and mismatch redirects. Chrome only here — never touch it.
+- **The per-actor shell split:** `CustomerLayout` / `NurseLayout` / `AdminLayout` / `PartnerLayout` mapped
+ 1:1 to route groups. Restyle and restructure the chrome *inside* this architecture; keep the split.
+- **`BottomBar`'s longest-prefix active matching** (`BottomBar.tsx:31-41`) — the one nav component that
+ matches correctly today. Keep its semantics; generalize them.
+- **Performance-conscious composition:** `DarkModeToggleButton`/`DarkModeFormSwitch` are the only
+ `useColorScheme` subscribers; `NotificationBell` isolates the polling unread count. Shells never re-render
+ on theme flips or bell updates — preserve this.
+- **`AdminLayout`'s capability-gated nav** via `useAdminCapabilities()` (`AdminLayout.tsx:21-37`).
+
+## 2. Required reading (do this first)
+
+- [audit/shell-and-navigation.md](audit/shell-and-navigation.md) — the full 20-problem inventory with
+ file/line evidence and the keep-list. This is your problem spec.
+- [audit/cross-cutting-ux.md](audit/cross-cutting-ux.md) — the chrome-adjacent items (English chrome
+ strings, `UserInfo`, `hooks/layout.ts`, metadata) and its keep-list.
+- The `frontend-designer` skill (`.claude/skills/frontend-designer/SKILL.md`) — the design contract; phase 0
+ will have updated it with the new tokens/icons.
+- Code, in this order: `client/src/layout/` (all files — you own this folder), `client/src/i18n/routing.ts`,
+ `client/src/components/common/AppLink/`, `client/src/components/UserInfo/UserInfo.tsx` (you will delete
+ it), `client/src/components/auth/RoleGuard.tsx` (do not touch — know why), `client/src/hooks/layout.ts`,
+ `client/src/hooks/auth.ts` (`useActorRole`, `useAdminCapabilities`), `client/src/context/auth/types.ts`
+ (`SessionUser.roles` — feeds the actor switcher), `client/src/constants/routes.ts`,
+ `client/src/components/TrustBadge/`, `client/src/services/profiles/` + `client/src/services/auth/hooks/useMe.ts`
+ (feed the identity card), `client/src/components/notifications/NotificationBell.tsx`.
+- `client/CLAUDE.md` — "Golden rules", "Direction (RTL/LTR)", the theme system, and the Project Structure
+ layout section you must update at close.
+- next-intl v4 docs for `createNavigation` (routing-aware `Link`/`usePathname`/`useRouter`/`redirect`).
+
+## 3. Scope — build this
+
+### 3.1 Locale-aware navigation — one wrapper, two bugs fixed
+
+Create **`src/i18n/navigation.ts`**: `createNavigation(routing)` from `next-intl/navigation`, exporting
+`Link`, `usePathname`, `useRouter`, `redirect`, `getPathname`. Then route **all chrome navigation** through it:
+
+- `SideBarNavItem` / `SideBarNavList`: link via the new `Link`; compute active state against the new
+ `usePathname()` (which strips the locale prefix, so unprefixed `ROUTES.*` compare directly). This makes the
+ active highlight fire for the first time **and** removes the middleware redirect hop + locale-flip risk in
+ one move.
+- Active matching must be **longest-prefix winner-takes-all** (the `BottomBar` semantics): extract a small
+ shared helper (e.g. `src/layout/matchActivePath.ts`, unit-tested) used by both the sidebar and the bottom
+ bars — plain `startsWith` would keep `/nurse` (dashboard) lit on every nurse route.
+- `BottomBar`: drop the manual `withLocale` prefixing in favor of the wrapper's router; keep its matching via
+ the shared helper.
+- `AppLink`'s `activeClassName` comparison, `NotificationBell`'s `router.push`, and `CustomerLayout`'s inline
+ `` `/${locale}${…}` `` all migrate to the wrapper. After this, `grep -r '/${locale}' src/` inside chrome
+ code should return nothing.
+
+### 3.2 Customer shell — contextual header + deliberate desktop
+
+Rework `CustomerLayout`:
+
+- **Contextual header.** On the 5 root tabs (`/`, `/bookings`, `/patients`, `/wallet`, `/profile` — the
+ `(customer)` group has no URL segment): the phase-0 **brand lockup** (mark + wordmark). On pushed routes
+ (nurse profile, booking detail, checkout, ticket thread…): **page title + back chevron** (the phase-0
+ auto-mirrored icon) that calls `router.back()`. Drive it with a route→title map in `src/layout/`
+ (longest-prefix over `ROUTES.*`, titles from the existing `nav`/`shell` namespaces) plus a lightweight
+ per-page override slot (React context) for dynamic titles — the area phases (4–6, 9) will feed nurse/booking
+ names into it later; ship static titles now. **Kill the static «اپلیکیشن خانواده» label**
+ (`tShell('customer_app')` today).
+- Keep the support entry, `NotificationBell`, and dark toggle in the header (badge/popover upgrades belong to
+ phase 10 — leave slots, don't build them).
+- **BottomBar:** add `paddingBottom: 'env(safe-area-inset-bottom)'` on the Paper (the home-indicator overlap
+ is on the primary mobile nav); refine the active state on top of the phase-0 BottomNavigation override
+ (selected color + label weight — tokens, not hexes).
+- **Desktop treatment — decide and implement deliberately.** Recommended: a constrained app frame — the
+ content column keeps `CONTENT_MAX_WIDTH`, gains side gutters on a `background.default` canvas — and above
+ the `md` breakpoint **hide the mobile tab bar** in favor of a top-nav variant (the same 5 items as inline
+ header tabs). Implement with CSS breakpoints (`sx` `display` keys), not `useIsMobile` branching (see 3.6).
+
+### 3.3 Nurse shell — a workspace, not a starter drawer
+
+Rework `NurseLayout` (still on the shared engine, which you are also refitting in 3.6):
+
+- **Grouped sidebar** with subheaders + dividers, replacing the flat 10-item array (`NurseLayout.tsx:19-33`):
+ **امروز** (dashboard, requests, visits) · **حرفهٔ من** (services, coverage, verification) · **مالی**
+ (earnings, bank) · **پشتیبانی** (support). Extend the nav-item model with a group key; the engine renders
+ `ListSubheader`-style sections. Group labels are i18n keys in both catalogs.
+- **A real identity card:** new typed **`ProfileSummary`** shared component
+ (`src/components/ProfileSummary/`, co-located test): avatar, display name, masked phone in Persian digits,
+ role label, and `TrustBadge` when the actor is a nurse. Feed it from the `/me` session (`useMe` — phone,
+ roles) plus the profiles domain for name/avatar where hydrated; render graceful skeleton/fallback states —
+ never English literals. **DELETE `src/components/UserInfo/`** (the starter `user?: any` card) and every
+ import/test of it.
+- **Mobile: a 5-tab nurse bottom nav** so field nurses stop digging through a drawer: امروز (dashboard) ·
+ درخواستها · ویزیتها · درآمد · بیشتر — «بیشتر» opens the drawer with the remaining items (profile,
+ services, coverage, bank, verification, support, sign-out). Reuse `BottomBar`.
+- **TopBar shows the current page title** via the same route→title engine as 3.2 (kill the static «نمای
+ پرستار»).
+
+### 3.4 Admin + partner shells — a dense console
+
+- **Slim top bar:** current page title (route→title map) as a start-anchored breadcrumb-style label — not the
+ centered static console name — plus a **bell** (widen `NotificationBell`'s `role` union to include
+ `'admin'`; the `/admin/notifications` center already exists — note this minimal foundation extension in
+ your report) and an **identity chip** (`ProfileSummary` compact variant or a chip: name/phone + the
+ fine-grained role label off `roleCodes` — a `finance` admin should *see* they're finance).
+- **Sectioned sidebar** with the now-working active state: **اعتماد** (verification, reviews) · **مالی**
+ (payouts; refunds are worked *via tickets* — do not invent a refunds nav item) · **پشتیبانی** (tickets,
+ alerts) · **سیستم** (config, holidays, audit, roles, partners, users). Add the missing `/admin/users` entry
+ (`ROUTES.ADMIN_USERS` exists; gate it like roles on `caps.canManageRoles` — display convenience, server is
+ the authority). The notifications sidebar item is replaced by the header bell. **Keep every
+ `useAdminCapabilities` gate exactly as is** — grouping must not change what a role sees.
+- **Partner shell:** same engine + its own 4-item nav; identity area shows the **center name** from
+ `useMyPartnerCenter` (fallback skeleton while resolving — the page-level access-denied handling stays where
+ it is).
+
+### 3.5 Public shell — strip it to a brand frame
+
+`PublicLayout` today wraps login in starter dashboard chrome: the hard-coded English `'Unauthorized -
+Balinyaar'` title (`PublicLayout.tsx:9`), a pencil button opening a drawer containing only a dark-mode
+switch, and an **empty** `BottomBar` strip on mobile (`PublicLayout.tsx:34`, `BOTTOM_BAR_ITEMS = []`).
+Replace it with a minimal centered brand shell: the phase-0 logo, a locale switcher, and the dark toggle — no
+sidebar, no bottom bar, no `TopBarAndSideBarLayout`. Delete the dead `BOTTOM_BAR_DESKTOP_VISIBLE` flag from
+`layout/config.ts` with it. (The login screen itself — hero, trust presence — is **DEFERRED → phase 3**.)
+
+### 3.6 Cross-cutting engine fixes (`TopBarAndSideBarLayout` + `SideBar` + `TopBar`)
+
+- **Drawer close scoping:** move the close handler off the content `Stack` (`SideBar.tsx:52`) onto the nav
+ links themselves — toggling dark mode or mis-tapping a divider must not close the drawer.
+- **Kill the desktop SSR flash:** stop deriving shell *structure* from `useIsMobile`
+ (`SERVER_SIDE_MOBILE_FIRST` renders the mobile shell, then jumps 240px). Render responsively with CSS:
+ breakpoint-keyed `sx` values (paddings, drawer variant/visibility via `display`) so the desktop first paint
+ already includes the persistent sidebar. `hooks/layout.ts` stays for non-structural consumers; the shells
+ stop depending on it for layout.
+- **Logical properties:** replace the physical `paddingLeft/Right` + `anchor?.includes('left')` logic
+ (`TopBarAndSideBarLayout.tsx:53-60, 89-91`) with `paddingInlineStart` / start-anchored drawer semantics —
+ RTL correctness by construction, not by the stylis double-flip coincidence.
+- **Chrome strings → both catalogs:** `'Open Sidebar'` (`TopBarAndSideBarLayout.tsx:71`), `'Logout Current
+ User'` (`SideBar.tsx:76`); the `UserInfo` literals die with the component. Zero English chrome remains on `/fa`.
+- **TopBar:** fix the `whiteSpace: 'nowrap'` overflow risk (ellipsis + `minWidth: 0`); delete the starter
+ comment residue (`TopBar.tsx:20`); title alignment becomes start-anchored for the console shells per 3.4.
+- **`layout/config.ts`:** delete the commented-out anchor alternates (lines 8-9) and dead flags; keep the
+ constants authoritative — update them, never bypass them.
+
+### 3.7 Session affordances — sign-out, actor switch, locale switch
+
+- **Sign-out reachable in EVERY shell.** The customer shell currently has **none** — add a sign-out row to
+ the profile-tab hub (`/profile`) now (the full hub redesign is **DEFERRED → phase 9**; one labeled row, not
+ a redesign). Sidebar shells keep sign-out in the drawer footer — now labeled and translated.
+- **Actor switcher for dual-role sessions:** `SessionUser.roles` already lives in AuthContext. When a session
+ holds both `customer` and `nurse`, show «نمای پرستار ⇄ اپلیکیشن خانواده» in the nurse sidebar and on the
+ customer profile hub. Navigation only — `RoleGuard` and `resolveRoleDestination` stay the "which app"
+ authority.
+- **Locale switcher (fa/en) in all shells** (sidebar footer / customer profile hub / public shell): switch
+ locale **preserving the current path** via the 3.1 wrapper (`router.replace(pathname, { locale })`).
+- **Keep `DarkModeToggleButton`/`DarkModeFormSwitch` as the only `useColorScheme` subscribers** — the
+ switchers must not add scheme subscriptions to the shells.
+
+## 4. Mocks & seams in this phase
+
+**None introduced.** This phase is chrome over data that already flows (`useMe`, profiles,
+`useMyPartnerCenter`, the notifications unread count) behind the existing `services/{domain}` seams — UI
+stays mock-tolerant regardless of each domain's mock flag. **REQ posture:** if a backend gap surfaces (the
+likely one: `/me` lacking a display name for `ProfileSummary`, forcing a second profile fetch per shell),
+append a REQ to
+[../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
+— REQ-001…038 are taken; number from **REQ-039**. Never edit `server/`.
+
+## 5. Critical rules you must not get wrong
+
+1. **RoleGuard and role/redirect logic untouched.** Refinement phase 2 built resolved-vs-pending hydration;
+ this phase is chrome, not security or routing policy. The actor switcher navigates; it never re-derives roles.
+2. **`useAdminCapabilities` gating stays exactly as is** — sectioning the admin nav must not add, remove, or
+ loosen a single capability gate.
+3. **Do not regress the keep-lists** ([audit](audit/shell-and-navigation.md)): the root `[locale]` layout
+ (lang/dir, conditional Mikhak, cookie-seeded scheme, RTL Emotion cache) is untouchable; the per-actor
+ shell split stays; the customer 5-tab IA (Home/Bookings/Patients/Wallet/Profile) stays; longest-prefix
+ active matching stays; `DarkModeButton` remains the sole `useColorScheme` subscriber; `NotificationBell`
+ keeps isolating the poll; `CONTENT_MAX_WIDTH` reading column stays for text-heavy views;
+ `ErrorBoundary` keeps wrapping every shell's main content.
+4. **Design contract non-negotiables:** every new string in **both** `en.json`/`fa.json` (fa is the product's
+ voice — write it first); tokens/palette keys, never hexes; logical/RTL-safe props only (this phase exists
+ partly to *remove* physical ones — do not add new ones); verify dark mode on every surface you touch;
+ MUI v9 API only (no `useFlexGap`/`flexWrap` as Stack props); new shared components (`ProfileSummary`, the
+ active-path helper, any header context) get co-located tests.
+5. **Fetch/cookies/provider rules untouched:** no raw `fetch`, no `document.cookie`, no layout above
+ `[locale]`, no `createTheme()` in components. `ProfileSummary` consumes existing hooks — it does not add
+ API calls of its own design.
+6. **Deleting `UserInfo` is a removal, not a rename** — check `src/**/*.test.{ts,tsx}` and the
+ `@/components` barrel for imports, and update the frontend-designer skill's component table (it lists
+ `UserInfo`) to point at `ProfileSummary`.
+
+## 6. Definition of Done
+
+On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):
+
+- [ ] `npm run check` green; `npm run test:ci` green (new tests for `ProfileSummary`, the active-path
+ helper, and any touched shared component); `en.json`/`fa.json` in sync.
+- [ ] `src/i18n/navigation.ts` exists and **all** chrome navigation flows through it — no raw `next/link`
+ and no manual `/${locale}` prefixing left in `src/layout/` or chrome components.
+- [ ] The sidebar highlights the active item on every nurse/admin/partner route (first time ever), and
+ clicking a sidebar link produces **one** navigation in the Network tab — no 307 middleware hop, no
+ locale flip on `/en`.
+- [ ] Customer shell: brand lockup on the 5 root tabs; title + mirrored back chevron on pushed routes;
+ safe-area padding on the bottom bar; on ≥`md` the mobile tab bar is hidden in favor of the desktop
+ treatment.
+- [ ] Nurse shell: grouped sidebar (امروز/حرفهٔ من/مالی/پشتیبانی), `ProfileSummary` identity card with
+ TrustBadge, 5-tab mobile bottom nav; `UserInfo` deleted repo-wide.
+- [ ] Admin/partner shells: sectioned capability-gated sidebar, page-title top bar, bell + identity chip
+ (admin), center-name identity (partner).
+- [ ] Public shell: no English title, no pencil, no empty drawer or bottom strip — logo + locale switcher +
+ dark toggle only.
+- [ ] Desktop first paint of a sidebar shell includes the persistent sidebar — no 240px post-hydration jump
+ (verify with a hard reload, network throttled).
+- [ ] Sign-out is reachable in all four shells; a dual-role session sees the actor switcher; every shell has
+ a locale switcher that preserves the current path.
+- [ ] Visual verification on the four axes — `/fa` + `/en` × light + dark — and mobile + desktop for every
+ shell (fa first).
+
+## 7. How to test (what a human can verify after this phase)
+
+1. Log in as the seeded nurse → `/nurse`. The sidebar shows four labeled groups and your name/phone/TrustBadge
+ — not "Current User". Click «ویزیتها»: the item highlights, the top bar reads the page title, and the
+ Network tab shows a single navigation (no 307).
+2. Resize to mobile (or open devtools device mode): the nurse shell shows a 5-tab bottom nav; tab «بیشتر»
+ opens the drawer; toggling dark mode inside the drawer does **not** close it; tapping a nav link does.
+3. As a customer on `/`: the header shows the brand lockup. Open a nurse profile from search → the header
+ flips to title + back chevron; the chevron returns to results. On `/en` the chevron mirrors correctly.
+4. On desktop ≥900px as a customer: no mobile tab bar pinned to the bottom; the desktop nav variant is
+ present; content sits in the framed column.
+5. Go to `/profile` as a customer: a sign-out row exists and works. With a dual customer+nurse session, the
+ actor switcher appears here and in the nurse sidebar, and lands on the other shell (RoleGuard permitting).
+6. Log in as the seeded admin: top bar shows the page title, the bell, and your role chip (e.g. «مالی» for a
+ finance admin); the sidebar is sectioned and still shows only capability-permitted consoles. As the
+ finance-only admin, confirm no new items appeared.
+7. Open `/login` logged-out on `/fa`: no English anywhere, no drawer, no bottom strip — brand mark, locale
+ switcher, dark toggle.
+8. Hard-reload `/nurse` on desktop: the sidebar is present at first paint; no sideways content jump.
+9. Switch locale from any shell's switcher on a deep route (e.g. `/fa/nurse/earnings`): you land on
+ `/en/nurse/earnings`, same page.
+
+## 8. Hand off & document (close the phase)
+
+- Update **`client/CLAUDE.md` → Project Structure**: the `layout/` section (new shell composition,
+ route→title map, removed starter engine parts), `i18n/navigation.ts`, `components/ProfileSummary/`, and
+ the `UserInfo` deletion. Update the frontend-designer skill's §4 component table (`UserInfo` →
+ `ProfileSummary`) and §5 layout description if shell variants changed.
+- Write the report at `dev/shared-working-context/reports/ui-phase-2-report.md`: what changed per shell, the
+ navigation-wrapper migration list, the desktop-treatment decision you made, any foundation files you
+ extended minimally (e.g. `NotificationBell` role union), and screenshots/notes from the four-axes check.
+- List any REQs filed (REQ-039+) with one-line rationales; "none" is an acceptable outcome.
+- Save a memory note per operating-rules §8: the shells are now the branded per-actor chrome, chrome
+ navigation is `createNavigation`-based (active state + no redirect hop), `UserInfo` is gone, and
+ phases 3–11 must route new chrome strings/titles through the route→title map rather than static labels.
diff --git a/dev/post-phase/ui/ui-phase-3-auth-and-first-run.md b/dev/post-phase/ui/ui-phase-3-auth-and-first-run.md
new file mode 100644
index 0000000..c375729
--- /dev/null
+++ b/dev/post-phase/ui/ui-phase-3-auth-and-first-run.md
@@ -0,0 +1,286 @@
+# UI Phase 3 — Auth & first-run
+
+> **Mission:** the login screen is the product's only front door — the middleware redirects every
+> unauthenticated hit to `/login` — and today it is a bare starter card fronted by a cartoon Twemoji
+> pencil, with no consent line, no trust content, and an OTP flow that never offers SMS autofill in an
+> OTP-first market. This phase rebuilds login → OTP → select-role → onboarding as a branded,
+> trust-forward, Persian-native entry: a login hero that states what Balinyaar verifies and escrows,
+> OTP ergonomics at Snapp/Digikala parity (WebOTP + `one-time-code`), terms/privacy consent, an
+> illustrated role fork, a chrome-free focused onboarding journey, and a `returnUrl` so deep links
+> survive login.
+>
+> **Track:** frontend · **Depends on:** [Phases 0–2](ui-phase-2-shells-and-navigation.md) ·
+> **Unlocks:** the first impression finally sells trust
+> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md)
+> and invoke the frontend-designer skill — both are mandatory.**
+
+## 1. Context — where this sits
+
+Balinyaar is a trust-first home-nursing marketplace; there is no public landing page (that is
+[Phase 13](ui-phase-13-public-front-door.md)), so **login IS the product's first impression** — and in a
+phone-OTP market, login is also signup. Mechanically the flow is strong (see the audit's keep-list); the
+problems are presentation, ergonomics, and a handful of genuine defects, all verified in code:
+
+1. **The login card carries zero trust evidence.** `client/src/components/auth/AuthCard.tsx:12-31` is an
+ outlined `Paper` with a `BrandMark` and nothing else — no illustration, no mention of nurse
+ verification or escrowed payment, on a product whose entire pitch is trust.
+2. **No OTP autofill.** A repo-wide grep for `autoComplete`, `one-time-code`, and `OTPCredential` over
+ `client/src` returns **zero hits** — `OtpInput.tsx:121-128` sets `inputMode`/`maxLength`/`aria-label`
+ but never `autoComplete`, and there is no WebOTP wiring. Every login feels worse than the apps users
+ compare against.
+3. **No consent, no terms, no privacy.** `PhoneStep.tsx:56-102` renders title/field/CTA/role-switch only;
+ `PUBLIC_PATHS = [ROUTES.LOGIN]` (`client/src/constants/routes.ts:169`) — no `/terms` or `/privacy`
+ route exists anywhere. Entering a phone number creates an account with no disclosed terms.
+4. **The 429 state is the least visible one.** `PhoneStep.tsx:76` renders `rate_limited` as grey
+ `helperText` while `error={invalid}` stays false — the one state where the user is blocked gets no
+ error styling.
+5. **Bidi + digit defects on the OTP screen.** `OtpStep.tsx:100` interpolates the masked phone
+ (`0912•••1234`) into an RTL sentence with no bidi isolation (the classic digits-around-neutrals
+ reversal); `formatMmSs` (`OtpStep.tsx:24-28`) renders Latin `01:23` in a Persian sentence; backspace on
+ an empty box (`OtpInput.tsx:89-93`) moves focus but never clears the previous digit.
+6. **Select-role is semantically confusing.** `SelectRole.tsx:21-24` gives the nurse option a **house**
+ icon (`{ role: 'nurse', icon: 'home' }`); selection feedback is border-color only
+ (`SelectRole.tsx:74-83`) — color is the sole signal.
+7. **Onboarding renders inside the full app shell.** `(customer)/onboarding/page.tsx` sits in the
+ `(customer)` route group, so the A3→A4 wizard shows the 5-tab BottomBar, support icon, and bell — a
+ user mid-setup can tab away. All four relation options share the same `'account'` icon
+ (`onboarding/page.tsx:31`), and there is no welcome moment.
+8. **Deep links die at login.** `middleware.ts:29` redirects to `/{locale}/login` discarding the attempted
+ path — an SMS booking link or shared nurse profile dumps the user on their role home after login.
+
+**What already exists (do not rebuild):**
+
+- **The real brand mark and de-startered theme** — [Phase 0](ui-phase-0-design-language.md) owns
+ `theme/`, the logotype replacing the Twemoji pencil, the icon registry, and `AppButton` (including
+ killing its `margin: 1` starter default). Consume the mark via `BrandMark`/`ICONS.logo`; do not design
+ a second one.
+- **Shared primitives** — [Phase 1](ui-phase-1-primitives-and-states.md) owns `formatNumber` (the
+ locale-digit helper), `StepperHeader`, and the state views. Consume them.
+- **The de-startered public shell** — [Phase 2](ui-phase-2-shells-and-navigation.md) owns `layout/`,
+ including retiring `PublicLayout`'s "Unauthorized - Balinyaar" chrome. This phase styles the **content**
+ of the auth routes, not the shell.
+- **The auth machinery** — `LoginFlow`/`PhoneStep`/`OtpStep`/`RoleRouter`/`AuthSplash`/`RoleGuard`/
+ `SelectRole`/`useCountdown`, `services/auth` (real backend, `USE_AUTH_MOCK = false`), the middleware
+ auth gate, and the resolved-vs-pending role hydration from refinement phase 2. This phase redesigns
+ surfaces and adds ergonomics; the session/cookie/refresh/hydration logic is untouched.
+
+## 2. Required reading (do this first)
+
+- [audit/auth-first-run.md](audit/auth-first-run.md) — the full evidence, opportunities, and the
+ **keep-list** this phase must not regress.
+- The auth surface: `client/src/components/auth/` (`LoginFlow.tsx`, `AuthCard.tsx`, `BrandMark.tsx`,
+ `PhoneStep.tsx`, `OtpStep.tsx`, `SelectRole.tsx`, `RoleRouter.tsx`, `constants.ts`),
+ `client/src/components/OtpInput/OtpInput.tsx`, `client/src/components/PhoneNumberField/`,
+ `client/src/services/auth/routing.ts` (`resolveRoleDestination` — pure, unit-tested).
+- First-run: `client/src/app/[locale]/(private-routes)/(customer)/onboarding/page.tsx`,
+ `client/src/components/RelationSelect/`, `client/src/components/StepperHeader/`, and the home redirect
+ gate at `(customer)/page.tsx:60-68`.
+- Routing: `client/middleware.ts`, `client/src/constants/routes.ts` (`ROUTES`, `PUBLIC_PATHS`).
+- The design contract: [.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md)
+ (invoke the skill) + `client/CLAUDE.md` Golden rules and the auth-cookies section (what you must not touch).
+- Product truth for the trust copy: [product/overview/platform-summary.md](../../../product/overview/platform-summary.md)
+ (the four ground truths), [product/business/02-nurse-verification.md](../../../product/business/02-nurse-verification.md)
+ and [product/business/08-payments-and-escrow.md](../../../product/business/08-payments-and-escrow.md) —
+ the trust bullets must state facts the platform actually implements, nothing aspirational.
+
+## 3. Scope — build this
+
+### 3.1 Login hero — a branded, trust-forward front door
+
+Redesign `LoginFlow`/`AuthCard` as a branded moment on the cream backdrop (`background.default` /
+`--bal-bg-default`): the Phase-0 logotype, the tagline «مراقبت مطمئن در خانه», and **2–3 trust bullets**
+beneath the card stating what the product actually does — «پرستاران تاییدشده» (license + identity
+verification), «پرداخت امن امانی» (escrow until confirmed check-out), «پشتیبانی» — each with a registry
+icon. Add a calm illustration treatment: **composed CSS/SVG shapes in brand tokens are fine; no stock
+photos, no raster illustrations.** Keep the card readable at 320px and let the hero breathe on desktop.
+Keep the single login stack parameterized by `intendedRole` (`LoginFlow.tsx:22-24`, seeded from
+`?role=nurse`) and the A1/B1 switch link — the nurse variant may re-tint copy, never fork the tree.
+Keep the `' '` helperText placeholder that prevents layout jump.
+
+### 3.2 OTP ergonomics — autofill, correction, and Persian digits
+
+- **`autoComplete="one-time-code"`** on the `OtpInput` digit inputs (`slotProps.htmlInput`), so
+ iOS/Android offer the SMS code as a keyboard suggestion.
+- **WebOTP:** feature-detect `'OTPCredential' in window`, call
+ `navigator.credentials.get({ otp: { transport: ['sms'] }, signal })` with an `AbortController`
+ (abort on unmount and on manual completion), distribute the received code through the existing
+ `onChange`/`onComplete` path so the existing auto-verify fires. Unsupported browsers silently fall back
+ to manual entry — no errors, no UI difference.
+- **File REQ-039** (see §4): WebOTP and native suggestion only work when the SMS **ends with the
+ origin-bound line `@ #`** — the SMS template is server/Kavenegar-side.
+- **Backspace-on-empty clears the previous box:** in `OtpInput.tsx:89-93`, when backspace is pressed on
+ an empty box, clear `chars[index-1]` *and* move focus — one keypress per digit to erase.
+- **Persian-digit countdown:** replace `formatMmSs`'s raw `String`/`padStart` (`OtpStep.tsx:24-28`) with
+ the Phase-1 `formatNumber` helper so `/fa` renders ۰۱:۲۳ and `/en` renders 01:23. Keep the clock an
+ LTR run inside the sentence.
+- **429 as a real error:** in `PhoneStep.tsx:76`, the rate-limited state must set `error` on the field
+ (e.g. `error={invalid || rateLimited}`) so the message renders in error styling, not grey helperText.
+- **Bidi-isolate the masked phone echo:** wrap `maskIranMobile(phone)` in `OtpStep.tsx:100` in a
+ ``/`dir="ltr"` inline element (via `t.rich` or splitting the sentence) so `0912•••1234` never
+ reorders inside the RTL sentence. Keep the masking itself.
+
+### 3.3 Consent + legal pages
+
+- Add the standard implicit-consent line under the login CTA in `PhoneStep`:
+ «با ورود، [شرایط استفاده] و [حریم خصوصی] را میپذیرید» — the bracketed terms are `AppLink`s.
+- Create `/terms` and `/privacy` under `(public-routes)` (`terms/page.tsx`, `privacy/page.tsx`):
+ readable, Typography-composed legal skeletons in **both locales**, clearly flagged in the phase report
+ and a code comment as **draft copy requiring human/legal review before launch**. Add
+ `ROUTES.TERMS`/`ROUTES.PRIVACY` and append both to `PUBLIC_PATHS` (`routes.ts:169`) so the middleware
+ lets them through logged-out.
+
+### 3.4 Select-role — an illustrated two-card fork
+
+Replace the generic icons (`SelectRole.tsx:21-24` — `account` for customer, a **house** for nurse) with
+an illustrated two-card fork: *family receiving care* vs *nurse professional*, using the same CSS/SVG
+illustration treatment as 3.1. Selected state = `--bal-primary-soft` fill **plus a check glyph** —
+color must not be the only signal. Add a reassurance line that the other role can be added later
+(dual-role sessions are already supported). **Keep the radio a11y semantics exactly**: `role="radio"`,
+`aria-checked`, `tabIndex`, Enter/Space handlers (`SelectRole.tsx:64-72`). Admin is never offered here.
+If a check/role icon is missing from the registry, extend `AppIcon/config.ts` minimally and note it in
+the report (Phase 0 owns the file; later phases extend, never fork).
+
+### 3.5 Onboarding as a focused journey
+
+- **Move the A3→A4 wizard out of the customer shell chrome.** Today `onboarding/page.tsx` renders inside
+ `CustomerLayout` (5-tab BottomBar + bell). Relocate it to a sibling route group, e.g.
+ `(private-routes)/(customer-focused)/onboarding/`, whose layout keeps **`RoleGuard(expected=customer)`**
+ (non-negotiable) but renders a chrome-free focused shell in the spirit of `AuthCard` — logo, progress,
+ content, nothing to tab away to. Route groups don't change the URL, so `/onboarding`, the
+ `ROUTES.ONBOARDING` constant, and the home redirect gate (`(customer)/page.tsx:63`) keep working
+ untouched.
+- **A one-screen welcome moment** before the relation step: «خوش آمدید — بگویید مراقبت برای چه کسی
+ است؟»-style framing with the brand mark, one CTA into the wizard.
+- **Distinct relation iconography:** the four options (`onboarding/page.tsx:31` — parent/spouse/child/
+ self, currently all `'account'`) each get a distinct registry icon so the first product interaction
+ isn't four identical cards.
+- **Progress via `StepperHeader`** (welcome doesn't count as a step; relation → patient does).
+- **Keep** the relation pre-shaping the patient form (relation field hidden on A4) and the settled-list
+ redirect gate exactly as they are.
+- (DEFERRED → [Phase 4](ui-phase-4-customer-storefront.md) + product decision) a «بعداً تکمیل میکنم»
+ skip path with a browse-capable home: it changes the zero-patient home gate, which is storefront
+ territory. The forced redirect stays as-is in this phase.
+- (DEFERRED → [Phase 12](ui-phase-12-copy-motion-and-polish.md)) OTP delivery fallback escalation
+ (voice-call OTP / support link after failed resend cycles) — needs a second delivery channel that
+ doesn't exist server-side.
+
+### 3.6 returnUrl — deep links survive login
+
+- `middleware.ts:29`: when redirecting an unauthenticated hit to login, append the attempted
+ locale-stripped path + query as `?next=` — e.g.
+ `/fa/bookings/42` → `/fa/login?next=%2Fbookings%2F42`.
+- Login honors it: `LoginFlow` reads `next` and passes it to `RoleRouter`; add a **pure, unit-tested**
+ helper beside `resolveRoleDestination` in `services/auth/routing.ts` (e.g.
+ `resolvePostLoginDestination(me, intendedRole, next)`) that returns `next` only when it is (a) a
+ same-origin **relative** path — starts with `/`, not `//`, no scheme — and (b) permitted by the
+ resolved roles (a customer's `next=/nurse/...` falls through). Anything else falls back to
+ `resolveRoleDestination` — which stays the single "which app" source of truth.
+- Touch `middleware.ts` **surgically**: the token check, locale detection, header propagation, and
+ next-intl handling are load-bearing; the auth gate semantics must not change.
+
+### 3.7 PhoneNumberField autofill
+
+Add `autoComplete="tel"` to the `PhoneNumberField` input so browsers/keyboards offer the user's own
+number. Keep the LTR forcing and digit normalization untouched.
+
+## 4. Mocks & seams in this phase
+
+**None introduced.** Auth is real (`USE_AUTH_MOCK = false` since refinement phase 4); every deliverable
+here is presentation, ergonomics, or client routing. Do not flip any mock flag.
+
+**REQ posture:** backend gaps become REQ entries appended to
+[../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
+— REQ-001…038 are taken; number onward. This phase files exactly one:
+
+- **REQ-039 — WebOTP-conformant OTP SMS template.** The OTP SMS must end with the origin-bound last line
+ `@ #` (the [WebOTP](https://web.dev/articles/web-otp) / origin-bound one-time-code
+ convention) so Chrome's WebOTP API and the iOS/Android keyboard suggestion can auto-read the code. This
+ is a server/SMS-template change (the Kavenegar adapter from refinement phase 8); zero API-shape impact.
+ The client work in 3.2 ships regardless and degrades to manual entry until this lands.
+
+## 5. Critical rules you must not get wrong
+
+- **Do not touch the auth/session machinery.** `services/auth` session logic, `persistAuthTokens`/
+ `clearAuthTokens`, the fetch-layer silent refresh, `useLogout`, cookie handling, and the
+ `RoleGuard`/`useRoleHydration` resolved-vs-pending behavior were hardened in refinement phases 2 and 5.
+ This phase restyles screens and adds a routing helper — nothing else.
+- **The no-wrong-shell-flash behavior stays.** `RoleRouter` + branded `AuthSplash` while `/me` is in
+ flight, `AuthAccountError` on failure, `RoleGuard` redirect-with-toast on mismatch. The relocated
+ onboarding layout must keep `RoleGuard(expected=customer)`.
+- **OTP mechanics that already work stay:** paste distribution across boxes, Persian/Arabic→ASCII digit
+ normalization, `dir="ltr"` on the OTP group and phone field, auto-verify on the 5th digit, the
+ server-driven resend cooldown (`resendAvailableInSeconds` seeds `useCountdown`), resend staying
+ available during lockout as the escape hatch, and the distinct wrong/expired/locked/429 states.
+- **`next` must never become an open redirect.** Only same-origin relative paths, validated in the pure
+ helper (with unit tests covering `//evil.com`, `https:` schemes, and role-forbidden paths).
+- **One login stack.** `intendedRole` parameterizes; it never forks customer/nurse login trees.
+- **Admin roles are never offered** in SelectRole; the middleware stays a UX gate, not a security boundary.
+- **Design-contract non-negotiables:** every new string in **both** `messages/en.json` and
+ `messages/fa.json`; tokens/palette keys, never hexes; RTL-safe logical props (verify `/fa` first);
+ both color schemes; MUI v9 API only; illustrations are CSS/SVG in tokens — no stock photos; shared
+ components (`OtpInput`, `PhoneNumberField`, `RelationSelect`, anything new in `components/`) keep/get
+ co-located `.test.tsx`; `clientFetch`/cookie rules untouched.
+
+## 6. Definition of Done
+
+On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):
+
+- [ ] `npm run check` green; `npm run test:ci` green for every touched shared component
+ (`OtpInput`, `PhoneNumberField`, `RelationSelect`, the new routing helper's unit tests, and any
+ new shared component); `en.json`/`fa.json` in sync.
+- [ ] Visual verification on all four axes — `/fa` + `/en` × light + dark — and mobile (360px) +
+ desktop for login, OTP, select-role, terms/privacy, and onboarding.
+- [ ] The login screen shows the brand mark, tagline, trust bullets, and consent line; no starter
+ chrome, no pencil, no hard-coded English anywhere in the flow.
+- [ ] OTP inputs carry `autoComplete="one-time-code"`; WebOTP auto-fills on a supporting
+ Android/Chrome device (or is verified degrading silently elsewhere); backspace clears the
+ previous digit in one keypress.
+- [ ] The resend countdown renders ۰۱:۲۳ on `/fa` and 01:23 on `/en`; the masked phone echo renders
+ un-scrambled inside the Persian sentence.
+- [ ] A 429 on request-OTP renders with error styling on the field, not grey helper text.
+- [ ] `/terms` and `/privacy` load logged-out in both locales; the consent line links to them.
+- [ ] Select-role shows the illustrated fork; the selected card has a soft fill + check glyph; keyboard
+ selection (Tab, Enter/Space) still works.
+- [ ] `/onboarding` renders chrome-free (no BottomBar/bell), opens with the welcome screen, shows four
+ visually distinct relation cards, and still creates the patient and lands on Home.
+- [ ] Visiting `/fa/bookings` logged-out → login → after OTP, landing back on `/fa/bookings`;
+ `?next=//evil.com` and `?next=https://evil.com` are ignored (role-home fallback).
+- [ ] REQ-039 appended to the tracker.
+
+## 7. How to test (what a human can verify after this phase)
+
+1. Open `/fa` logged-out → redirected to `/fa/login`. The screen is a branded hero: logotype, tagline,
+ trust bullets, consent line with working «شرایط استفاده»/«حریم خصوصی» links. Toggle dark mode and
+ `/en` — everything tracks.
+2. Enter a phone (demo seeds `0912000000x`), request the code. On the OTP screen the masked number reads
+ `0912•••…` correctly inside the Persian sentence; the countdown ticks in Persian digits on `/fa`.
+3. Type a wrong digit mid-code, press backspace twice — each press erases one digit. Paste a 5-digit
+ code — boxes fill and verify fires automatically (unchanged).
+4. Spam request-OTP until a 429 → the message renders in error styling on the field.
+5. On an Android/Chrome device with a WebOTP-formatted SMS (post REQ-039): the code fills without
+ typing. On desktop Firefox/Safari: no errors in console; manual entry works.
+6. Log in with a fresh phone → `/select-role` shows two illustrated cards; select the nurse card —
+ soft fill + check glyph appear; the reassurance line about adding the other role later is present;
+ arrow/Enter keyboard selection works.
+7. As a fresh customer with zero patients → land on `/onboarding`: welcome screen → relation step with
+ four distinct icons → patient form (relation pre-filled and hidden) → save → Home. During the wizard
+ there is no bottom nav or bell to tab away to.
+8. Logged-out, open `/fa/bookings` → login carries `?next=%2Fbookings` → after OTP you land on
+ `/fa/bookings`. Repeat with `?next=https://evil.com` manually — you land on your role home.
+9. Sanity: a seeded nurse still lands on `/nurse`, an admin on `/admin` (RoleRouter untouched); `?role=nurse`
+ still pre-selects the nurse copy and carries into select-role.
+
+## 8. Hand off & document (close the phase)
+
+- Update `client/CLAUDE.md` **Project Structure**: the new `(public-routes)` `terms`/`privacy` pages, the
+ relocated onboarding route group and its focused layout, any new/renamed auth components, and the new
+ routing helper — in the same change.
+- Append **REQ-039** to
+ [for-backend.md](../../shared-working-context/frontend/requests/for-backend.md) as specified in §4.
+- Write the frontend report at `dev/shared-working-context/reports/ui-phase-3-report.md`: what shipped
+ per scope item, the REQ filed, the **draft-legal-copy flag** for `/terms`/`/privacy` (human/legal
+ review required before launch), any icon-registry additions made under the Phase-0 ownership rule, and
+ the four-axes verification evidence.
+- Save a memory note per operating-rules §8: auth/first-run redesigned (login hero + consent, WebOTP +
+ OTP ergonomics, illustrated select-role, focused onboarding, validated `returnUrl`), REQ-039 pending
+ server-side SMS template, skip-onboarding deliberately deferred to Phase 4.
diff --git a/dev/post-phase/ui/ui-phase-4-customer-storefront.md b/dev/post-phase/ui/ui-phase-4-customer-storefront.md
new file mode 100644
index 0000000..7ce18df
--- /dev/null
+++ b/dev/post-phase/ui/ui-phase-4-customer-storefront.md
@@ -0,0 +1,280 @@
+# UI Phase 4 — Customer storefront
+
+> **Mission:** turn home → search → results → nurse profile from functional screens into a **storefront that
+> answers "would I let this person into my mother's home?"**. The data layer already fetches the trust signals
+> (gender, completed visits, verification badge, ratings) — the UI drops most of them on the floor, ships a dead
+> search bar, a fake sort control, a Gregorian date picker in a Shamsi product, and a primary CTA buried under an
+> infinite review list. This phase fixes the funnel's honesty defects and builds the reusable trust-presentation
+> components (`VerificationPanel`, the tappable `TrustBadge` explainer) that phase 8's public-profile preview reuses.
+>
+> **Track:** frontend · **Depends on:** [Phases 0–2](ui-phase-2-shells-and-navigation.md) ·
+> **Unlocks:** [Phase 5 (booking funnel)](ui-phase-5-booking-lifecycle.md) + [Phase 8](ui-phase-8-nurse-business-and-verification.md) (reuses the trust components built here)
+> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md) and invoke the frontend-designer skill — both are mandatory.**
+
+## 1. Context — where this sits
+
+The storefront is the four customer discovery screens: `client/src/app/[locale]/(private-routes)/(customer)/page.tsx`
+(A5 home), `search/page.tsx` (C1 filters), `search/results/page.tsx` (C2 results), and
+`search/nurse/[nurseId]/page.tsx` (C3 profile). Phases 0–2 already de-startered the theme, primitives, and chrome —
+this phase redesigns the route tree itself. Diagnosed defects (all verified in code, full evidence in
+[audit/customer-storefront.md](audit/customer-storefront.md)):
+
+1. **The home search bar is a dead affordance.** `HomeSearchBar` pushes `?q=` (page.tsx L130) but
+ `SearchFilterScreen` reads only `category_id` (search/page.tsx L49–50) and silently discards `q`. The placeholder
+ promises «جستجوی خدمت یا پرستار…» and the input does nothing.
+2. **A failed patients query bricks the home forever.** The gate `if (data == null || isEmpty) return `
+ (page.tsx L66–68) has no `isError`/retry branch — a transient API failure leaves the app's front door on a spinner.
+3. **The patient-record nudge renders unconditionally forever** (page.tsx L96–102), unlike the profile nudge which is
+ gated on `hasCustomerProfile` (L103).
+4. **The C1 visit-date filter is a native Gregorian `type="date"`** (search/page.tsx L104–110) in a product where
+ every displayed date is Shamsi; the gender facet re-implements an inline `ToggleButtonGroup` (L86–100) instead of
+ the shared `GenderToggle`; the live-count CTA is the last child of a long scrolling form (L130–140).
+5. **C2 ships a fake sort** — a `TextField select` with one hard-coded `MenuItem`, `value="rating"`, no `onChange`
+ (results/page.tsx L67–69) — and no filter recap: `backToFilters` renders only inside the EmptyState (L58/L88), so
+ editing filters from a populated list means browser-back.
+6. **`NurseResultCard` is information-thin.** The result row IS a bookable variant, but the card never names the
+ service — three variants of one nurse render as identical cards differing only in price. `nurseGender` and
+ `totalCompletedBookings` are **served by the real backend today** (REQ-012 delivered them onto the index row — see
+ `services/search/apis/clientApi.ts` `NurseSearchResultDto`) and never rendered. The variant display name is a
+ genuine wire gap: neither `NurseSearchResult` (types.ts) nor the DTO carries it.
+7. **C3 doesn't read as a dossier.** The «درخواست رزرو» CTA is the last page child (nurse/[nurseId]/page.tsx L92–101)
+ — below the infinite reviews list when that tab is open — and not sticky. `totalCompletedBookings` is fetched and
+ never shown; `TrustBadge` is a static chip with no affordance to learn *what* was verified; and the profile DTO
+ does **not** serve gender (`clientApi.ts` stubs `nurseGender: 'female'` with an "unused" comment — never render it).
+8. **The empty-results copy is nonsense**: `search.empty_suggest_city` = «شهر نزدیک دیگری مانند مشهد، اصفهان یا شیراز
+ را امتحان کنید» (fa.json L361) — a family cannot "try Shiraz"; the patient lives where they live.
+
+**What already exists (do not rebuild):**
+
+- The search architecture: **the filter object IS the URL IS the React Query cache key**
+ (`services/search/keys.ts` + `filterParams.ts`), `keepPreviousData`, debounced Toman price inputs, live count on C1.
+- The **real** search backend (`USE_SEARCH_MOCK = false` since refinement-phase-4): the index row serves
+ `nurseName`/`avatarUrl`/`distanceKm`/`nurseGender`/`totalCompletedBookings`; `GET nurses/{id}/profile` serves
+ bio/years/services/latestReview.
+- The public trust-badge read: `useNurseTrustBadge(nurseId)` → `GET nurses/{id}/trust_badge` →
+ `{ isVerified, approvedAt, credentialTypes[] }` (`services/verification`) — real data for the explainer.
+- Phase 0's theme/brand/icon registry; phase 1's primitives (EmptyState/ErrorState kit, PageHeader, ``,
+ **Jalali date picker**, skeleton twins pattern, RatingInput v2 with honest fractional stars); phase 2's shells,
+ safe-area-aware `BottomBar`, and contextual customer header. Consume them; never fork local variants.
+- All four data states on C1/C2/C3, `ProfileSkeleton`, the honest «کل شهر» district semantics, `PriceDisplay` money
+ rules, the gender facet's humane hint copy.
+
+## 2. Required reading (do this first)
+
+- [audit/customer-storefront.md](audit/customer-storefront.md) — the 21 problems + keep-list for this route tree.
+- [audit/feature-components.md](audit/feature-components.md) — `NurseResultCard`/`TrustBadge` findings + the
+ component-layer keep-list (presentational purity, caller-owned i18n, token discipline).
+- The four pages under `client/src/app/[locale]/(private-routes)/(customer)/` listed above, plus
+ `search/useSearchFilters.ts` and `services/search/{types.ts,filterParams.ts,apis/clientApi.ts}` — know exactly what
+ the wire serves before deciding what needs a REQ.
+- `client/src/components/{NurseResultCard,TrustBadge,GenderToggle,ServicePriceRow,PriceDisplay}/` — the components
+ this phase owns or extends.
+- `client/src/services/verification/types.ts` (`TrustBadge` wire type, `publicBadgeState`) and
+ `hooks/useNurseTrustBadge.ts` — the explainer's data source.
+- [../../../.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md) — invoke it;
+ and skim `client/CLAUDE.md` "Golden rules".
+- Product rules: [../../../product/overview/platform-summary.md](../../../product/overview/platform-summary.md) (the
+ four ground truths), [../../../product/business/04-search-and-matching.md](../../../product/business/04-search-and-matching.md)
+ (verified-only, same-gender, variant-is-the-unit), and
+ [../../../product/business/02-nurse-verification.md](../../../product/business/02-nurse-verification.md) (what the
+ pipeline actually verifies — the explainer must narrate this truthfully).
+
+## 3. Scope — build this
+
+### 3.1 Customer home — a front door that works
+
+- **Fix the dead search bar — decision: convert it into an honest search entry point.** Replace the free-text
+ `TextField` with a tappable search affordance (a faux-input `ButtonBase` styled as the search field) that routes to
+ C1 (`/search`), optionally focusing the category grid. *Why not client-side text match:* the index has no text
+ column, variant names are not queryable client-side, and the only matchable dataset (5–6 cached category names) is
+ already better served by the category grid directly below — a half-working text field over-promises exactly where
+ trust matters. File the **text-search REQ** (`q` over nurse/variant/category names on `search/nurses`) so the bar
+ can upgrade to a real typeahead when the backend serves it; note the upgrade path in the component's JSDoc.
+- **Add the missing `isError` branch**: `usePatients()` failure renders the phase-1 `ErrorState` with retry — never an
+ eternal spinner. Keep the onboarding redirect logic (`isEmpty` → onboarding) exactly as is.
+- **Compact trust strip** under the greeting: three icon+label items — «پرداخت امن امانی» · «پرستاران تاییدشده» ·
+ «پشتیبانی» — quiet, one row, tokens only. This is ambient reassurance, not a hero; keep it to one line on mobile.
+- **Gate the patient-record NudgeCard on actual record incompleteness.** Derive a completeness signal from the cached
+ patients data (e.g. a patient missing conditions/age — inspect `services/patients` types for what's derivable). If
+ no honest signal is derivable client-side, show the nudge only when a patient list exists but a patient was never
+ opened/completed — and make it dismissible (session-scoped) rather than permanent. Never a forever-nudge.
+- **Rebook shortcut row**: source recent bookings from the existing `useBookingList('customer')` cache
+ (`services/bookings`) and render up to 2 «رزرو دوباره با …» cards deep-linking to the nurse's C3 profile. Repeat
+ care is the dominant pattern in home nursing. Render nothing (no empty state) when there are no past bookings.
+
+### 3.2 Search (C1) — Persian-native filters, visible feedback
+
+- **Replace the native `type="date"`** with the phase-1 Jalali controls: a horizontal day-chip strip — «امروز»،
+ «فردا»، then day-name + Shamsi date chips for the next 7 days — plus an entry to the full Jalali picker for later
+ dates. The value stays the same ISO string `dateIntent` the flow already carries (intent-only, never a hard filter
+ — preserve that semantic and its hint copy).
+- **Make the live-count CTA a sticky bottom bar**: «مشاهده N پرستار» pinned above the `BottomBar`, safe-area aware
+ (compose with phase 2's safe-area handling — don't reimplement `env(safe-area-inset-bottom)` locally). The count is
+ the screen's best feedback instrument; it must be visible while adjusting the upper filters.
+- **Reuse the shared `GenderToggle`** instead of the divergent inline `ToggleButtonGroup`. `GenderToggle` is
+ male/female-only by design (required, never defaulted — booking context); extend it with an **opt-in `allowAny`
+ prop** (adds the «فرقی ندارد» option) rather than forking, keep the booking-context behavior unchanged, and update
+ its co-located test for both modes.
+- **Never render «مشاهده ۰ پرستار» as a tappable CTA.** When the live count is 0, the sticky bar shows a non-CTA
+ message («پرستاری با این فیلترها یافت نشد») with the same relaxation hints as 3.6. Full ICU zero-case catalog sweep
+ is (DEFERRED → [phase 12](ui-phase-12-copy-motion-and-polish.md)) — just don't ship the zero-CTA here.
+
+### 3.3 Results (C2) — honest header, editable filters
+
+- **Tappable filter-recap chip row** under the count: category · region (city/district or «کل شهر») · gender · price
+ range — each chip deep-links back to C1 **with state preserved**. The filters already live in the URL
+ (do-not-regress); C1 must initialize from the carried params, not just `category_id` — extend `useSearchFilters` to
+ hydrate from a full `searchParamsToFilters` read.
+- **Kill the fake sort.** Replace the single-option `TextField select` with a static caption «مرتبشده بر اساس
+ امتیاز» (the contract's only MVP sort — `SearchSort = 'rating'`). Real sort options only if the API grows them
+ (DEFERRED — do not file a REQ for sorts the product hasn't asked for).
+- **Adopt skeleton twins**: replace the generic `Skeleton height={112}` rows with `NurseResultCard.Skeleton`
+ (built in 3.4, following phase 1's twin pattern) so loading matches the card anatomy exactly.
+
+### 3.4 `NurseResultCard` v2 — the decision card (this phase owns it)
+
+Rebuild the card as the four-second decision unit, staying **presentational + memoized** with its co-located test:
+
+- **Service/variant display name** — the row IS a variant; the card must name what is being bought. This field is
+ missing from the wire (`NurseSearchResultDto` has no display name — verified): **file the REQ** to denormalize
+ `variantDisplayName` onto the index row. Until it lands, render the row's category name (available via the cached
+ catalog reference data, passed in by the page — the card stays data-agnostic) so multi-variant nurses are at least
+ distinguishable by price row; switch to the served name when the REQ lands.
+- **Nurse gender indicator** — load-bearing for same-gender matching; `nurseGender` is served today and never
+ rendered. A quiet chip/glyph («خانم»/«آقا» styled per the design language), never color-only.
+- **Completed-visits count** — `totalCompletedBookings` is served today; render «N ویزیت موفق» (fa digits). This
+ carries more information than the uniform verified chip (every row is verified by invariant).
+- **Optional top review tag** — one line, e.g. «منظم و دقیق», only if served. The index row carries no review tag
+ (verified): file it as an optional field on the same index-row REQ; the card renders it conditionally.
+- **`NurseResultCard.Skeleton`** static twin matching the new anatomy (avatar disc, name+badge row, meta row, price
+ row).
+- Keep: `memo`, stable `onSelect`, keyboard/focus a11y, `PriceDisplay` for money, `TrustBadge` for the badge.
+
+### 3.5 Nurse profile (C3) — the trust dossier + the shared trust components
+
+- **Header identity card**: large avatar, name, gender chip, years of experience, completed visits («N ویزیت موفق» —
+ fetched today, never rendered), rating + count. The profile DTO does **not** serve gender (the client stub is a
+ placeholder — never render it): **file the REQ** to add `nurseGender` to `NursePublicProfileDto`; until it lands,
+ omit the gender chip on C3 (the C2 card and the carried `required_gender` param cover the matching flow honestly).
+- **`VerificationPanel` — new shared component** (`src/components/VerificationPanel/`, barrel + test): "what
+ Balinyaar verified" as a check-listed panel, fed by `useNurseTrustBadge(nurseId)` — verified state, `approvedAt`
+ (Shamsi), and one row per `credentialTypes[]` entry (stable codes → i18n labels, never raw wire values). Render
+ only what is served — no invented steps, no fake dates.
+- **Tappable `TrustBadge` explainer**: give `TrustBadge` an opt-in `onClick`/expand affordance that opens a
+ bottom-sheet (mobile) / dialog (desktop) narrating the verification story — «این پرستار این مراحل را گذرانده است…»
+ — rendering the same `VerificationPanel` inside. The default (non-interactive) badge everywhere else is unchanged;
+ update `TrustBadge.test.tsx`. Wire it up on C2 cards and C3. **File the REQ** for public per-step verification
+ detail (step codes + decided dates) so the panel can list identity/Shahkar/license individually with dates; until
+ then the panel shows the credential-type rows + the approval date — honest, served data only.
+ These two are **REUSED by [phase 8](ui-phase-8-nurse-business-and-verification.md)'s public-profile preview — build
+ them shared**, presentational, caller-fed.
+- **Sticky «درخواست رزرو» bottom CTA** (same sticky-bar treatment as 3.2, price-from beside the button) so the CTA
+ survives the infinite reviews list. Keep the carried variant/gender/date param handoff to f7 exactly as is.
+- **Reviews tab polish**: aggregate header uses phase 1's RatingInput v2 (fractional — kill the `Math.round`
+ overstatement at L260 by consuming the v2 API), review cards get the card-kit treatment. Rating-distribution bars
+ are (DEFERRED → phase 12) unless trivially derivable from served data.
+- Render the already-fetched `latestReview` snippet on the services tab if it improves the dossier — optional.
+
+### 3.6 Empty-results copy — honest relaxation
+
+Replace `empty_suggest_city` in **both** catalogs with honest relaxation suggestions the family can actually act on:
+widen to the whole city («منطقه را خالی کنید تا کل شهر جستوجو شود» — keep/merge with the existing district line),
+try other dates, remove the gender filter. Delete the «مشهد/اصفهان/شیراز» line. Reuse the same relaxation vocabulary
+in the C1 zero-count bar (3.2). Final wording sweep is phase 12's; this phase removes the nonsense and lands
+plausible copy in both `en.json`/`fa.json`.
+
+**(DEFERRED)**: public/guest storefront and landing page (→ [phase 13](ui-phase-13-public-front-door.md));
+save/favorite/share nurses (post-MVP, product decision); collapsing multi-variant nurses into one card (needs the
+variant-name REQ served first — note it in the report as a follow-up); C1/C2 Suspense-fallback skeletons beyond what
+phase 1's route-level `loading.tsx` already covers.
+
+## 4. Mocks & seams in this phase
+
+**None introduced.** Search and reviews are real (`USE_SEARCH_MOCK = false`); verification stays mock-primary but the
+public `trust_badge` read flows through the existing `services/verification` seam either way — this phase only
+consumes hooks. Backend gaps become REQ entries appended to
+[../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
+— **REQ-001…038 are taken; check the tracker for the next free number at execution time** (parallel UI phases also
+file). Expected filings from this phase:
+
+1. **Free-text search** — `q` param on `GET search/nurses` matching nurse/variant/category names (the home bar's
+ upgrade path, 3.1).
+2. **Index-row enrichment** — `variantDisplayName` (required) + optional `topReviewTag` on `NurseSearchResultDto`
+ (3.4).
+3. **`nurseGender` on `NursePublicProfileDto`** (3.5 header chip).
+4. **Public verification-step detail** — per-step passed checks + decision dates on (or beside) the `trust_badge`
+ payload (3.5 explainer depth).
+
+The UI stays mock-tolerant: every new render is conditional on the field being served; nothing blocks on a REQ.
+
+## 5. Critical rules you must not get wrong
+
+- **Verified-only invariant.** Every returned row is verified by the search-index invariant; the UI **never
+ re-filters or re-checks** verification and never fakes a badge state. The explainer *adds* narration to served
+ truth — `TrustBadge`'s three honest states (verified/unverified/expired) and their server-driven derivation stay
+ exactly as they are.
+- **Never render placeholder data as truth.** The C3 profile's stubbed `nurseGender: 'female'` must not reach the
+ screen; no invented verification steps or dates in the panel.
+- **The filter-object-IS-the-URL-IS-the-query-key architecture stays.** Recap chips, C1 hydration, and the sticky CTA
+ all read/write the same `filterParams` (de)serializer; deep-linking and back/forward cache hits must keep working.
+- **Money rules**: amounts render only through `PriceDisplay`/the money util (BigInt IRR, Toman at the boundary);
+ totals only ever price × sessionCount; never parse money to a float.
+- **Same-gender facet care**: never defaulted, hint copy preserved, the chosen value carried into booking as before.
+- **Design contract**: i18n keys in **both** catalogs; tokens not hexes (`--bal-*` / palette keys only); RTL logical
+ props (`marginInlineStart`, `start`/`end`) — verify at `/fa` first; dark mode via tokens; MUI v9 API only; shared
+ components (`VerificationPanel`, extended `TrustBadge`/`GenderToggle`/`NurseResultCard`) keep/get co-located tests;
+ `clientFetch`/cookies/services rules untouched — this phase adds **no** fetch code outside existing hooks.
+- **Ownership**: phase 0 owns `theme/`/`AppIcon`/`AppButton`; phase 1 owns the shared primitives (Jalali picker,
+ RatingInput, state views, skeleton-twin pattern); phase 2 owns `layout/`. If a foundation gap blocks you (e.g. a
+ missing icon or sticky-bar primitive), extend the foundation file **minimally** and note it in your report — never
+ fork a local variant.
+
+## 6. Definition of Done
+
+On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):
+
+- [ ] `npm run check` green; `npm run test:ci` green — including updated/new tests for `NurseResultCard` (+ its
+ Skeleton), `TrustBadge` (interactive mode), `GenderToggle` (`allowAny`), and `VerificationPanel`.
+- [ ] `en.json`/`fa.json` in sync; «مشهد/اصفهان/شیراز» is gone from both.
+- [ ] The home search affordance routes to C1; a failed patients query shows retry (kill the API to prove it); the
+ record nudge no longer renders for a complete record; a customer with past bookings sees the rebook row.
+- [ ] C1: Jalali day-chips replace the Gregorian input; the count CTA is sticky and never tappable at count 0; the
+ gender facet is the shared `GenderToggle`.
+- [ ] C2: recap chips deep-link back to C1 with **all** filters preserved (deep-link a full URL to prove hydration);
+ no interactive-looking dead sort; skeleton twins during load.
+- [ ] Cards show variant/category label, gender, completed visits; C3 has the sticky CTA, completed visits in the
+ header, `VerificationPanel`, and tappable badges opening the explainer; no gender chip on C3 until the REQ lands.
+- [ ] Visual verification on the four axes — `/fa` + `/en` × light + dark — and mobile + desktop for home, C1, C2,
+ C3 (sticky bars must clear the BottomBar and the home-indicator safe area on mobile).
+- [ ] REQs filed in the tracker with the next free numbers; no edits outside `client/` and the tracker/report files.
+
+## 7. How to test (what a human can verify after this phase)
+
+1. **Home**: log in as the seeded customer → the search bar tap opens C1 (no dead `?q=`). Stop the API and reload →
+ an error card with «تلاش مجدد», not an eternal spinner. Restart → home recovers; trust strip visible under the
+ greeting; a customer with a completed booking sees «رزرو دوباره با …».
+2. **C1**: the date filter shows «امروز/فردا» + Shamsi day chips (no Gregorian browser calendar anywhere). Pick
+ filters → the count CTA stays pinned at the bottom while scrolling; set filters matching nothing → the bar shows
+ the no-results message and is not tappable.
+3. **C2**: run a search → recap chips show category/region/gender/price; tap one → C1 opens with every filter
+ pre-filled; browser-back returns to identical results with zero network (cache hit). The header reads «مرتبشده
+ بر اساس امتیاز» as text, not a dropdown.
+4. **Cards**: a nurse with multiple variants shows distinguishable cards (service label + price); every card shows
+ gender and «N ویزیت موفق»; tapping the ✓ badge opens the verification bottom-sheet.
+5. **C3**: open a profile → header shows completed visits + rating; `VerificationPanel` lists the served credential
+ types + approval date (Shamsi); open the reviews tab and scroll deep → «درخواست رزرو» stays pinned; a 4.5 average
+ renders as a fractional star row, not five full stars. Tap the CTA → the C4 request form receives the same
+ nurse/variant/gender/date params as before.
+6. Repeat 1–5 on `/en` (LTR) and in dark mode; on a mobile viewport confirm both sticky bars sit above the BottomBar.
+
+## 8. Hand off & document (close the phase)
+
+- Update `client/CLAUDE.md` "Project Structure": the C1/C2/C3 line items (Jalali chips, sticky CTA, recap chips,
+ dossier layout) and the new `VerificationPanel` component entry; note `TrustBadge`'s new interactive mode and
+ `GenderToggle`'s `allowAny` on their lines.
+- Write the report at `dev/shared-working-context/reports/ui-phase-4-report.md`: what changed per screen, the exact
+ REQ numbers filed (text search, index-row enrichment, profile gender, public step detail), the home-search-bar
+ decision + rationale, the multi-variant-collapse follow-up, and any foundation files you extended.
+- Save a memory note per operating-rules §8: phase 4 owns `NurseResultCard`; `VerificationPanel` + the `TrustBadge`
+ explainer are the shared trust components phase 8 consumes; the C3 profile DTO still lacks gender (placeholder
+ field — never render it) until its REQ lands.
diff --git a/dev/post-phase/ui/ui-phase-5-booking-lifecycle.md b/dev/post-phase/ui/ui-phase-5-booking-lifecycle.md
new file mode 100644
index 0000000..b3b0161
--- /dev/null
+++ b/dev/post-phase/ui/ui-phase-5-booking-lifecycle.md
@@ -0,0 +1,280 @@
+# UI Phase 5 — Booking lifecycle
+
+> **Mission:** the request→track→booking→cancel→review flow is functionally rich but visually generic, and
+> hides real defects: a confirm dialog whose **dismiss button carries the destructive action's own label**,
+> required-field validation that is **provably dead code**, and a customer requests inbox
+> (`useCustomerRequests`) **exported but wired to nothing** — a pending, money-adjacent request is orphaned
+> once the user leaves C5. Make the lifecycle legible, calm, and trustworthy: the family always sees *who*
+> they're inviting home, always has a way back to a pending request, and every terminal state offers recovery.
+>
+> **Track:** frontend · **Depends on:** [Phases 0–2](ui-phase-2-shells-and-navigation.md);
+> [Phase 4](ui-phase-4-customer-storefront.md) recommended (funnel order) · **Unlocks:**
+> [Phase 6](ui-phase-6-checkout-and-money.md) (checkout follows acceptance)
+> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md)
+> and invoke the frontend-designer skill — both are mandatory.**
+
+## 1. Context — where this sits
+
+The booking lifecycle is the emotional core of the product: a family hands a stranger their patient's details
+and home address, then waits on two countdowns (nurse response, then a 30-minute payment window). Every
+screen already works — services real or contract-shaped, deadlines server-frozen, money display-only — but
+the presentation is a default-MUI form stack that undersells the trust story and ships four verified defects
+(all re-confirmed in code, 2026-07-16):
+
+1. **The cancel-request dialog is mislabeled.** In `bookings/request/[id]/page.tsx` lines 223–225 the
+ dismiss button renders `t('cancel_request')` («انصراف از درخواست») — the destructive action's own label —
+ next to the real destructive `cancel_confirm_yes`; users who want to cancel click the button that keeps
+ the request.
+2. **C4's inline errors are unreachable.** `setAttempted(true)` runs only inside `handleSubmit` (line 128)
+ while submit is `disabled={!requiredChosen || …}` (line 439), so `error={attempted && …}` (line 230 etc.)
+ never renders; the only feedback for an incomplete form is a silently disabled button.
+3. **A pending request is unreachable once you leave C5.** `useCustomerRequests` is exported from
+ `services/bookingRequests` and consumed by **zero** pages (grep: only `index.ts` + its own hook file);
+ `bookings/page.tsx` lists only post-payment bookings; the bottom nav has no requests entry.
+4. **The family never sees who they're inviting home.** C4 fetches `useNurseProfile` (line 61) but renders no
+ nurse name, avatar, rating, or badge — the profile is used only for the gender-mismatch check.
+
+Also verified: the bookings list calls `useBookingList('customer')` with no `page` param though the hook
+paginates — booking #21 is unreachable; C4 keeps a native Gregorian `type="date"` (line 336), a
+`pointerEvents: 'none'` fake-map preview (line 317), and negative-margin stitching (lines 272/424);
+`BookingRequestSummaryCard.tsx` builds `whenLabel` (line 60) with no bidi isolation while
+`SessionCard.tsx:99` wraps its identical range in `dir="ltr"`; the review page fires `useMyReviewForBooking`
+ungated (line 43) though the hook accepts `{ enabled }`; the cancel page pre-defaults its reason to
+`'changed_mind'` (line 58); `BookingDetailView.tsx:92` reuses the `unnamed_nurse` *fallback* key as the
+nurse field *label*.
+
+**What already exists (do not rebuild):**
+
+- The full functional flow: C4 form, C5 tracker, bookings list + detail, cancel flow with
+ `CancellationPolicyDisclosure`, refund status, review page — and the service layer beneath it:
+ `services/bookingRequests` (incl. the unused `useCustomerRequests`), `services/bookings` (paginated
+ `useBookingList`, sessions/EVV), `services/refunds`, `services/reviews` (`useReviewEligibility`,
+ `useMyReviewForBooking` with an `enabled` option), `services/tickets`.
+- Phase 1's primitives: CountdownTimer v2 (progress ring), the vertical `StatusTimeline`, StatusChip v2
+ (soft tints), the Jalali date picker, ``, skeleton twins, EmptyState/ErrorState. **Consume these;
+ never fork a local variant** (README ownership rules). Phase 4's trust components (`TrustBadge` patterns,
+ NurseResultCard v2, rating display).
+- The do-not-regress architecture: server-frozen deadlines, two-stage disclosure gate, honest refund copy,
+ advisory EVV, shaped skeletons (§5).
+
+## 2. Required reading (do this first)
+
+- [audit/booking-lifecycle.md](audit/booking-lifecycle.md) — the 19 problems, 10 opportunities, and
+ keep-list this phase is built from, with file/line evidence.
+- Code — read before touching, under `client/src/app/[locale]/(private-routes)/(customer)/bookings/`:
+ `request/page.tsx` (C4), `request/[id]/page.tsx` (C5), `page.tsx` (list), `[id]/page.tsx`,
+ `[id]/cancel/page.tsx`, `[id]/review/page.tsx`. Components: `booking/BookingDetailView/`,
+ `BookingRequestSummaryCard/`, `booking/SessionCard/` (the `dir="ltr"` precedent), `CountdownTimer/`.
+ Services: `services/bookingRequests/hooks/useCustomerRequests.ts`,
+ `services/bookings/hooks/useBookingList.ts` (it already paginates).
+- Phase 1's report (`dev/shared-working-context/reports/ui-phase-1-report.md`) for the exact APIs of
+ CountdownTimer v2, StatusTimeline, StatusChip v2, the Jalali picker.
+- `.claude/skills/frontend-designer/SKILL.md` — the design contract (invoke the skill, don't just read it).
+- Product rules: [product/business/05-booking-and-scheduling.md](../../../product/business/05-booking-and-scheduling.md)
+ (request lifecycle, deadlines, two-stage disclosure),
+ [product/business/07-cancellation-and-refunds.md](../../../product/business/07-cancellation-and-refunds.md)
+ (policy tiers — the disclosure copy is product-mandated),
+ [product/business/11-reviews-trust-and-safety.md](../../../product/business/11-reviews-trust-and-safety.md)
+ (moderation-before-publish), [product/business/06-evv-and-service-delivery.md](../../../product/business/06-evv-and-service-delivery.md)
+ (EVV is advisory, never blocking).
+
+## 3. Scope — build this
+
+### 3.1 C4 request form — a trust-anchored request, not a form stack
+
+`bookings/request/page.tsx`:
+
+- **Sticky nurse identity summary** at the top: avatar, name, rating + review count, `TrustBadge`, gender —
+ all already available from the fetched `useNurseProfile` (line 61). The family must always see who they're
+ inviting home. Compose from phase-4's card anatomy; compact and sticky on mobile scroll.
+- **«چه اتفاقی میافتد؟» strip** — a 3-step visual (درخواست → پاسخ پرستار → پرداخت امن) reinforcing the
+ money-free promise already in `form_subtitle`; use phase 1's step primitives, no new one-off stepper.
+- **Jalali picker + time-window chips** replacing the native Gregorian `type="date"` (line 336) and free
+ time fields: phase 1's Jalali date picker plus tappable window presets (صبح ۸–۱۲ / بعدازظهر ۱۲–۱۶ /
+ عصر ۱۶–۲۰) with a «زمان دلخواه» custom option that reveals the time fields — presets kill the end≤start
+ error class for most users. Nurse-availability hints on the picker are (DEFERRED → needs a backend
+ availability read; file a REQ only if you build the seam now).
+- **Fix the dead validation:** switch to touched-on-blur field errors **plus** a disabled-CTA explainer — a
+ one-line caption under the disabled submit listing what's missing («برای ادامه: انتخاب بیمار، تاریخ»).
+ Delete the unreachable `attempted`-only branches. Keep the gender-mismatch inline block exactly as is.
+- **Replace the fake-map preview** (lines 315–327, `pointerEvents: 'none'` around `AddressMapPicker`) with a
+ compact address row: icon, title, one-line street text, an «تغییر» affordance back to the select — the
+ grid-canvas stand-in communicates nothing and eats vertical space.
+- **Remove the negative-margin hacks** (`mt: -1.5` line 272, `mt: -2` line 424) — group price-under-select
+ and counter-under-notes with real composed containers (Stack spacing), so helper text can't collide.
+
+### 3.2 C5 tracker — a calm wait with a way out
+
+`bookings/request/[id]/page.tsx`:
+
+- **Countdown as the phase-1 progress ring** with humanized framing: the ring shows the fraction of the
+ response window remaining ((deadline − createdAt), server fields only — the client never recomputes the
+ deadline, §5); above ~10 minutes remaining render «حدود ۳ ساعت» instead of per-second digits, switching to
+ precise digits in the final minutes. Add a one-line «نتیجه را اطلاع میدهیم» note so users feel safe leaving.
+- **FIX the cancel-request dialog defect** (lines 223–225): the dismiss button must never carry the
+ destructive label. Adopt a clear confirm convention — destructive: «بله، انصراف از درخواست» (error,
+ contained); dismiss: «نه، نگه دار» (text, neutral) — and apply it to every confirm dialog you touch.
+- **Terminal-state recovery:** rejected/expired cards currently funnel to generic search. When the rejection
+ reason permits (not gender/coverage), offer «درخواست دوباره با زمان دیگر» — C4 reopened prefilled with the
+ same nurse/variant/patient/address (extend the query params C4 already accepts from C3) — plus a
+ «پرستاران مشابه» entry (same service + area) into existing search. Recover the intent, not from zero.
+
+### 3.3 Customer requests visibility — /bookings becomes the lifecycle home
+
+`bookings/page.tsx`:
+
+- **Segmented tabs:** «در انتظار پاسخ» / «فعال» / «گذشته». The pending tab finally wires the
+ exported-but-unused `useCustomerRequests` — rows show the nurse name, requested Shamsi slot, and a **live
+ mini-countdown chip** (compact CountdownTimer v2); accepted-awaiting-payment rows make the payment deadline
+ the primary CTA. Rows deep-link to C5.
+- **Pagination:** the list renders page 1 only while `useBookingList` already accepts `{ page, pageSize }`
+ and returns `total`. Add a pager (or load-more, matching the C2 results pattern) with locale digits.
+- **Status-accent rows:** a soft `StatusChip` (phase 1's soft-tint system) plus a status-colored
+ `borderInlineStart` accent per row so a page of bookings ranks visually without reading every chip.
+- **Rows fully tappable** — the whole row navigates (keyboard-focusable, `role`/`aria` correct), not just
+ the small button; give the error branch a retry and the empty state a CTA into search.
+
+### 3.4 Booking detail — a hero that answers where/when/who
+
+`components/booking/BookingDetailView/BookingDetailView.tsx` (+ detail page):
+
+- **Next-upcoming-session headline** («ویزیت ۲ · فردا ۹:۰۰» — derived from the served sessions,
+ display-only), the visit address, nurse avatar with a support entry (reuse `BookingSupportEntry`), and
+ **add-to-calendar** — a client-side `.ics` download for the next session (no backend; Gregorian UTC in the
+ file, Shamsi in the UI).
+- **Adopt phase 1's vertical `StatusTimeline`** in place of the horizontal `StepperHeader` usage — per-stage
+ icons, timestamps where the server provides them, terracotta marker on the current stage, distinct terminal
+ branch. The timeline remains server truth: never advance a step client-side.
+- **EVV as a presence state:** elevate the existing advisory EVV data into a headline on the detail («پرستار
+ در محل است · ورود ۰۹:۰۲») and a compact indicator on the in-progress list row. Keep the tri-state semantics
+ (in-range / out-of-range warning / no-GPS neutral) — never error-toned, never blocking.
+- **Session list polish:** align session cards to the phase-1 card anatomy; keep SessionCard's `dir="ltr"`
+ isolation. Fix the `unnamed_nurse` key misuse (line 92) — add a proper `bd_nurse_label` key and leave
+ `unnamed_nurse` as the fallback value it was written to be.
+
+### 3.5 Cancel flow — off-ramps before the kill switch
+
+`bookings/[id]/cancel/page.tsx`:
+
+- **Keep the policy disclosure untouched** — `CancellationPolicyDisclosure` (tier, refund %/fee %,
+ reconciled Toman split, per-session refundable/locked, acknowledgement checkbox) is do-not-regress.
+- **Add off-ramps above the disclosure:** «تغییر زمان» — opens a support ticket via the existing
+ `ContactSupportDialog`/tickets service, pre-categorized `coordination`; real rescheduling is (DEFERRED →
+ product decision + backend) — and «گفتگو با پشتیبانی», plus one human line about nurse impact. The
+ destructive path stays fully available — these are exits, not obstacles.
+- **Do not pre-default the reason:** replace `useState('changed_mind')` (line 58) with
+ an empty placeholder state; confirm stays disabled until a reason is chosen. Keeps the analytics honest.
+
+### 3.6 Review flow — context and expectations up front
+
+`bookings/[id]/review/page.tsx` + the bookings-list row:
+
+- **Context recap header:** service name, Shamsi visit date, nurse name/avatar (from the cached booking
+ detail — no new fetch) so the user knows exactly what they're reviewing.
+- **Moderation expectation note up front:** «نظر شما پس از بررسی منتشر میشود» before submit, not only in the
+ post-submit state — moderation-before-publish is a product rule the UI should disclose early.
+- **Post-completion star-strip CTA on the list row:** a completed booking without a review shows a compact
+ RatingInput-styled strip on its /bookings row deep-linking into the review page (the eligibility +
+ my-review hooks exist; gate the extra queries to completed rows only).
+- **Fix the ungated hook call:** pass `{ enabled }` to `useMyReviewForBooking` on the review page (line 43)
+ exactly as the detail page gates it — the hook already accepts the option.
+
+### 3.7 Misc verified defects
+
+- **Bidi-isolate `BookingRequestSummaryCard`'s `whenLabel`** (line 60/123): wrap the time-range segment in a
+ `dir="ltr"` span with tabular-nums, exactly matching `SessionCard.tsx` line 99. This is a shared, tested
+ component — update `BookingRequestSummaryCard.test.tsx` accordingly.
+- The «ادامه پرداخت ←» arrow-in-string CTA (fa/en `continue_payment`) is **phase 12's catalog sweep** — note
+ it in your report; removing the arrow on strings you already touch is fine, but don't sweep copy-wide here.
+
+## 4. Mocks & seams in this phase
+
+**None introduced.** Every deliverable is client-side over existing seams: `useCustomerRequests`,
+`useBookingList` pagination, and the review hooks already exist; the `.ics` file is generated in the browser;
+the «تغییر زمان» off-ramp rides the existing tickets service. `bookingRequests`/`bookings`/`reviews` run
+real (de-mocked in refinement-phase-4) — build against the real wire, UI mock-tolerant behind the seams.
+
+**REQ posture:** if a deliverable surfaces a genuine backend gap (a rejection-reason code C5's recovery logic
+needs, nurse-availability for the time chips), append a REQ to
+[for-backend.md](../../shared-working-context/frontend/requests/for-backend.md) — **REQ-001…038 are taken
+(tracker verified); number from REQ-039** — and ship degraded-gracefully. Never edit `server/`.
+
+## 5. Critical rules you must not get wrong
+
+From the audit keep-list (all currently true in code — regressions fail this phase):
+
+1. **Server-truth discipline.** `CountdownTimer` never computes or extends a deadline — the ring is
+ presentation over server-frozen instants; timelines never advance a step client-side; money stays
+ display-only IRR digit-strings via the money utils — never summed or re-split.
+2. **Two-stage disclosure is a hard UI gate.** The customer's care-instructions query **never fires**
+ (`useCareInstructions` stays enabled-gated to the nurse on confirmed+); keep the BookingDetailView test
+ proving it.
+3. **Honest cancel/refund copy stays.** The full pre-confirm disclosure, the acknowledgement checkbox, the
+ failed-refund state that suppresses all success framing, and the BNPL 7–10-day ETA are product-mandated.
+4. **Gender preference stays first-class.** Never silently defaulted; the inline mismatch block stays; the
+ culturally-tuned hint copy is untouched.
+5. **EVV stays advisory.** Out-of-range is warning-toned, no-GPS neutral, never blocking — the presence
+ headline is a positive reframe, not a new gate.
+6. **Skeletons stay shaped like content.** Every redesigned layout updates its skeleton twin in the same
+ change — no spinner-only regressions.
+
+Design-contract non-negotiables that bite here: i18n keys in **both** catalogs (this phase adds many);
+tokens/palette only, never hexes (soft chips use the phase-0/1 `--bal-*-soft` tokens); RTL logical props
+(`borderInlineStart` accents, `dir="ltr"` islands for times/digits); dark mode on every new surface; MUI v9
+API only; changed shared components keep/gain co-located tests; fetch/cookies rules untouched.
+
+## 6. Definition of Done
+
+On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):
+
+- [ ] `npm run check` green; `npm run test:ci` green including updated tests for every touched shared
+ component (`BookingRequestSummaryCard`, booking composites, new list-row components);
+ `en.json`/`fa.json` in sync.
+- [ ] Cancel-request dialog: dismiss reads «نه، نگه دار», destructive reads «بله، انصراف از درخواست» — the
+ dismiss button never carries a destructive label anywhere in the flow.
+- [ ] C4: nurse identity summary renders; blurred-empty required fields show inline errors; the disabled CTA
+ explains what's missing; no Gregorian `type="date"`, fake-map preview, or negative-margin stitching.
+- [ ] /bookings: three tabs; pending tab lists live requests with mini-countdowns deep-linking to C5; a list
+ of >20 bookings is fully reachable via the pager; rows fully tappable with status accents.
+- [ ] Booking detail: next-session hero with address + `.ics` download; vertical StatusTimeline (no
+ horizontal stepper); EVV presence headline while checked-in.
+- [ ] Cancel page: reason select starts empty (confirm disabled until chosen); both off-ramps present;
+ `CancellationPolicyDisclosure` byte-identical in behavior.
+- [ ] Review page: context recap + up-front moderation note; `useMyReviewForBooking` gated with `enabled`;
+ completed list rows show the star-strip CTA.
+- [ ] Visual verification on all four axes — `/fa` + `/en` × light + dark — and mobile + desktop for C4, C5,
+ /bookings, booking detail (`/fa` mobile first: the primary user).
+
+## 7. How to test (what a human can verify after this phase)
+
+1. From a nurse profile (C3) tap «درخواست رزرو» → C4 shows the sticky nurse card and the 3-step strip. Blur
+ the empty patient select → inline error; the disabled CTA lists the missing fields.
+2. Pick a date from the **Jalali** picker and tap the «صبح» chip → times fill; choose «زمان دلخواه» → custom
+ time fields appear. The address select shows a compact text row, not a grid canvas.
+3. Submit → C5 shows the countdown progress ring with «حدود …» framing. Tap «انصراف از درخواست» → the
+ dialog's keep-button reads «نه، نگه دار» and keeps the request; «بله، انصراف از درخواست» cancels it.
+4. Leave C5 → /bookings «در انتظار پاسخ» tab shows the pending request with a live mini-countdown; the row
+ returns to C5. Reject/expire a request (dev sim) → the terminal card offers «درخواست دوباره با زمان
+ دیگر» (C4 opens prefilled) and «پرستاران مشابه».
+5. With >20 bookings seeded, page 2 is reachable and booking #21 opens. Rows are tappable end-to-end; each
+ carries a soft status chip + matching inline-start accent.
+6. Open an active booking → hero shows «ویزیت … · », the visit address, nurse avatar, support entry,
+ a working `.ics` download, and a vertical timeline. Check a nurse in (dev EVV) → detail and list row show
+ «پرستار در محل است · ورود …».
+7. Start a cancellation → reason select is empty and confirm disabled; the off-ramps open a support ticket /
+ support chat; completing the flow shows the unchanged policy disclosure and acknowledgement gate.
+8. Open a completed booking's /bookings row → star-strip CTA → review page shows the service/date/nurse recap
+ and «نظر شما پس از بررسی منتشر میشود» before submit. Repeat 1–8 spot-wise on `/en` (LTR) and in dark
+ mode: `BookingRequestSummaryCard` time ranges read start–end both directions; no stock-MUI colors appear.
+
+## 8. Hand off & document (close the phase)
+
+- Update `client/CLAUDE.md` **Project Structure**: the /bookings entry (tabs + wired `useCustomerRequests` +
+ pagination), the C4/C5 descriptions, and any new shared components under `components/(booking/)`.
+- Write the report at `dev/shared-working-context/reports/ui-phase-5-report.md`: what changed per scope item,
+ the defects fixed (dialog labels, dead validation, orphaned inbox, ungated hook, bidi label), REQs filed
+ (REQ-039+, appended to [for-backend.md](../../shared-working-context/frontend/requests/for-backend.md) in
+ the standard entry shape — or "none"), and the phase-12 note (arrow-in-string CTA sweep).
+- Save a memory note per operating-rules §8: the lifecycle redesign decisions (tabs model, recovery paths,
+ presence state), the confirm-dialog labeling convention now in force, and what phase 6 (checkout) should
+ know about the accepted-request → payment handoff surfaces you touched.
diff --git a/dev/post-phase/ui/ui-phase-6-checkout-and-money.md b/dev/post-phase/ui/ui-phase-6-checkout-and-money.md
new file mode 100644
index 0000000..9900ca1
--- /dev/null
+++ b/dev/post-phase/ui/ui-phase-6-checkout-and-money.md
@@ -0,0 +1,280 @@
+# UI Phase 6 — Checkout & money
+
+> **Mission:** money UI is where trust is won or lost — and today the checkout total is a `subtitle2` row
+> buried inside a breakdown card, the payment "confirmation" has no reference code a user could quote in a
+> dispute, the top-level «کیفپول» tab is permanently empty for every card payer, and BNPL plans hide the
+> numbers people decide with. Restructure checkout around "who am I paying for / how much", turn the
+> confirmation into a real screenshot-worthy receipt, fill the wallet into the customer money hub, and make
+> the BNPL comparison honest — without touching one rial of the money pipeline itself.
+>
+> **Track:** frontend · **Depends on:** [Phases 0–2](ui-phase-2-shells-and-navigation.md) ·
+> [Phase 5](ui-phase-5-booking-lifecycle.md) recommended · **Unlocks:** the money moment finally earns the trust the ledger deserves
+> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md) and invoke the `frontend-designer` skill — both are mandatory.**
+
+## 1. Context — where this sits
+
+Balinyaar's pitch is "pay on-platform because your money sits in escrow until the visit is confirmed." The
+checkout → confirmation → wallet → invoice chain is where a family decides whether to believe that. The
+plumbing is excellent; the presentation treats the payment moment like any other form. Diagnosed current
+state (all verified in code):
+
+1. **The total is visually a footnote.** C6's page h1 is a plain `variant="h6"`
+ (`…/bookings/checkout/page.tsx:159`); the total — the most important number on screen — is a `subtitle2`
+ row inside `PriceBreakdown` (`components/PriceBreakdown/PriceBreakdown.tsx:62-63`); the pay CTA is the
+ last element of a scroll column (`page.tsx:202-225`), not sticky, not paired with the amount.
+2. **A live race between two payment paths.** During pay-initiate, `busy` (`page.tsx:148`) disables only the
+ card CTA (`:207`); the «پرداخت اقساطی» button (`:214-224`) stays tappable mid-redirect.
+3. **The confirmation is not a receipt.** `checkout/confirmation/page.tsx:51-113` renders only total +
+ variant + nurse name — no کد پیگیری, date-time, method, or booking number — and the amount panel is
+ `{summary ? … : null}` with no loading/error fallback: a failed fetch silently erases the paid amount.
+ The wire can't serve a receipt yet (`PaymentOutcomeDto`, `services/payment/types.ts:104-110`).
+4. **«کیفپول» is an empty promise.** `wallet/page.tsx` renders only `WalletInstallments` — provider-reported
+ BNPL plans. For card payers (the default; BNPL is mock-gated) the tab shows «طرح اقساط فعالی ندارید»
+ forever, wasting 1 of 5 bottom-nav slots — and it self-constrains to `maxWidth: 560`
+ (`WalletInstallments.tsx:24`) while checkout/confirmation/invoice run at the shell's 800.
+5. **BNPL hides the deciding numbers.** `BnplPlanCard.tsx:77-98` renders the down payment as a percent +
+ `LinearProgress` bar — a static fact styled as a loading indicator — with no Toman figure and no total
+ repayment cost; `PlanStep.tsx`'s «مبلغ کل» header silently swaps on plan tap (`plans.find(…) ?? plans[0]`);
+ providers are picked from two-letter text glyphs; `EligibilityStep.tsx:163` runs a multi-second credit
+ check behind a bare disabled button; `bnpl/return/page.tsx:100-104`'s invalid-link CTA says
+ «پرداخت با کارت» but navigates to the bookings list.
+6. **Spinner-stacked waits, forked terminal cards.** `checkout/return/page.tsx:139-147` shows title +
+ `CircularProgress` + a `pending` chip — three redundant signals at the flow's most anxious moment — and
+ its `StateCard` / C6's `MessageCard` are copy-pasted four times across the card & BNPL flows.
+7. **Currency-unit ambiguity.** Breakdown rows and installment amounts render bare grouped numbers; only the
+ total carries «تومان» (`PriceBreakdown.tsx:53` vs `:63`, `InstallmentScheduleRow.tsx:57`) — the classic
+ Toman/Rial second-guess, on wire amounts that are Rials displayed as Toman.
+
+**What already exists (do not rebuild):**
+
+- The money pipeline: `utils/money.ts` (BigInt, `formatIrrToToman`, `parseIrr`), IRR digit-strings on the
+ wire, Toman display-only, and `PriceBreakdown`'s dev reconciliation guard (`PriceBreakdown.tsx:35-42`).
+- The full C6 → return → confirmation flow and the 4-step BNPL wizard: idempotency-key-per-attempt,
+ benign-409 convergence, bounded outcome polling, every state (skeleton/error/empty/expired) covered.
+- `EscrowNotice` (product-mandated verbatim copy), `CountdownTimer` (server-frozen, LTR clock),
+ `RefundStatusCard` + `RefundEtaBanner`, `StatusChip`, `TrustBadge`, `PaymentStatusBadge`; the invoice's
+ print mechanics — visibility-scoped print area, `insetInlineStart` anchoring, dark→light token flip so
+ dark-mode users print on paper colors (`invoice/page.tsx:98-113`).
+- Phase 0's theme/`AppIcon` work, phase 1's primitives (``, PageHeader, state views, StatusTimeline),
+ phase 2's shells. Consume them; extend a foundation file minimally (noted in your report) — never fork a copy.
+
+## 2. Required reading (do this first)
+
+- [audit/checkout-money.md](audit/checkout-money.md) — the 18-problem inventory with file/line evidence, the
+ 8 opportunities this scope is drawn from, and the keep-list §5 repeats.
+- [../../../.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md) —
+ the design contract (two-layer tokens, `App*` wrappers, icon registry). Invoke it.
+- Code (paths abbreviate `client/src/app/[locale]/(private-routes)/(customer)` as `…`):
+ `…/bookings/checkout/page.tsx` (+ `return/`, `confirmation/`), `…/bookings/checkout/bnpl/page.tsx` (+ the
+ four step files, `return/`, `gateway/`), `…/wallet/` (both files), `…/bookings/[id]/invoice/page.tsx`;
+ components `PriceBreakdown`, `BnplPlanCard`, `InstallmentScheduleRow`, `EscrowNotice`, `RefundStatusCard`;
+ services `payment` (types + hooks + both `PaymentApi` impls), `bnpl`, `refunds`; `components/config.ts`
+ (`CONTENT_MAX_WIDTH`) and `src/layout/config.ts` (bottom-bar dimensions, for the sticky bar).
+- Product truth: [escrow-ledger.md](../../../product/payments/escrow-ledger.md) (what escrow actually
+ promises — the explainer must not overclaim),
+ [bnpl-landscape.md](../../../product/payments/bnpl-landscape.md) (provider-financed; the agreement is
+ customer ↔ provider), [iranian-payment-reality.md](../../../product/payments/iranian-payment-reality.md)
+ (gateway redirects, receipt culture, Toman/Rial). Plus `client/CLAUDE.md` "Golden rules" + Project Structure.
+
+## 3. Scope — build this
+
+### 3.1 C6 checkout: real hierarchy + sticky pay bar
+
+- **Prominent total.** Lift the total out of the breakdown into an unmissable figure near the top (an
+ `h4`-weight `` with «تومان»); replace the `h6`-as-h1 with the phase-1 PageHeader treatment.
+ `PriceBreakdown` keeps its total row — the page-level figure is the same served `totalIrr`, never recomputed.
+- **Sticky pay bar.** A page-level bar pinned above the customer bottom nav: total + pay CTA, always
+ co-located on mobile. Respect `env(safe-area-inset-bottom)` and the bottom-bar height from
+ `src/layout/config.ts` — do **not** edit `layout/` (phase 2 owns it); this is page composition.
+- **Identity moment.** Give `EngagementSummary` the nurse's avatar + verified `TrustBadge` so "who am I
+ paying for" is answered at the moment of payment. `CheckoutSummaryDto` serves neither field — file the REQ
+ (§4) and serve them from the payment mock meanwhile.
+- **Kill the race:** `disabled={busy}` on the BNPL branch button too — both CTAs freeze during initiate.
+- **Back affordance.** The payable state gets an explicit «بازگشت به درخواست» text link — a user under a
+ ticking countdown needs an in-page path back to the request, beyond phase 2's header chrome.
+- **Trust line at the CTA:** lock icon + «پرداخت امن از طریق درگاه بانکی» caption under the pay bar so the
+ gateway redirect is expected, not alarming. Real gateway/Shaparak logos are (DEFERRED — until licensed
+ assets exist; never fake a bank's mark).
+- **Un-bake the arrows.** `payment.cta_pay` embeds «←»/«→» in the translated string (fa/en.json:577) — move
+ the arrow to an `endIcon` slot (register a direction-aware chevron in `AppIcon/config.ts` if phase 0/1 didn't).
+
+### 3.2 Confirmation as a receipt
+
+- Rebuild `…/checkout/confirmation/page.tsx` as a screenshot-worthy receipt card (Iranian users screenshot
+ receipts — design for that): **کد پیگیری** in a copyable `dir="ltr"` row with a copy button, Shamsi payment
+ date-time, method («کارت بانکی» / «اقساطی — {provider}»), booking reference, the paid total as a ``
+ hero, and the escrow reassurance via the `EscrowNotice` component — never rewritten copy.
+- **No silent vanish:** the summary fetch gets real loading (skeleton) and error (retry) states. Add a
+ compact "what happens next" 2-step strip (اطلاعرسانی به پرستار ← ویزیت و ثبت ورود) via the phase-1
+ StatusTimeline idiom, extending trust past the payment.
+- The wire serves none of کد پیگیری/paidAt/method today — file the REQ (§4), render from the mock now, and
+ degrade gracefully on the real path (hide the row rather than show a fake code). Web-Share/save-image
+ actions are (DEFERRED — the copyable code is the contract).
+
+### 3.3 Wallet as the money hub
+
+- Restructure `…/wallet/` into segmented sections (MUI Tabs or a segmented control — one pattern, both
+ locales): **پرداختها** (payment history: every card/BNPL transaction with amount, Shamsi date,
+ `PaymentStatusBadge`, deep-link to the booking — needs the customer payment-transactions list REQ, §4;
+ build mock-tolerant behind `services/payment`), **اقساط** (the existing `WalletInstallments` content,
+ semantics unchanged — provider-reported, never a Balinyaar ledger), **استردادها** (reuse `RefundStatusCard`
+ entries; `services/refunds` has by-booking/status reads but no "all my refunds" list — REQ if confirmed
+ missing, §4), **رسیدها** (invoice deep-links derived client-side from succeeded payments — no endpoint).
+ Each section keeps four-state discipline with money-hub-specific empty copy («هنوز پرداختی نداشتهاید» —
+ not the installments placeholder).
+- **Normalize money-surface width:** drop the wallet's local `maxWidth: 560`; all money surfaces (checkout,
+ confirmation, wallet, invoice) read at one width system — the shell's `CONTENT_MAX_WIDTH`. If a narrower
+ measure is deliberately wanted, make it a named constant in `components/config.ts` applied to **all four**.
+
+### 3.4 BNPL honesty + polish
+
+- **Plan cards decide with Toman, not percent.** Each `BnplPlanCard` shows: پیشپرداخت (امروز) in Toman,
+ قسط ماهانه, and **مجموع بازپرداخت** with the fee delta vs interest-free explicit («+۴۵۰٬۰۰۰ تومان کارمزد»
+ in terracotta / «بدون کارمزد» neutral). Replace the `LinearProgress` down-payment bar with a plain labelled
+ amount row — a static fact must not look like loading. All figures are **served** by the plan DTO; if one
+ isn't, REQ it — never compute percent × total client-side.
+- **PlanStep's swapping total explained.** The «مبلغ کل» header names the plan it reflects
+ («مبلغ کل با طرح {plan}») and shows the fee delta when it changes — no silently morphing number.
+- **Provider logos strategy (decide and document):** build a small `BnplProviderLogo` registry component —
+ `providerCode` → bundled SVG when an asset exists, falling back to a *designed* neutral chip (full provider
+ name + tinted monogram, not the current two-letter glyph). Asset-tolerant: real logos drop in without
+ touching call-sites.
+- **Eligibility gets feedback.** During the credit check the button swaps to «در حال استعلام اعتبار…» with a
+ spinner (mirror C6's `state_initiating` pattern); the prefilled mobile field becomes `readOnly`
+ presentation (normal contrast), not `disabled`.
+- **Fix the mislabelled CTA** on `bnpl/return`'s invalid-link state: it navigates to the bookings list, so
+ label it «رزروهای من» (reuse C6's `bd_my_bookings` invalid-link pattern) — or route to an actual card
+ checkout if a `request_id` is recoverable. Label and destination must agree.
+- **Guard the dev harness.** `…/checkout/bnpl/gateway/page.tsx` is a test harness reachable in production —
+ env-gate it with `notFound()` outside development, mirroring the card-gateway harness refinement phase 4
+ deleted. Also remove the dead `CHECKOUT_GATEWAY` constant in `src/constants/routes.ts:24` (verify unreferenced).
+
+### 3.5 Invoice: fiscal grade
+
+- Extend `…/bookings/[id]/invoice/page.tsx` toward a document a family can file: buyer name, service
+ description + visit date(s) (composable client-side from the booking detail read — a UI join, not money
+ math), booking + transaction references, payment method, and a seller fiscal-identity block (legal name,
+ economic code, address). `InvoiceDto` serves none of the fiscal fields — REQ (§4); render what the client
+ can compose now, reserve labelled slots for the rest.
+- Add an A4 print stylesheet: `@page` margins, a document footer (invoice number + issue date + مودیان
+ reference when present), type sized for paper. **Keep** the existing print mechanics untouched.
+- Delete the stale comment at `invoice/page.tsx:148-149` (it claims fa `common.brand` reads «بلینیار»; the
+ catalog actually has «بالین یار»). The brand-spelling unification itself is (DEFERRED →
+ [phase 12](ui-phase-12-copy-motion-and-polish.md), which owns the catalog sweep).
+
+### 3.6 Designed wait states + one terminal-state component
+
+- Replace `checkout/return`'s pending spinner+chip+title triple with a staged 2-node progress:
+ «بازگشت از درگاه ✓» → «در انتظار تایید بانک» (active, calm animated indicator) + expected-duration copy
+ («معمولاً کمتر از یک دقیقه طول میکشد»). Keep the manual «بررسی دوباره» escape hatch and the bounded poll.
+- Extract the four copy-pasted terminal cards (`MessageCard` in `checkout/page.tsx:255-286` +
+ `bnpl/page.tsx`, `StateCard` in `checkout/return/page.tsx:151+` + `bnpl/return/page.tsx`) into **one**
+ shared `src/components/PaymentStateCard/` (co-located test, barrel export) covering
+ icon/tone/title/body/actions — the card and BNPL flows can no longer drift.
+
+### 3.7 Money-display sweep
+
+- Adopt the phase-1 `` primitive on every money row in this area — `PriceBreakdown` rows **and**
+ total, confirmation, `MethodStep`, `PlanStep`, `EligibilityStep`, `WalletInstallments`, `BnplPlanCard`,
+ `InstallmentScheduleRow` — so **every** amount carries «تومان» and identical typography. Bare grouped
+ numbers on a Rial-wire/Toman-display product are the exact ambiguity users second-guess. If `` lacks
+ a needed variant (e.g. strike-through for fee comparison), extend the phase-1 component minimally + its
+ test, and note it in your report.
+- An expandable «چطور کار میکند؟» escrow explainer **around** `EscrowNotice` — a 3-step visual
+ (پرداخت ← امانت نزد بالینیار ← آزادسازی پس از تایید پایان ویزیت) plus the cancellation/refund implication,
+ available from checkout and confirmation. The mandated `EscrowNotice` sentence is **untouchable** — the
+ explainer wraps it, never edits it. Ground the steps in
+ [escrow-ledger.md](../../../product/payments/escrow-ledger.md); never promise timing the ledger doesn't guarantee.
+
+## 4. Mocks & seams in this phase
+
+**No new mocks or seams.** Everything stays behind the existing `services/payment`, `services/bnpl`, and
+`services/refunds` seams (`USE_PAYMENT_MOCK` / `USE_BNPL_MOCK` unchanged); the UI is mock-tolerant — real-path
+gaps degrade gracefully, never faked data.
+
+Backend gaps become REQ entries appended to
+[../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md).
+REQ-001…038 are taken and parallel UI phases are also filing — **check the tracker at execution time and take
+the next free numbers.** File (verify each gap against the current types first):
+
+1. **Checkout/receipt enrichment** — `nurseAvatarUrl` + `nurseVerified` on `CheckoutSummaryDto` (§3.1);
+ tracking/reference code, `paidAt`, and payment method on the payment outcome or a confirm-read for the
+ §3.2 receipt (`PaymentOutcomeDto` today: `types.ts:104-110`; extends REQ-016/017).
+2. **Customer payment-transactions list** — card+BNPL transactions (amount, status, date, bookingId) for the
+ wallet «پرداختها» section (§3.3); receipts derive from it client-side.
+3. **Customer refunds list** — an "all my refunds" read for «استردادها» (§3.3) if `services/refunds` truly
+ has only by-booking/status reads (extends REQ-021).
+4. **Invoice fiscal fields** — buyer name, payment method + transaction reference, seller fiscal identity
+ (legal name, economic code, address) on `InvoiceDto` (§3.5; extends REQ-018).
+
+## 5. Critical rules you must not get wrong
+
+- **`EscrowNotice` copy is product-mandated — never edit it.** Wrap and explain around it; verbatim, one
+ shared component.
+- **Money values are served IRR digit-strings, formatted via the BigInt util. The UI never computes a
+ figure** — no percent × total, no client-side VAT, no float anywhere on the money path. `PriceBreakdown`'s
+ dev reconciliation guard stays.
+- **Idempotency-key-per-attempt and benign-409 convergence stay exactly as implemented** (`page.tsx:54-57`,
+ `:137-143`) — a 409 on initiate routes to the outcome read, never a toast.
+- **`CountdownTimer` semantics:** server-frozen deadline, LTR-forced tabular clock — restyle, never recompute.
+- **BNPL honesty architecture stays:** D5 installments are provider-reported (never a Balinyaar ledger),
+ ownership notes at point-of-choice and contract step, consent checkboxes gate credit check and contract,
+ every decline path offers the card fallback. `RefundEtaBanner`'s honest 7–10-day BNPL window is untouched.
+- **Terracotta is the single money accent** — pay CTA, BNPL selection, outstanding balance, breakdown total.
+ Never as body-text color; teal/neutral carries everything else. **Invoice print mechanics + dark→light
+ print flip are do-not-regress** (`invoice/page.tsx:98-113,128-136`).
+- Design-contract non-negotiables that bite here: every string in **both** `en.json`/`fa.json`; tokens, never
+ hexes; logical/RTL-safe props only (`dir="ltr"` islands for codes and clocks); both color schemes verified;
+ MUI v9 API only; co-located tests for shared components; `clientFetch`/cookie rules untouched; **frontend
+ lane only** — never edit `server/`.
+
+## 6. Definition of Done
+
+On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):
+
+- [ ] `npm run check` green; `npm run test:ci` green for every touched shared component (`PriceBreakdown`,
+ `BnplPlanCard`, `InstallmentScheduleRow`, `PaymentStateCard`, `BnplProviderLogo`); `en.json`/`fa.json` in sync.
+- [ ] C6 answers "who + how much" without scrolling at 375px: identity block with avatar + TrustBadge,
+ prominent total, safe-area-aware sticky pay bar; **both** CTAs disabled during initiate; back link.
+- [ ] Confirmation renders a receipt: copyable LTR کد پیگیری, Shamsi date-time, method, booking reference,
+ escrow line — with real loading/error states (mock shows all fields; real path hides unserved rows).
+- [ ] Wallet has the four sections with four-state coverage each; a card-paying user sees payment history
+ (mock) instead of an installments-only empty tab; money surfaces share one width system.
+- [ ] BNPL plan cards show down-payment/monthly/total-repayment in Toman with the fee delta explicit; no
+ `LinearProgress` as a static fact; PlanStep's total names its plan; eligibility shows in-progress
+ feedback; the `bnpl/return` invalid-link CTA label matches its destination.
+- [ ] `bnpl/gateway` returns 404 outside development; dead `CHECKOUT_GATEWAY` constant removed.
+- [ ] Every amount in the area carries «تومان» via ``; `PriceBreakdown` still reconciles (dev guard
+ fires on a deliberate mismatch). Invoice prints as an A4 document (footer, paper colors from dark mode).
+- [ ] REQs filed with the next free numbers; visual verification on all four axes (`/fa` + `/en` × light +
+ dark), mobile **and** desktop, for checkout, confirmation, wallet, BNPL wizard, invoice.
+
+## 7. How to test (what a human can verify after this phase)
+
+1. Login as a seeded customer with an accepted request → `/fa/bookings/checkout?request_id=…` at 375px:
+ total figure at top, sticky pay bar above the bottom nav with total + «پرداخت»; nurse avatar + TrustBadge
+ on the summary; tap pay → both CTAs disable, label swaps to «در حال شروع…».
+2. Complete the mock gateway round-trip → the return page shows the staged 2-node wait
+ («بازگشت از درگاه ✓ → در انتظار تایید بانک») with duration copy — no bare spinner+chip stack.
+3. Confirmation: کد پیگیری renders LTR with a working copy button; Shamsi date-time, method, booking
+ reference, escrow line present; reload with the network blocked → skeleton then error+retry, never a
+ silently missing amount.
+4. Open «کیفپول» → four sections; پرداختها lists the payment just made (mock) linking to the booking;
+ استردادها shows `RefundStatusCard` entries after cancelling a paid booking; رسیدها links to the invoice.
+5. Back on checkout, tap «پرداخت اقساطی» → provider rows show designed logos/chips (no two-letter glyphs);
+ plan cards show پیشپرداخت/قسط ماهانه/مجموع بازپرداخت in Toman with «+… تومان کارمزد» on fee plans, no
+ progress bar; selecting a plan names the header total; the eligibility check shows progress while pending.
+6. `/fa/bookings/checkout/bnpl/gateway` in a production build → 404; in dev it still works. Open a paid
+ booking's invoice → buyer/service/reference rows present (mock); print preview shows an A4 document with
+ footer; triggered from dark mode → paper colors.
+7. Repeat 1–5 on `/en` (LTR) and in dark mode: no clipped RTL/LTR islands, no unreadable tokens, «تومان»
+ (fa) / unit label (en) on every amount.
+
+## 8. Hand off & document (close the phase)
+
+- Update `client/CLAUDE.md` (Project Structure) for new components/route changes (`PaymentStateCard`,
+ `BnplProviderLogo`, wallet sections); note any minimal extension made to a phase-0/1 foundation file.
+- Write the report at `dev/shared-working-context/reports/ui-phase-6-report.md`: what shipped per §3 item,
+ the provider-logo and money-width decisions, four-axes screenshots, and the exact REQ numbers filed.
+- Save a memory note per operating-rules §8: checkout/receipt/wallet/BNPL end state, REQ numbers, and gotchas
+ (sticky-bar vs bottom-nav layering, print stylesheet interactions) the next phase should know.
diff --git a/dev/post-phase/ui/ui-phase-7-nurse-daily-ops.md b/dev/post-phase/ui/ui-phase-7-nurse-daily-ops.md
new file mode 100644
index 0000000..ebfa50c
--- /dev/null
+++ b/dev/post-phase/ui/ui-phase-7-nurse-daily-ops.md
@@ -0,0 +1,296 @@
+# UI Phase 7 — Nurse daily ops
+
+> **Mission:** give the nurse side a home and make the daily loop phone-first. Today the nurse's landing
+> page is literally a `PlaceholderScreen` (`nurse/page.tsx:7`), the day-of flow has **no address, no
+> contact, no navigation affordance** anywhere (`addressSnapshotJson` has zero render sites in
+> `client/src`), and the request inbox hides the decision-critical facts. Every data hook a real
+> dashboard needs already exists and is cached; this phase is mostly composition plus surgical fixes
+> and a handful of backend REQs.
+>
+> **Track:** frontend · **Depends on:** [Phases 0–2](ui-phase-2-shells-and-navigation.md) ·
+> **Unlocks:** the nurse side finally has a home; field work is phone-first; [Phase 8](ui-phase-8-nurse-business-and-verification.md) plugs its activation checklist into the dashboard slot this phase leaves.
+> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md) and invoke the frontend-designer skill — both are mandatory.**
+
+## 1. Context — where this sits
+
+Nurses are the supply side of a trust-first marketplace, and they live this product **on a phone,
+on shift, at a doorstep**. The functional layer under `client/src/app/[locale]/(private-routes)/nurse/`
+is competent — but composed like a settings area, not an operational tool. Diagnosed, code-verified:
+
+1. **The landing page is empty.** `nurse/page.tsx` renders `PlaceholderScreen` (line 7) while
+ `useNurseRequestInbox`, `useTodaySessions`, `useNurseEarningsBalance`, `useVerificationStatus`,
+ `useNurseTrustBadge`, and `useUnreadCount` all exist as cached queries — the dashboard is pure assembly.
+2. **The day-of flow is blind.** `BookingDetailView` renders only patient + nurse names;
+ `addressSnapshotJson` is never rendered anywhere — and per the b9 contract it is **`null` in the
+ nurse view by design** (`services/bookings/types.ts:132–133`; `bookings-evv.md`: "The nurse view
+ omits `addressSnapshotJson`"), so serving it needs a REQ (§3.3). The contact affordance needs
+ **no** REQ: `CareInstructionsDto` already carries `emergencyContactName`/`emergencyContactPhone`
+ in the gated post-confirmation read (`types.ts:209–217`).
+3. **The most important tap of a nurse's day is a tertiary button.** Both EVV CTAs in
+ `SessionCard.tsx` render `sx={{ m: 0, alignSelf: 'flex-start', py: 1 }}` (lines 117–141) —
+ visually equal to the "view booking" text link beside them.
+4. **The inbox hides the decision.** `InboxCard` (`requests/page.tsx:57–115`) shows patient, time,
+ gender chip, notes — no service, no price; `BookingRequestListItem` carries **no variant fields**
+ (the detail DTO has them — REQ-013 delivered). It is pending-only page-1-only (hook defaults, no
+ tabs, no pager, API pages at 20), and a failed query renders the *empty* state
+ (`requests/page.tsx:19` destructures only `{ data, isLoading }`) — paid work silently missed.
+5. **Earnings answers every question except the one nurses ask** ("when do I get paid, how much"),
+ and failed payouts print the bank rail's `failureReason` verbatim — LTR English bank codes in a
+ Persian UI (`earnings/payouts/[id]/page.tsx` failure box, `PayoutHistoryRow`).
+
+**What already exists (do not rebuild):**
+
+- The nurse route tree and all its pages: requests (+detail), visits (+detail with
+ `BookingDetailView` + `BookingSupportEntry` + `NurseVisitNotesPanel`), earnings (+payout history
+ and detail), profile/services/coverage/bank/verification.
+- The data layer: `useNurseRequestInbox` (15s poll), `useTodaySessions`, `useSessionEvv`,
+ `useEvvController` (advisory GPS via the never-rejecting location seam), `useNurseEarningsBalance`,
+ `useNursePayoutHistory`/`useNursePayoutDetail`, `useVerificationStatus`, `useNurseTrustBadge`, `useUnreadCount`.
+- Shared components: `SessionCard`, `EvvStatusBanner`, `EarningsBalanceHeader` (with test),
+ `TrustBadge`, `StatusChip`, plus [Phase 1](ui-phase-1-primitives-and-states.md)'s `CountdownTimer`
+ v2, EmptyState/ErrorState kit, `PageHeader`, ``, and skeleton twins.
+- The nurse shell chrome — grouped sidebar, bottom nav, identity card — is
+ [Phase 2](ui-phase-2-shells-and-navigation.md)'s property (`client/src/layout/`). **Do not touch it.**
+
+## 2. Required reading (do this first)
+
+- [audit/nurse-trust-ops.md](audit/nurse-trust-ops.md) and
+ [audit/nurse-workspace.md](audit/nurse-workspace.md) — the full evidence + the keep-lists §5 folds in.
+- The design contract: `.claude/skills/frontend-designer/SKILL.md` (invoke the skill).
+- Code, in this order: the nurse route tree `client/src/app/[locale]/(private-routes)/nurse/`
+ (`page.tsx`, `requests/**`, `visits/**`, `earnings/**`); `client/src/components/booking/`
+ (`SessionCard`, `BookingDetailView`, `useEvvController`, `EvvStatusBanner`);
+ `client/src/services/bookingRequests/types.ts` (the disclosure semantics live in its header
+ comments), `client/src/services/bookings/types.ts`, `client/src/services/payouts/`;
+ `client/src/constants/routes.ts` (NURSE_* routes).
+- Contracts: [booking-requests.md](../../contracts/domains/booking-requests.md),
+ [bookings-evv.md](../../contracts/domains/bookings-evv.md), [payouts.md](../../contracts/domains/payouts.md).
+- Product ground truth: [data-model/index.md](../../../product/data-model/index.md) (Principle 6 —
+ two-stage disclosure is a hard rule),
+ [06-evv-and-service-delivery.md](../../../product/business/06-evv-and-service-delivery.md)
+ (address-match is advisory, never a block),
+ [05-booking-and-scheduling.md](../../../product/business/05-booking-and-scheduling.md),
+ [10-payouts.md](../../../product/business/10-payouts.md), and
+ [12-messaging-and-emergencies.md](../../../product/business/12-messaging-and-emergencies.md)
+ (**no nurse↔customer chat channel** — contact is tel: + coordination tickets).
+- The REQ tracker: [for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
+ — REQ-001…038 are taken; this phase files REQ-039 onward.
+
+## 3. Scope — build this
+
+### 3.1 The «امروز» dashboard (`nurse/page.tsx`)
+
+Replace the `PlaceholderScreen` with the operational home. Pure assembly — every widget reads an
+already-cached query (verify each hook's exact return shape before wiring):
+
+- **Greeting header:** nurse name (from the cached profile/`me` query — verify the field) +
+ `TrustBadge` (via `useNurseTrustBadge`/`useVerificationStatus` — one cached status query, not two).
+- **Next-visit card:** first actionable session from `useTodaySessions` — patient name, time range,
+ a "time until" line (display-only relative time, not a deadline computation), and a check-in
+ shortcut deep-linking to `/nurse/visits`. Empty → a calm «امروز ویزیتی ندارید», not a warning.
+- **«منتظر پاسخ شما» strip:** pending requests from `useNurseRequestInbox` — count, the most urgent
+ request's countdown (Phase 1 `CountdownTimer` with urgency tiers), and an inline open into
+ `/nurse/requests/{id}`. The most time-critical widget; it sorts above earnings.
+- **Earnings snapshot:** compact stat row from `useNurseEarningsBalance` — reuse
+ `EarningsBalanceHeader` compact or compose a two-stat row with the Phase 1 `` primitive;
+ deep-link to `/nurse/earnings`. Signed values render signed (never clamp a negative).
+- **Activation/verification banner slot:** a clearly named composition point (e.g.
+ `DashboardActivationSlot`) filled for now with only the existing verification-status banner when
+ not yet approved. The activation checklist itself is
+ **(DEFERRED → [Phase 8](ui-phase-8-nurse-business-and-verification.md))**, which owns the slot's
+ content — document the slot in your report so Phase 8 finds it.
+- **Notifications entry:** unread count via `useUnreadCount` linking to `ROUTES.NURSE_NOTIFICATIONS`
+ (the bell in the shell chrome is Phase 2's; this is just a dashboard row).
+
+The page is currently a server component; keep `page.tsx` a thin composition rendering client
+widgets. Four-state pattern on every widget: skeleton → error-with-retry (Phase 1 kit) → empty → data.
+
+### 3.2 Visits day surface (`visits/page.tsx` + `SessionCard`)
+
+- **Date anchor header:** replace the static title with «امروز، ۲۴ تیر»-style Shamsi date (via
+ `formatShamsiDate`, Phase 1 `PageHeader`), so the page reads as *today*, not a generic list.
+- **Service name on session cards:** rows show patient name + session index only —
+ `BookingSessionListItemDto` has no service/variant field (verified, `types.ts:173–183`). File
+ **REQ-041** (§4); meanwhile render it when present (mock-tolerant optional field) — never fetch
+ per-row booking details to fake it (N+1).
+- **Freshness:** add a modest `refetchInterval` to `useTodaySessions` (e.g. 60s — same-day schedule
+ changes currently never appear without re-navigation; the hook sets only `staleTime`). Keep the
+ EVV-mutation invalidation untouched.
+- **EVV CTA as the hero:** in `SessionCard`, when `showEvvControls` is on, render check-in/check-out
+ as the **full-width, thumb-reach primary action** — ≥48px touch target, full row width, the
+ existing busy states, and a lightweight confirm on check-*out* (it ends the visit and starts the
+ payout clock). `SessionCard` is shared both-roles — gate every change on `showEvvControls` so the
+ customer's booking detail is untouched, and update its co-located test.
+
+### 3.3 Visit detail workspace (`visits/[id]/page.tsx` + `BookingDetailView`)
+
+- **Address card:** render `addressSnapshotJson` (title, address line, city/district) with a map
+ deep-link built client-side from the snapshot's lat/lng — `geo:{lat},{lng}` URI with a web
+ Neshan/Balad fallback; link only, no SDK, no API key. The nurse view is masked server-side
+ (contract-level), so file **REQ-040** (§4) and build the card mock-tolerant: render when non-null,
+ otherwise a quiet "address available after confirmation" note. Never source an address from the
+ request-stage (b8) data.
+- **Contact affordance:** a `tel:` action from the care-instructions read —
+ `emergencyContactName`/`emergencyContactPhone` are already in `CareInstructionsDto`, gated to the
+ assigned nurse post-confirmation. `tel:` only (no VoIP, per product); `BookingSupportEntry` remains
+ the coordination path. Register `call`/`navigation` icons in `AppIcon/config.ts` — the registry
+ has `location`/`gps` but no phone or directions glyph.
+- **In-visit mode:** when the viewer is the nurse and a session is checked in, a state header —
+ «در حال ویزیت» + elapsed time (from server `checkInAt`, never a client clock) + the check-out CTA
+ promoted to the top. Compose from the existing `EvvStatusBanner`/`formatElapsed`.
+- **Notes placement polish:** keep `NurseVisitNotesPanel` below the EVV surface (it already is) but
+ give the page one visual rhythm — the detail currently reads as three unrelated stacks.
+
+### 3.4 Request inbox redesign (`requests/page.tsx` + `requests/[id]/page.tsx`)
+
+- **Decision-first cards:** service name + price as the headline; patient, time, gender chip as
+ secondary facts. The list DTO has neither field → **REQ-039** (§4); render them when present,
+ degrade to today's layout when absent (mock-tolerant). Price via `` (Toman display).
+- **Urgency-tinted countdown pill** using Phase 1's `CountdownTimer` tiers — teal >2h → amber <2h →
+ terracotta <30min, `aria-live="polite"`, with a label (the inbox currently renders bare unlabeled
+ digits). If the Phase 1 component lacks the tier API, extend it minimally there (never fork a
+ local variant) and note it in your report.
+- **Tabs + pagination:** «در انتظار» / «پاسخداده» / «منقضی» plus a pager (`useNurseRequestInbox`
+ already accepts `status` and `page`; the API pages at 20 — a 21st pending request is unreachable
+ today). The API filters by a *single* status: map «در انتظار» → `pending_nurse_response`, «منقضی»
+ → `expired_no_response`; for «پاسخداده» either merge the accepted/converted/rejected single-status
+ queries (page-1, documented limitation) or wait on REQ-039's status-group filter — pick one and
+ say so in the report.
+- **Accept confirmation dialog:** accept currently mutates on a single tap sitting flex:1 beside
+ reject (`requests/[id]/page.tsx:198–219`). Add a confirm dialog with a consequence summary —
+ «با پذیرش، خانواده برای پرداخت دعوت میشود؛ پس از پرداخت، رزرو قطعی میشود.» Do **not** hard-code
+ the payment-window duration into copy (config-owned policy number; after acceptance render the
+ `paymentDeadlineAt` countdown from the server instant instead).
+- **Error state:** a failed inbox query must render the Phase 1 ErrorState with retry — today it
+ renders "no incoming requests" (income-critical false negative). Same fix on the day surface
+ (`visits/page.tsx:20` destructures only `{ data, isLoading }` too).
+
+### 3.5 Earnings clarity (`earnings/**`)
+
+- **«برداشت بعدی» forecast line** above the tabs: next batch date (holiday-shifted) + expected
+ eligible amount — **server-served only**, file **REQ-042** (§4); do NOT compute it client-side
+ (holiday shifting, eligibility, clawback netting are backend truth). Render only when served.
+- **Failure-reason mapping:** map known bank-rail `failureReason` codes to Persian labels (i18n
+ keys) in the payout detail and `PayoutHistoryRow`; unknown codes get a generic Persian message
+ with the raw code as a secondary `dir="ltr"` caption — never the raw vendor string as the headline.
+- **ExplainerCard a11y:** real button semantics on the collapse header (today a `cursor: pointer`
+ Stack — no role, no keyboard path), `aria-expanded`, and the registered `expand` chevron instead
+ of the `visibilityon`/`visibilityoff` eye icons.
+- **Width normalization:** nurse pages ship three shapes — 620/640 hugging the start edge, unbounded
+ (earnings), 640+`mx:'auto'` (`BookingDetailView`). Adopt one **page-level** convention (a single
+ width constant + `mx: 'auto'`; the dashboard may go wider) across the pages this phase touches.
+ The shell gutter (`layout/`) is Phase 2's — do not edit it.
+
+### 3.6 Web-push for new requests (DEFERRED)
+
+The 2h response window vs a 15s poll that only works while the tab is open is a real tension, but
+push infrastructure (service worker + backend push rail) is out of scope. File it as **REQ-043**
+marked deferred/non-blocking (§4) so the need is on record; build nothing for it.
+
+## 4. Mocks & seams in this phase
+
+**No new mocks or seams.** Bookings and booking-requests already run **real**
+(`USE_BOOKINGS_MOCK = false`, `USE_BOOKING_REQUESTS_MOCK = false`); the nurse payouts read is still
+mock-primary (`USE_PAYOUTS_MOCK = true`). All new UI must therefore tolerate both worlds: optional
+fields render when present, degrade quietly when absent. If you extend a mock (payouts forecast,
+today-feed service label) to exercise the UI, keep it behind the existing `services/{domain}` seam
+and record it in [mocks-registry.md](../../shared-working-context/reports/mocks-registry.md).
+
+Backend gaps become REQ entries appended to
+[for-backend.md](../../shared-working-context/frontend/requests/for-backend.md) (REQ-001…038 taken):
+
+- **REQ-039 — Nurse inbox decision data:** `variantLabel` + `variantPrice` (+ unit) on the nurse
+ `booking_requests/list` row; optionally a status-group filter (`answered`) for the inbox tabs.
+- **REQ-040 — Nurse-view address on confirmed+ bookings:** serve `addressSnapshotJson` (or a
+ nurse-shaped subset incl. lat/lng) to the *assigned nurse* once status ∈ confirmed/in_progress —
+ a deliberate b9 contract change; include the open product question of whether `recipientPhone`
+ joins the post-confirmation nurse view.
+- **REQ-041 — Service label on the today feed:** variant display name on `booking_sessions/today` rows.
+- **REQ-042 — Payout forecast:** server-computed «برداشت بعدی» (next batch date, holiday-shifted +
+ expected eligible amount) on the nurse earnings read.
+- **REQ-043 — Web-push for new requests** (deferred, non-blocking — see §3.6).
+
+## 5. Critical rules you must not get wrong
+
+1. **EVV is advisory, never a block.** GPS denial/timeout/out-of-range never disables check-in/out;
+ mismatch renders warning-toned, never error (`EvvStatusBanner`); the location seam never rejects.
+ Making the CTA bigger must not make it stricter.
+2. **Two-stage disclosure stays pre-acceptance.** Inbox and request detail show only `customerNotes`
+ + coarse city·district. The address card (§3.3) exists **only** on the confirmed booking from the
+ b9 read; `useCareInstructions` stays gated (assigned nurse, confirmed+ — never fired for
+ customers). REQ-039's price enrichment is fine (money isn't clinical); address/contact enrichment
+ of the *request* stage is not.
+3. **Money is display-only.** Payout math, eligibility, forecast, clawbacks — server truth. Signed
+ net balance renders signed (the "owed back" negative state must survive the compact dashboard
+ snapshot). BNPL commission never appears on nurse surfaces.
+4. **CountdownTimer's server-frozen contract stays:** a server instant rendered against `Date.now()`,
+ its own isolated 1s tick, never a recomputed deadline; digits locale-aware inside a `dir="ltr"` island.
+5. **Locale digits + Shamsi everywhere** — `Intl` fa-IR digits, `formatShamsiDate`, Toman at the
+ display boundary via the shared money utils; `dir="ltr"` islands for clocks/IBANs/phone numbers.
+6. **Ownership boundaries:** `layout/` is Phase 2's; shared primitives are Phase 1's — extend
+ minimally there if a gap bites, never fork locally. `SessionCard`/`BookingDetailView` changes must
+ keep the customer view pixel-compatible (gate on `showEvvControls`/`viewerRole`).
+7. **Design contract non-negotiables:** every string in both message catalogs; tokens/palette keys,
+ never hexes; logical properties only (RTL); dark mode via tokens; MUI v9 API; the icon registry
+ for new icons; co-located tests for touched shared components; fetch/cookies rules untouched.
+8. **Do not regress the audit keep-lists** (both audits, "Keep" sections): the four-state pattern
+ where it exists, dashed-border empty states, per-session busy isolation, `EarningsBalanceHeader`
+ money honesty, non-accusatory failure copy, the 15s inbox poll + `onElapsed` refetch.
+
+## 6. Definition of Done
+
+On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):
+
+- [ ] `npm run check` green; `npm run test:ci` green for every touched shared component
+ (`SessionCard`, `BookingDetailView`, `CountdownTimer` if extended, new shared widgets).
+- [ ] `en.json`/`fa.json` in sync — no orphan keys either way.
+- [ ] `/nurse` renders the real dashboard (greeting + TrustBadge, next visit, requests strip with
+ countdown, earnings snapshot, notifications entry, Phase 8 activation slot), each widget with
+ skeleton/error/empty/data states.
+- [ ] Visits page: Shamsi date anchor, interval refresh, full-width EVV CTA with busy/confirm
+ states; customer booking detail unchanged.
+- [ ] Nurse booking detail: address card + map deep-link when the API serves the snapshot
+ (mock-verified), `tel:` contact from care instructions, in-visit header with elapsed time.
+- [ ] Inbox: cards lead with service + price when served; tabs + pager work; a failed query shows
+ an error state with retry (not the empty state); accept requires a confirm dialog.
+- [ ] Payout failure reasons render as Persian labels (raw code demoted to a secondary LTR line);
+ ExplainerCard is keyboard-operable with `aria-expanded`.
+- [ ] REQ-039…043 appended to the tracker with the exact DTO/route shapes proposed.
+- [ ] Visual verification on all four axes — `/fa` + `/en` × light + dark — and mobile + desktop for
+ dashboard, visits, inbox, and visit detail (`/fa` mobile first: this is the phone-first phase).
+
+## 7. How to test (what a human can verify after this phase)
+
+1. Log in as the seeded verified nurse (refinement phase 1 demo accounts) → `/nurse` shows a real
+ dashboard: greeting + trust badge, next visit, pending-requests strip counting down, earnings
+ snapshot in Toman.
+2. As a customer, create a request targeting that nurse → within the poll interval the dashboard
+ strip and `/nurse/requests` show it; the card leads with service + price (REQ pending: gracefully
+ headline-less); the countdown pill escalates teal → amber → terracotta (adjust a mock deadline).
+3. Open the request → «پذیرش» → a confirm dialog summarizes the consequence; confirm → status flips;
+ a stale second accept still 409s into the refetch path.
+4. Force the inbox query to fail (stop the API) → an error panel with retry — **not** «درخواستی ندارید».
+5. On `/nurse/visits` (mobile, `/fa`): Shamsi «امروز …» header; check-in is a full-width primary CTA.
+ Deny browser GPS → check-in still succeeds with the advisory warning banner. Check out → confirm
+ prompt → elapsed duration renders from server timestamps.
+6. Open a confirmed visit's detail with `USE_BOOKINGS_MOCK = true` (mock serves an address snapshot)
+ → address card + map link opening `geo:`/Neshan; `tel:` dials the care-instructions emergency
+ contact. On the real (masked) path → the quiet "available after confirmation" note, no crash.
+7. On `/nurse/earnings`: the forecast line appears only when the (mock) API serves it; a failed
+ payout shows a Persian failure label with the raw code as a small LTR caption; the explainer
+ header opens with Enter.
+8. Repeat 1, 2, and 5 on `/en` and dark mode — no stock-MUI colors, no Latin digits in fa timers.
+
+## 8. Hand off & document (close the phase)
+
+- Update `client/CLAUDE.md` "Project Structure" if you added dashboard widget components or new
+ shared components (the nurse route tree itself doesn't change shape).
+- Write the report at
+ [ui-phase-7-report.md](../../shared-working-context/reports/ui-phase-7-report.md): what shipped
+ per §3, the exact name/location of the Phase 8 activation slot, the inbox-tab strategy picked
+ (§3.4), any minimal extensions to Phase 0/1 foundation files, and four-axes verification notes.
+- List REQ-039…043 as filed (one-line status each) in the report and confirm they're appended to
+ [for-backend.md](../../shared-working-context/frontend/requests/for-backend.md).
+- Save a memory note per operating-rules §8: the nurse daily loop is now dashboard → visit →
+ check-in/out → earnings; address/contact are REQ-gated (REQ-040) with mock-tolerant UI; EVV
+ advisory and two-stage disclosure invariants unchanged.
diff --git a/dev/post-phase/ui/ui-phase-8-nurse-business-and-verification.md b/dev/post-phase/ui/ui-phase-8-nurse-business-and-verification.md
new file mode 100644
index 0000000..6adc762
--- /dev/null
+++ b/dev/post-phase/ui/ui-phase-8-nurse-business-and-verification.md
@@ -0,0 +1,293 @@
+# UI Phase 8 — Nurse business & verification
+
+> **Mission:** the nurse's business tools work, but the go-live journey is scattered across four
+> disconnected nags, the verification flow runs two competing progress metaphors, and the publish CTA
+> fires a success snackbar while publishing **nothing**. This phase unifies the setup journey into one
+> activation checklist, rebuilds verification as a single vertical trust journey with the TrustBadge
+> payoff visible, and fixes the real defects along the way (rejected-upload feedback, dead-end bank
+> form, non-hydrating credentials, whole-city double-encoding). Verification is the product's core
+> ritual; after this phase it should feel like it.
+>
+> **Track:** frontend · **Depends on:** [Phases 0–2](ui-phase-2-shells-and-navigation.md);
+> [Phase 4](ui-phase-4-customer-storefront.md) (reuses its verification-explainer / trust-dossier
+> components) · **Unlocks:** the supply side can set up, get verified, and go live with confidence
+> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md)
+> and invoke the frontend-designer skill — both are mandatory.**
+
+## 1. Context — where this sits
+
+Balinyaar is a trust-first marketplace: a nurse's verification badge is what a family buys. The nurse
+workspace (`client/src/app/[locale]/(private-routes)/nurse/`) is functionally complete — profile,
+services/variant builder, coverage, bank, and the B3–B6 verification flow all exist and are
+token-disciplined — but the composition undersells the product. Diagnosed root causes (all verified):
+
+1. **The publish CTA fakes success.** `nurse/services/PublishGate.tsx:64` —
+ `onClick={() => enqueueSnackbar(t('publish_done'), { variant: 'success' })}`. Nothing is published.
+ The server computes searchability itself: `is_searchable = isVerified && isAcceptingBookings &&
+ status != Suspended && variant.IsActive`, one index row per variant × coverage area
+ (`SearchIndexMaintainer.cs:248` `NurseBookable`) — so ≥1 coverage area is structurally required
+ too. **And `IsAcceptingBookings` defaults to `false`** (`NurseProfileConfig.cs:19`) with a real,
+ *unwired* toggle endpoint (`NurseProfilesController.SetAcceptingBookings`) that reindexes in the
+ same transaction. A fully verified nurse can still be invisible, and no UI surfaces why.
+2. **Four disconnected go-live nags for one journey:** the blocked-until-verified banner (profile),
+ PublishGate (services), the empty-coverage state, and the bank empty state each warn in isolation.
+3. **Two competing progress metaphors:** the B3 hub counts 7 checklist steps («X از Y» +
+ `LinearProgress`, inflated by the synthetic mobile step — `verificationSteps.ts:17` `MOBILE_STEP`)
+ while B4/B5/B6 show an unrelated bare 3-step `StepperHeader` (`verification/review/page.tsx:28`).
+4. **Rejected-upload recovery shows no feedback:** in `components/DocumentUpload/DocumentUpload.tsx`
+ the `{rejected ? (` branch (line 155) wins over `: state === 'uploading' ?` (line 193) — a
+ re-upload stays frozen on the red rejected card, and the re-upload button stays enabled mid-flight.
+5. **The credentials form doesn't survive re-entry:** `verification/credentials/page.tsx` initialises
+ every field to `''`/`[]` (lines 37–44); submit is `disabled={... || !anyUploaded}` (line 255) where
+ `anyUploaded` reads only this-session local state (line 106); license dates are native Gregorian
+ `type="date"` inputs (lines 221–236) on a trust-critical Persian form.
+6. **Bank is a dead end once verified:** `nurse/bank/page.tsx:31` —
+ `showFormNow = !isLoading && (accounts.length === 0 || showForm)`; `setShowForm(true)` exists only
+ in the mismatch branch (line 81). A nurse who switches banks cannot add an account.
+7. **Whole-city is encoded twice:** the coverage scope toggle (`coverage/page.tsx:198–210`) vs
+ `CascadingRegionSelect.tsx:152`'s own `{t('whole_city')} ` — picking
+ the latter under "specific districts" trips the district-required error (`page.tsx:89`).
+8. **Qualifications aren't editable:** `nurse/profile/page.tsx:58–60` silently round-trips
+ `educationLevel`/`educationField`/`specializationsJson` — yet the server's
+ `UpsertNurseProfileCommand` accepts and persists all three (verified). Pure frontend gap, no REQ.
+
+**What already exists (do not rebuild):**
+
+- The `services/verification` seam — ONE cached `useVerificationStatus()` query feeding B3+B6, the
+ data-driven step catalog (`verificationSteps.ts`), the dev-only mock admin sim
+ (`verification/page.tsx:87`). Restructure the *presentation*, keep the architecture.
+- `DocumentUpload`'s idle→uploading(%)→success/error state machine + rejected variant — fix the
+ branch precedence, don't rewrite the machine. `BankStatusPanel`'s three states with masked
+ `dir="ltr"` IBAN; the pending ownership poll in `services/nurse`.
+- Tested shared components: `VariantCard`, `PriceDisplay` (BigInt-safe, Toman-at-the-boundary),
+ `CategoryTile`, `CascadingRegionSelect`, `TrustBadge`, `StatusChip`.
+- Phase 0/1 foundations (themed `StepperHeader`, Jalali date picker, EmptyState/ErrorState kit,
+ PageHeader, card kit) and phase 4's verification-explainer / trust-dossier components — consume,
+ never fork.
+- Real backends: profiles, catalog, serviceAreas, nurse-bank run `USE_*_MOCK = false`; verification
+ remains mock-primary (`USE_VERIFICATION_MOCK = true`) — leave the flags as they are.
+
+## 2. Required reading (do this first)
+
+- [audit/nurse-workspace.md](audit/nurse-workspace.md) and
+ [audit/nurse-trust-ops.md](audit/nurse-trust-ops.md) — the full evidence + keep-lists.
+- [.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md) —
+ the design contract (invoke the skill, don't just read it).
+- `product/business/02-nurse-verification.md` (automated vs manual checks, honesty constraints);
+ `product/business/03-service-catalog-and-pricing.md` + `04-search-and-matching.md` (searchability).
+- Code: the nurse route tree (`profile`, `services`, `coverage`, `bank`, `verification/**`),
+ `components/DocumentUpload/`, `components/geography/CascadingRegionSelect.tsx`,
+ `services/verification/types.ts` (nurse-facing `VerificationStatus` has **no** `submittedAt`;
+ `NurseCredential` **never** carries `credentialNumber`), `services/profiles/types.ts`
+ (`NurseProfileDto.isAcceptingBookings` is read-only today; `UpsertNurseProfileInput` lacks it).
+- Phase 7's report (`../../shared-working-context/reports/ui-phase-7-report.md`) — where the nurse
+ dashboard left its setup-status slot — and the REQ tracker
+ [for-backend.md](../../shared-working-context/frontend/requests/for-backend.md) (REQ-001…038 taken;
+ confirm the high-water mark before filing).
+
+## 3. Scope — build this
+
+### 3.1 Activation checklist («راهاندازی») + an honest, *real* go-live gate
+
+- Build `src/components/ActivationChecklist/` (shared, tested): one tracker composing the five
+ scattered states from already-cached queries — تأیید هویت و مدارک ✓ (`useVerificationStatus`),
+ تکمیل نمایه ✓ (bio + avatar), حداقل یک خدمت فعال ✓ (`useMyVariants`), محدودهٔ پوشش ✓
+ (`useServiceAreas`), شبای تأییدشده ✓ (`useNurseBankAccounts`) — each row a StatusChip-style state +
+ a deep link to its fix. Distinguish the tiers honestly: the first four drive **search visibility**;
+ bank drives **getting paid** (not part of `is_searchable` — label it «برای دریافت درآمد»).
+- Mount it on the services page (above `MyServicesList`) and in the phase-7 dashboard's setup slot
+ (replace any placeholder card phase 7 left there — one shared component, not a fork). Collapse it to
+ a compact «فعال در جستجو» state once every row passes and accepting-bookings is on.
+- **Replace PublishGate's fake success with the real switch.** Wire the existing, unwired
+ `POST nurse_profiles/set_accepting_bookings` through the profiles seam (types + clientApi + mockApi +
+ a `useSetAcceptingBookings` hook that invalidates the profile query). The gate becomes state-driven:
+ unmet conditions → guidance («برای نمایش در جستجو: …» listing exactly the unmet real conditions);
+ met but `isAcceptingBookings === false` → «شروع پذیرش رزرو» calling the real endpoint; live → the on
+ state + «توقف موقت پذیرش». Success copy only after the mutation succeeds — never a no-op snackbar.
+
+### 3.2 Unified verification journey (one spine, one metaphor)
+
+- Rebuild the B3 hub (`verification/page.tsx` + `VerificationChecklist.tsx`) as ONE vertical journey:
+ grouped step cards — **هویت** (identity KYC + Shahkar + mobile), **مدارک حرفهای** (MoH license, INO
+ membership, criminal record), **بانک** (IBAN-owner match) — on a single spine, each group folding the
+ data-driven steps from `verificationSteps.ts` (keep the catalog + synthetic-mobile-step architecture
+ — do-not-regress; regroup presentation only).
+- Remove the competing 3-step `StepperHeader` from B4/B5/B6; those pages get a journey-context header
+ (group name + «بازگشت به مسیر تأیید»). One progress answer everywhere.
+- The payoff: a live TrustBadge preview panel on the hub — «این نشان را خانوادهها میبینند» — that
+ fills as groups pass, reusing phase 4's verification-explainer/trust-dossier components for the
+ framing (consume, don't duplicate).
+- B6 under-review: submitted timestamp (Shamsi) + a what-happens-next timeline (بررسی توسط کارشناس →
+ نتیجه در ۲۴–۴۸ ساعت → فعالسازی نشان) + the same journey header as siblings. `VerificationStatus`
+ carries no `submittedAt` today → file the REQ (§4); omit the line when absent, never fake a time.
+
+### 3.3 DocumentUpload fixes + capture guidance
+
+- **Fix the precedence bug:** `state === 'uploading'` (and the success flash) must win over the
+ `rejected` prop so a re-upload shows live progress. Keep the rejection reason visible *above* the
+ progress UI during re-upload, and disable the re-upload button while in flight. Update the
+ co-located test to cover rejected→re-upload→progress→success.
+- Capture guidance where cheap: a frame-overlay illustration for the ID-card/selfie local-capture mode
+ (B4) + static hints («نور کافی، بدون تاری، چهار گوشهٔ کارت داخل کادر»). Client-side too-dark/blurry
+ heuristics only if trivially cheap; no new dependencies.
+
+### 3.4 Credentials form (B5) — survives re-entry, Persian dates
+
+- Hydrate from server state: steps already `in_review`/`passed` render as submitted summaries (from
+ `useVerificationStatus`), not blank inputs; derive the submit gate from server + session state so a
+ returning nurse never sees a dead disabled button with no explanation. The raw INO/credential number
+ is **never returned by design** (encrypted server-side) — render a submitted state («شمارهٔ نظام ثبت
+ شد»), never re-prompt as if lost. If no nurse-facing read-back of the structured details
+ (authority/dates/specialties) exists, file the REQ (§4) and hydrate mock-tolerantly.
+- Replace both native `type="date"` fields with the phase-1 Jalali picker — license issue/expiry feed
+ the credential-expiry sweep; wrong dates are a correctness risk, not a style nit.
+- Specialty chips polish: selected/unselected states off tokens, custom-specialty entry kept.
+
+### 3.5 Services & variant builder
+
+- Step 3 live preview: render the real `VariantCard` as «اینگونه در جستجو دیده میشوید» composing the
+ entered name/category/price — the nurse is composing a listing; show the listing.
+- Replace the wrapped `ToggleButtonGroup` option values (`VariantBuilder.tsx:424`
+ `sx={{ flexWrap: 'wrap' }}` — grouped-button borders/corners break on wrap) with a chip group.
+- Fix the duplicate-warning contrast: `VariantBuilder.tsx:263` sets body text to `var(--bal-warning)`
+ (amber on paper fails light-mode contrast) — restyle as an accent-edge panel with `text.primary`
+ body, warning reserved for the edge/icon; add an "edit the existing listing" affordance on the 409
+ duplicate. The themed `StepperHeader` arrives free from phase 0/1 — just consume it.
+
+### 3.6 Coverage — one control owns whole-city (+ cheap map viz)
+
+- Collapse the double encoding: exactly one control owns the whole-city/districts choice. Preferred:
+ drop the separate scope toggle and let `CascadingRegionSelect`'s district level own it (its
+ «کل شهر» empty option *is* the choice — `districtId=null` = whole city both ways, matching the
+ serviceAreas contract); alternatively keep the toggle and add a prop suppressing the select's own
+ whole-city item. Either way the district-required error can no longer be triggered by a choice the
+ UI itself offered. Other `CascadingRegionSelect` consumers (addresses, search) must be unaffected —
+ prop-gate any change; run its test.
+- Optional static map visualization of covered areas consuming `components/geography` — keep it cheap.
+ Real tile rendering (DEFERRED → [Phase 9](ui-phase-9-customer-account-and-care-circle.md)'s map
+ picker; reuse what it lands).
+- Give `removeArea.mutate` an `onError` toast (currently silent).
+
+### 3.7 Bank — an accounts section, not a one-shot form
+
+- Restructure `nurse/bank/page.tsx` as an accounts section with a persistent «افزودن حساب دیگر» CTA
+ (evidence §1.6). Change-IBAN path: add new → pending inquiry → verified → make primary → old account
+ remains listed. No delete affordance unless the seam supports it (it doesn't — don't invent).
+- Surface the pending ownership poll explicitly: «در حال استعلام صحت شبا، معمولاً چند دقیقه طول
+ میکشد…» instead of a silent pending chip.
+- Fix the error→false-empty hazard: a failed `useNurseBankAccounts` query renders the phase-1
+ ErrorState with retry — never the "no account yet" empty state + open form (which invites a
+ duplicate-IBAN submission).
+- Keep the three-state `BankStatusPanel` design exactly as is (do-not-regress).
+
+### 3.8 Profile — qualifications editable + public preview
+
+- Make education level/field and specializations real form fields (select + chips), submitted through
+ the existing upsert — the server accepts them today (§1.8); no REQ, no server change.
+- Avatar upload + profile save get the phase-1 mutation-error convention (`onError` toasts — both are
+ silent today), and warn before navigating away with a staged-but-unsaved avatar.
+- «نمایهٔ عمومی من»: a preview screen (`/nurse/profile/preview`) composing phase 4's C3 trust-dossier
+ pieces (TrustBadge, attribute chips, `ServicePriceRow` list, coverage chips) **from the nurse's own
+ data** (own profile + `useMyVariants` + `useServiceAreas` + own badge) — no dependency on the search
+ index, so it works pre-publish. Link it from profile and services pages: it is the strongest
+ motivator to complete bio/photo/credentials.
+
+## 4. Mocks & seams in this phase
+
+**No new mocks or seams.** All work stays behind the existing `services/{domain}` seams; do not flip
+any `USE_*_MOCK` flag (verification is deliberately still mock-primary). The one seam *extension* is
+adding `setAcceptingBookings` to the profiles seam (types + clientApi + mockApi in lockstep — the
+endpoint is real; the mock mirrors the flip).
+
+Backend gaps become REQ entries appended to
+[for-backend.md](../../shared-working-context/frontend/requests/for-backend.md) — REQ-001…038 are
+taken; check the tracker's high-water mark (other UI phases may have filed more) and number onward.
+Expected filings, both rendered mock-tolerantly (present → render, absent → degrade gracefully):
+
+- **`submittedAt` on the nurse-facing `VerificationStatusDto`** (B6 timestamp — the data exists; the
+ admin queue DTO already serves it).
+- **Nurse-facing read-back of submitted credential details** (issuing authority, dates, specialties;
+ masked/type-only for the number — never the raw encrypted `credentialNumber`) so B5 hydrates on
+ re-entry. Verify against Swagger first — file only if it truly doesn't exist.
+
+## 5. Critical rules you must not get wrong
+
+- **Verification status is server truth.** The client NEVER flips `is_verified`, never fakes a step
+ result, never derives "verified" from anything but the aggregate. The mock admin sim stays dev-only.
+- **`is_searchable` conditions are server-side.** The activation checklist *reflects* them; the search
+ index flips only via server writes (the accepting-bookings endpoint reindexes in-transaction). Never
+ claim "you are now visible" from a client-side condition check alone.
+- **No fake success — anywhere.** A CTA either performs a real mutation or is guidance. This is the
+ bug this phase exists to kill.
+- **Honest-automation copy stays:** only genuinely automated checks say «استعلام خودکار»; manual-review
+ steps never claim an authority check. TrustBadge `verified` renders only from the approved
+ aggregate; `expired` stays visually distinct from never-verified.
+- **Masked IBAN + `dir="ltr"` stays** on every bank/IBAN render; national-ID and price inputs keep
+ their LTR-pinned `textAlign:'start'` treatment.
+- **Keep-lists from both audits:** token discipline (zero hexes), `borderInlineStart` accents +
+ logical props, money via `PriceDisplay`/BigInt (never a total from rate alone), soft-deactivate-only
+ `VariantCard`, edit-mode locking of variant identity fields, `CascadingRegionSelect`'s cached geo
+ queries + prefill guard, belt-and-braces duplicate coverage handling, locale digits + Shamsi dates.
+- **Design contract:** i18n in both catalogs, dark mode via tokens, MUI v9 API only (no `flexWrap` as
+ a `Stack` prop), `App*` wrappers + icon registry (new icons in `AppIcon/config.ts`, lowercase),
+ co-located tests for every shared component touched (`DocumentUpload`, `CascadingRegionSelect`, new
+ `ActivationChecklist`), fetch/cookies rules untouched (`clientFetch` via the seam, never raw `fetch`).
+
+## 6. Definition of Done
+
+On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):
+
+- [ ] `npm run check` green; `npm run test:ci` green including updated `DocumentUpload` tests
+ (rejected→re-upload→progress) and new `ActivationChecklist` tests.
+- [ ] `en.json`/`fa.json` in sync for every new key; no hard-coded strings.
+- [ ] The publish CTA performs a real `set_accepting_bookings` mutation; grep proves the
+ `publish_done`-snackbar-with-no-effect pattern is gone.
+- [ ] Exactly ONE progress metaphor across B3–B6; `verificationSteps.ts` still drives rendering
+ (no hard-coded step lists).
+- [ ] Re-uploading a rejected document shows live progress with the rejection reason still visible.
+- [ ] Returning to the credentials page with steps `in_review` shows submitted state — not blank
+ fields with a dead submit button; license dates are Jalali inputs.
+- [ ] A verified-account nurse can add another bank account; a failed accounts query shows an error
+ state, never the empty-state form.
+- [ ] The whole-city choice is owned by exactly one control; the district-required error can no
+ longer be triggered by picking a UI-offered option.
+- [ ] Education/specializations round-trip through the real upsert and re-render after reload.
+- [ ] Visual verification on all four axes (`/fa` + `/en` × light + dark), mobile + desktop, for
+ every touched screen — `/fa` first.
+
+## 7. How to test (what a human can verify after this phase)
+
+1. As the seeded **unverified** nurse → `/nurse/services`: the activation checklist shows unmet rows,
+ each deep-linking to its page; the go-live CTA is guidance, not a button that toasts success.
+2. Complete verification via the dev admin sim → rows flip; the CTA becomes «شروع پذیرش رزرو»; click
+ it → Network tab shows `POST nurse_profiles/set_accepting_bookings`; the panel shows the live/pause
+ state. Searching as a customer now finds the nurse (server-side flip).
+3. `/nurse/verification`: one vertical journey with grouped cards (هویت / مدارک حرفهای / بانک) and a
+ TrustBadge payoff preview; B4/B5 show no 3-step Stepper anywhere; B6 shows what-happens-next (+
+ timestamp once the REQ lands).
+4. In B5, upload a doc, have it rejected (mock), re-upload → the progress bar animates while the
+ rejection reason stays visible; the re-upload button is disabled mid-flight. Leave and return →
+ submitted steps render as summaries; submit is not silently dead; dates open a Jalali picker.
+5. New variant: step-2 options render as chips (no broken grouped borders); step 3 shows the live
+ `VariantCard` preview; a duplicate yields a readable warning + "edit existing" path.
+6. `/nurse/coverage`: no second whole-city affordance that errors; whole-city adds via the single
+ control; (if built) the map shows covered areas.
+7. `/nurse/bank` with a verified account → «افزودن حساب دیگر» opens the form; pending shows the
+ explicit inquiry copy; kill the API and reload → error state with retry, not the empty form.
+8. `/nurse/profile`: edit education + specializations, save, reload → values persist; navigate away
+ with an unsaved avatar → warning. «نمایهٔ عمومی من» renders the own-data listing with TrustBadge +
+ prices + coverage.
+9. Repeat the key screens on `/en`, dark mode, and a ~390px viewport.
+
+## 8. Hand off & document (close the phase)
+
+- Update `client/CLAUDE.md` (Project Structure) in the same change: the `/nurse/profile/preview`
+ route, `components/ActivationChecklist/`, the profiles-seam `setAcceptingBookings` addition, and
+ the reshaped verification hub.
+- Write `dev/shared-working-context/reports/ui-phase-8-report.md`: what shipped per §3 subsection,
+ the REQ numbers actually filed, the PublishGate→real-toggle decision with its server evidence, and
+ any foundation files you extended (per the README ownership rules — minimally, noted, never forked).
+- File the REQs in the tracker (§4) with `filed by ui-phase-8` attribution.
+- Save a memory note per operating-rules §8: the activation checklist's two-tier honesty (search
+ visibility vs getting paid), the accepting-bookings wiring, the unified-journey decision, the
+ DocumentUpload precedence fix.
diff --git a/dev/post-phase/ui/ui-phase-9-customer-account-and-care-circle.md b/dev/post-phase/ui/ui-phase-9-customer-account-and-care-circle.md
new file mode 100644
index 0000000..7c119ac
--- /dev/null
+++ b/dev/post-phase/ui/ui-phase-9-customer-account-and-care-circle.md
@@ -0,0 +1,280 @@
+# UI Phase 9 — Customer account & care circle
+
+> **Mission:** the customer account area is functionally complete but emotionally wrong for a home-care
+> product: a flat settings form with no sign-out, care recipients as clinical rows behind an invisible tap
+> target, a whole-list free-text care-record edit mode that loses drafts, and an address "map" that is a
+> blank coordinate grid feeding the EVV proximity check. Reframe the area around **the people being cared
+> for** — an account hub, a care circle with faces, per-item structured record editing, a real Neshan map —
+> and fix the real defects along the way (silent profile-load error, error→false-empty, cramped dialogs).
+>
+> **Track:** frontend · **Depends on:** [Phases 0–2](ui-phase-2-shells-and-navigation.md) ·
+> **Unlocks:** the account area feels like caring for people, not filling forms
+>
+> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md)
+> and invoke the frontend-designer skill — both are mandatory.**
+
+## 1. Context — where this sits
+
+Balinyaar's customer is a **family member arranging care for someone they love** — a parent, a spouse,
+sometimes themselves. The account area (`client/src/app/[locale]/(private-routes)/(customer)/` — `profile/`,
+`patients/`, `patients/[id]/record/`, `addresses/`) is where that relationship lives, and today it reads
+like an admin table. Every problem below is verified in code:
+
+- **Profile is a flat form, and a failed load silently blanks it.** `profile/page.tsx:16–18` handles only
+ `isLoading` from `useCustomerProfile()`; on error the form renders with `initial={profile ?? null}` — a
+ save from that blank state **overwrites server truth with empty fields**. No identity header, no phone,
+ no sign-out; the only status affordance is a color-only completeness line (lines 75–77).
+- **Query errors collapse into false-empty.** `patients/page.tsx:83` — `const isEmpty = !isLoading &&
+ patients.length === 0;` never reads `isError`, so a failed query tells a family «هنوز بیماری ثبت نشده».
+ Identical bug in `addresses/page.tsx:91`.
+- **The care record is undiscoverable and error-prone to edit.** `PatientCard.tsx:66–85` renders the
+ tap-to-open area as an unstyled `component="button"` (`background: 'none', border: 'none'`) — nothing
+ signals the richest screen in the area exists. Inside, `record/page.tsx` edits each tab as a whole-list
+ mode with per-tab `useState` drafts (lines 197/275/346) **destroyed on tab switch**; dose/frequency/
+ time-of-day are plain free text (`routine_time`, line 321); سوابق is a flat card list with bare
+ «قبلی/بعدی» paging (lines 439–445).
+- **The map is not a map.** `components/geography/AddressMapPicker.tsx:28–35` documents itself as "NOT a
+ real map (no Neshan/Google tiles), only a bounded canvas" and surfaces raw lat/lng captions — yet the pin
+ it produces feeds nurse arrival and the EVV proximity check.
+- **Forms are crammed into `maxWidth="sm"` dialogs** on a phone-first shell with no `fullScreen` and no
+ dirty-state guard (`patients/page.tsx:160`, `addresses/page.tsx:170`); backdrop-click discards work.
+- **No avatar/photo concept exists in the customer identity system** — `CustomerProfile`
+ (`services/profiles/types.ts:44–50`) and `Patient` are text-only; `avatarUrl` is nurse-only.
+
+**What already exists (do not rebuild):**
+
+- [Phase 0](ui-phase-0-design-language.md) theme/brand, [Phase 1](ui-phase-1-primitives-and-states.md)
+ primitives (ErrorState/EmptyState kit, PageHeader, card kit, skeleton twins), [Phase 2](ui-phase-2-shells-and-navigation.md)
+ customer shell + the **sign-out affordance** — this phase gives sign-out its *home*, not its first existence.
+- The `services/{domain}` layer: `profiles`, `patients` (+ `age.ts`), `addresses`, `patientRecords` are
+ wired and (except `patientRecords`, REQ-027) **real**. This phase is presentation + defect fixes.
+- `PatientHeader` (shared by E1 card + E2 record), `PatientForm`, `PatientCard`, `CascadingRegionSelect`,
+ `AddressForm`, `AddressCard`, `AddressMapPicker` — all tested; restyle, don't fork.
+- The care record's **non-leaking access gate**: `useRecordAccess` before any clinical fetch,
+ `usePatient(patientId, { enabled: canView })` (`record/page.tsx:50–52`), access-denied card at 59–74,
+ ownership banner at 108–116. Preserve verbatim.
+- Soft-archive semantics + copy, the dashed-border empty states («اولین آدرس را اضافه کنید تا پرستار بداند
+ کجا بیاید»), content-shaped skeletons, inline per-field validation with error-clearing.
+
+## 2. Required reading (do this first)
+
+- [audit/customer-account.md](audit/customer-account.md) — the 16 problems + 10 opportunities + the
+ keep-list this phase executes; every file/line above comes from it.
+- Code, in this order: `profile/page.tsx`, `patients/page.tsx`, `patients/[id]/record/page.tsx`,
+ `addresses/page.tsx` (all under `client/src/app/[locale]/(private-routes)/(customer)/`), then
+ `client/src/components/{PatientCard,PatientHeader,PatientForm,RelationSelect,GenderToggle}/` and
+ `client/src/components/geography/{AddressMapPicker,AddressForm,AddressCard,CascadingRegionSelect}.tsx`,
+ then `client/src/services/{profiles,patients,addresses,patientRecords}/types.ts` and
+ `client/src/services/patients/age.ts`.
+- [../../../.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md) —
+ the design contract (invoke the skill, don't just read it).
+- Product: [../../../product/business/01-actors-and-onboarding.md](../../../product/business/01-actors-and-onboarding.md)
+ (who the "patient" actually is — relations include «خودم») and the Persian glossary in
+ [../../../product/overview/platform-summary.md](../../../product/overview/platform-summary.md) — both feed
+ the naming decision in 3.2; [../../../product/business/06-evv-and-service-delivery.md](../../../product/business/06-evv-and-service-delivery.md)
+ for why the address pin matters (advisory EVV proximity).
+- The REQ tracker tail: [../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
+ — REQ-001…038 taken at audit time; earlier UI phases may have appended more. Take the next free number.
+- `client/CLAUDE.md` — Golden rules + the `(customer)` part of Project Structure.
+
+## 3. Scope — build this
+
+### 3.1 Profile → account hub (`profile/page.tsx`)
+
+Rebuild the Profile tab as the customer's account center:
+
+1. **Identity header** — warm auto-colored initials (same util as 3.2) + full name + masked phone from
+ `/me` (`useMe().phone` via `maskIranMobile` from `@/components/PhoneNumberField`, in a `dir="ltr"` span).
+ The first place the customer sees *themselves* in the app.
+2. **Grouped tappable rows** below the header — اطلاعات شخصی / مخاطب اضطراری / نشانیها / زبان / اعلانها /
+ پشتیبانی / **خروج**. نشانیها → `/addresses`, اعلانها → `/notifications`, پشتیبانی → `/support/tickets`;
+ اطلاعات شخصی and زبان open focused edit surfaces (prefer bottom sheets so `/profile` stays the single
+ route; sub-routes require a `client/CLAUDE.md` Project Structure update). The زبان row owns the
+ server-stored `preferredLanguage` and hands the actual locale switch to phase 2's switcher — don't build
+ a second locale mechanism. The خروج row is the sign-out's home: confirm dialog → `useLogout()` (the
+ single logout path — never hand-roll cookie clearing).
+3. **Emergency contact as a status card**, not two bare fields: complete → success check + contact name +
+ a `tel:` link (tel-only, per the f14 emergency rule); incomplete → a warm nudge explaining *why* nurses
+ need it («پرستار باید بداند در شرایط اضطراری با چه کسی تماس بگیرد») + edit CTA.
+4. **Fix the silent load error**: on `useCustomerProfile()` error render the phase-1 ErrorState with retry —
+ **never** the editable form. The blank-form-overwrites-server-truth path must be impossible.
+
+### 3.2 Care-circle reframe (`patients/page.tsx`, `PatientCard`, `PatientHeader`)
+
+1. **Naming**: consult the product docs + glossary (§2) before renaming «بیماران». Candidates: «عزیزان شما»
+ / «حلقه مراقبت». Mind the «خودم» relation — the term must not be absurd for self-care («حلقه مراقبت» is
+ the safer default). **Record the decision in your report** and apply it consistently (page title, nav
+ label, empty states, the A5 home nudge). **Copy-level rename only**: the `/patients` route,
+ `services/patients`, and the `patients.*` i18n key names stay unchanged.
+2. **Avatar slot on `PatientHeader`** — the patients API has no photo field, so ship warm auto-colored
+ initials: a deterministic util (name hash → one of ~6 new `--bal-avatar-*` token pairs added to
+ `tokens.css` **both scheme blocks**; note the token addition as a foundation extension in your report —
+ phase 0 owns `theme/`). `PatientHeader` is shared by the E1 card and E2 record, so one change gives both
+ a face; update its co-located test. Real photo upload is (DEFERRED → optional REQ, see §4).
+3. **Visible record affordance on `PatientCard`** — replace the invisible button with a
+ `CardActionArea`-style hover/press surface or an explicit «مشاهده پرونده» chevron row. Add a
+ **last-visit meta line** («آخرین ویزیت: ۱۲ تیر») via an optional `lastVisitLabel` prop — sourced
+ best-effort from the cached customer bookings list; the server-truth field is a REQ (§4). Omit when
+ unknown; never fabricate.
+4. **Fix error→false-empty**: `isError` branch with phase-1 ErrorState + retry on **both** `patients/page.tsx`
+ and `addresses/page.tsx`. A failed query must never render «هنوز بیماری ثبت نشده» + add CTA.
+5. **Keep** soft-archive semantics and its copy verbatim (do-not-regress).
+
+### 3.3 Care record editing (`patients/[id]/record/page.tsx`)
+
+1. **Per-item bottom sheets replace whole-list edit mode.** Each medication/routine/task row gets edit (and
+ each tab an add CTA) opening a bottom sheet (`Drawer anchor="bottom"`; standard dialog on desktop).
+ Medication sheet: name + **structured dose** (amount + unit select قرص/کپسول/قطره/سیسی/واحد) +
+ **frequency presets** («روزی ۱ بار» … «هر ۸ ساعت» / «در صورت نیاز» + free-text fallback) + **time-of-day
+ chips** (stable codes `morning|noon|evening|night` → صبح/ظهر/عصر/شب). Routine items get the same chip row
+ instead of the free-text `routine_time`; tasks stay label + done. A dirty sheet gets a discard-confirm;
+ killing the whole-list mode removes the silent tab-switch draft loss by construction.
+2. **REQ posture**: the family-owned record is REQ-027 mock territory (no backend). Keep the UI seam-tolerant
+ behind `services/patientRecords` — extend the client model + `mockApi` for the structured fields, and
+ **append the structured shape as an addendum to REQ-027** in the tracker (dose amount/unit, frequency
+ preset codes, time-of-day codes) so the eventual table matches what the UI collects.
+3. **Visit-note history becomes a timeline**: group سوابق by Shamsi month with a subtle rail; each
+ `VisitNoteCard` gains a done/undone task summary line; when `VisitNote.bookingId` is non-null
+ (`services/patientRecords/types.ts:79–81`) link «مشاهده رزرو» → `/bookings/[id]`, showing the service
+ name only when derivable from the cached booking — never block on it. Keep paging (grouped within pages
+ is fine). A read-mode "daily schedule" view (meds grouped صبح/ظهر/شب) is (DEFERRED — not in this chain).
+4. **Do not touch the access gate** — `useRecordAccess` before any clinical fetch, the access-denied card,
+ and the ownership banner survive the redesign verbatim.
+
+### 3.4 Addresses + the real map (`addresses/page.tsx`, `components/geography/`)
+
+1. **Real Neshan web tiles behind the existing `AddressMapPicker` boundary** — the component's props/output
+ (`{ latitude, longitude }`) don't change, so `AddressForm` is untouched. Client-side embed (official
+ Neshan web SDK or Leaflet + Neshan tiles), **dynamically imported with `ssr: false`**. Key from
+ `NEXT_PUBLIC_NESHAN_KEY` — the **web key is separate config** from the server's `NeshanGeocoder` adapter
+ (refinement phase 8; lives under `server/src/Infrastructure/…/Seams/Real/` — do not touch it). Document
+ the variable in `client/.env.sample` (the repo uses `.env.sample`, **not** `.env.example`). **When the
+ key is unset, fall back to the current grid stand-in** so dev/CI/jsdom tests keep working — keep the
+ fallback code, don't delete it.
+2. **Map features**: address search box (Neshan geocode), locate-me (GPS) button, draggable pin, and a
+ reverse-geocoded pin preview («پین روی: خیابان ولیعصر…») replacing the raw lat/lng captions — coordinates
+ never render in the UI again. The pin still only *refines* coordinates; the bookable geography remains
+ the `CascadingRegionSelect` choice. Tokenize the pin's hard-coded `rgba` drop-shadow (line 116) too.
+3. **Pin-quality cue on `AddressCard`**: `Address.latitude` is nullable (`services/addresses/types.ts:38`) —
+ show «پین ثبت شده» / «پین ندارد» (the latter with a warm fix-it nudge, since a missing pin degrades the
+ nurse's arrival + EVV). A static map thumbnail per card is (DEFERRED — revisit once tiles are proven).
+4. **Full-screen mobile form dialogs** for the patient form (`patients/page.tsx:160`) and the address form
+ (`addresses/page.tsx:170`): `fullScreen` below the `sm` breakpoint with an app-bar header (title / close
+ / save) and a dirty-state discard-confirm on close/backdrop. If phase 1 shipped a form-dialog primitive,
+ use it; otherwise add shared `components/FormDialogShell/` (co-located test) and note the foundation
+ extension in your report.
+5. **Fix the fa copy bug** in `messages/fa.json:228`: «…جزئیاتی که پرستار برای یافتن در نیاز دارد.» → «…برای
+ یافتن درِ منزل نیاز دارد.» (or «برای یافتن نشانی نیاز دارد.»). Phase 12 owns the global copy sweep — fix
+ this one here since you touch the form, and flag it in your report so phase 12 doesn't double-edit.
+
+### 3.5 Form quality (`PatientForm`, loading consistency)
+
+1. **Structured name fields**: replace the single full-name field with نام / نام خانوادگی (the wire already
+ takes `firstName`/`lastName`/`displayName`; today `splitName` at `PatientForm.tsx:32–38` guesses, with
+ `lastName` falling back to `firstName`). `displayName` = the joined value. If the Patient *read* DTO
+ lacks `firstName`/`lastName` for edit-prefill, fold that into the REQ from 3.2.
+2. **Birth-year presentation honesty**: `age.ts:10` fabricates a Jan-1 `birthDate` from the collected age —
+ that stays (wire mapping), but the UI must never render the fabricated full date anywhere; display
+ age-only (`age_years`), and consider collecting سال تولد instead of سن if it reads warmer. Verify no
+ surface prints raw `birthDate`.
+3. **Loading consistency**: profile's full-page `AppLoading` (`profile/page.tsx:18`) → a form-shaped
+ skeleton, matching the skeleton language of patients/addresses/record.
+4. **Small a11y fixes from the audit** (check an earlier phase didn't already land them): `GenderToggle`
+ gets `width: '100%'` so its `flex: 1` children actually split (today it renders content-width, misaligned
+ against `fullWidth` fields); `RelationSelect` selected state gains a check icon + fill (border-color-only
+ today — fails WCAG 1.4.1). Both are shared → update their co-located tests; **never** loosen
+ GenderToggle's never-defaulted, non-deselectable constraint.
+
+## 4. Mocks & seams in this phase
+
+**No new mocks or seams.** The area's domains stay as they are: `profiles`/`patients`/`addresses` real,
+`patientRecords` mock-primary behind its existing seam (REQ-027). The Neshan web embed is client config
+(`NEXT_PUBLIC_NESHAN_KEY`), not a backend seam — with a grid-stand-in fallback when unset.
+
+Backend gaps become REQ entries appended to [the tracker](../../shared-working-context/frontend/requests/for-backend.md)
+— check its tail and number onward (REQ-001…038 taken at audit time). Expected filings:
+
+1. **REQ-(next): patient care metadata** — `lastVisitAt` (optionally `visitCount`) on the patient read
+ model, plus `firstName`/`lastName` on the read DTO if absent (3.2/3.5).
+2. **REQ-027 addendum** (not a new number): the structured care-record field shape — medication
+ `{ doseAmount, doseUnit, frequencyCode|frequencyText, timeOfDay[] }`, routine `timeOfDay` codes (3.3).
+3. **REQ-(next), optional & product-gated: patient photo upload** — the UI ships initials-only either way.
+
+## 5. Critical rules you must not get wrong
+
+- **The non-leaking access-denied gate stays.** `useRecordAccess` gates **before** any clinical fetch
+ (`usePatient` stays `enabled: canView`); a 403/denied renders the access-denied card with zero clinical
+ data. Preserve the family-ownership banner. **Clinical text is never logged, never in localStorage, never
+ in a query string** (`patientRecords` rule).
+- **`AddressMapPicker`'s RTL engineering survives the tile swap**: the canvas stays `dir="ltr"`, marker
+ positioning stays inline-`style` (the stylis RTL plugin flips `left` and `translate` — see the comment at
+ lines 57–59). Real map containers get the same `dir="ltr"` island treatment.
+- **`CascadingRegionSelect` architecture stays** — parent-gated enabling, per-level progress adornments, the
+ explicit "whole city" MenuItem as a real choice. The map pin refines, never replaces, the region choice.
+- **`GenderToggle` stays never-defaulted and non-deselectable** (same-gender matching). **Soft-archive copy
+ + optimistic-with-explanatory-error stays**; the archive confirm's dismiss stays the safe/neutral button.
+- **Sign-out goes through `useLogout()`** — the single logout path (server revoke + cookie clear + LOG_OUT +
+ cache drop). No cookie handling in page code.
+- Design-contract non-negotiables that bite here: i18n keys in **both** catalogs; tokens not hexes (new
+ avatar colors = `--bal-*` pairs in both scheme blocks); RTL logical props (phone numbers get deliberate
+ `dir="ltr"` islands); dark mode on every new surface; MUI v9 API only; shared components keep co-located
+ tests; fetch/cookies only via `@/lib/api` + `@/lib/cookies`.
+
+## 6. Definition of Done
+
+On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):
+
+- [ ] `npm run check` green; `npm run test:ci` green with updated tests for every touched shared component
+ (`PatientHeader`/`PatientCard`/`PatientForm`/`GenderToggle`/`RelationSelect`/`AddressMapPicker`/
+ `AddressCard`/any new `FormDialogShell`).
+- [ ] `en.json`/`fa.json` in sync; the «بیماران» rename applied consistently in both; the `fa.json`
+ line_hint bug fixed.
+- [ ] Visual verification on the four axes (`/fa` + `/en` × light + dark), mobile **and** desktop — verify
+ mobile at `/fa` first.
+- [ ] With the API stopped: profile shows ErrorState + retry (no editable blank form); patients and
+ addresses show ErrorState + retry (no false-empty). With the API back, retry recovers in place.
+- [ ] `/profile` shows identity header (initials + name + masked LTR phone), grouped rows, emergency-contact
+ status card, and a working خروج row (confirm → logged out → `/login`).
+- [ ] Care circle: every person has a colored-initials avatar (stable across reloads), a visible
+ «مشاهده پرونده» affordance, and archive/edit still work.
+- [ ] Care record: add/edit medication via bottom sheet with structured dose/frequency/time-of-day; tab
+ switches lose nothing; سوابق is a month-grouped timeline with booking links when `bookingId` exists.
+- [ ] Addresses: with `NEXT_PUBLIC_NESHAN_KEY` set — real tiles, search, locate-me, draggable pin,
+ reverse-geocoded preview, no raw lat/lng anywhere; with the key unset — the grid fallback still works
+ and tests pass. `AddressCard` shows the pin-quality cue. `.env.sample` documents the variable.
+- [ ] Patient + address forms are full-screen dialogs on mobile with app-bar header and dirty-state confirm.
+ REQ entries filed per §4; REQ-027 addendum recorded.
+
+## 7. How to test (what a human can verify after this phase)
+
+1. Log in as a seeded customer (`0912000000x`) on a mobile viewport at `/fa`. Open the Profile tab → see
+ your initials, name, and masked phone (digits LTR); rows for اطلاعات شخصی/مخاطب اضطراری/نشانیها/زبان/
+ اعلانها/پشتیبانی/خروج. Tap خروج → confirm → you land on `/login`, session revoked.
+2. Clear the emergency contact → the warm "why nurses need this" nudge; fill it → check + `tel:` link.
+3. Stop the API, reload `/profile`, `/patients`, `/addresses` → each shows an error card with retry — no
+ blank form, no «هنوز بیماری ثبت نشده». Start the API, tap retry → data returns without a full reload.
+4. Open the care circle → each person has a colored-initials avatar and a visible «مشاهده پرونده» chevron;
+ tap it → the record opens. The renamed title appears here and in the tab bar.
+5. In the record, tap "add medication" → bottom sheet with dose amount + unit, frequency preset chips, and
+ صبح/ظهر/عصر/شب chips; save → the row renders the structured summary. Start editing, switch tabs, come
+ back → nothing lost. Close a dirty sheet → discard confirm.
+6. Open سوابق → notes grouped by Shamsi month on a rail; a booking-linked note links to `/bookings/[id]`.
+7. Add an address on mobile → the form opens full-screen with app-bar header; with a Neshan key set, search
+ «ولیعصر», drag the pin, tap locate-me → the preview line shows the reverse-geocoded street, never raw
+ coordinates. Back on the list, that address shows «پین ثبت شده»; an old pin-less address shows «پین ندارد».
+ Tap close with unsaved changes → discard confirm; cancel keeps your draft.
+8. Repeat 1, 4, 5, 7 on `/en` (LTR) and in dark mode — avatars, map island, timeline, and status cards all
+ render correctly on all four axes.
+
+## 8. Hand off & document (close the phase)
+
+- Update `client/CLAUDE.md` "Project Structure" for anything added/renamed (new `components/` entries such
+ as `FormDialogShell`, the avatar util, any profile sub-routes) and the `(customer)` route notes for the
+ profile hub + the copy-level care-circle rename.
+- Append the REQ entries + the REQ-027 addendum to
+ [../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md).
+- Write the frontend report at `dev/shared-working-context/reports/ui-phase-9-report.md`: the naming
+ decision and why, the avatar-token additions (foundation extension), the Neshan embed choice + fallback
+ behavior, the fa copy fix (flag it for phase 12's sweep), REQs filed, deferrals.
+- Save a memory note per operating-rules §8: account-hub structure, the naming decision, the Neshan web-key
+ config (`NEXT_PUBLIC_NESHAN_KEY`, fallback-to-grid), and the REQ-027 structured-fields addendum.
diff --git a/server/src/API/Baya.Web.Api/appsettings.Development.json b/server/src/API/Baya.Web.Api/appsettings.Development.json
index 0977c9e..2ca0064 100644
--- a/server/src/API/Baya.Web.Api/appsettings.Development.json
+++ b/server/src/API/Baya.Web.Api/appsettings.Development.json
@@ -1,15 +1,41 @@
{
+ "ConnectionStrings": {
+ "SqlServer": "Server=87.107.152.16,1433;Database=Baya;User Id=sa;Password=N8@s5Taw1zWeh@#Hm;TrustServerCertificate=True;Encrypt=False;",
+ "logDb":"Server=87.107.152.16,1433;Database=Baya_Logs;User Id=sa;Password=N8@s5Taw1zWeh@#Hm;TrustServerCertificate=True;Encrypt=False;"
+ },
"IdentitySettings": {
- "SecretKey": "dev-only-jwe-signing-key-not-for-production-0123456789abcdef",
- "Encryptkey": "dev-only-16bytes"
+ "SecretKey": "SET_VIA_USER_SECRETS_OR_ENV",
+ "Encryptkey": "SET_VIA_USER_SECRETS_OR_ENV",
+ "Issuer": "Balinyaar",
+ "Audience": "BalinyaarClient",
+ "NotBeforeMinutes": "0",
+ "ExpirationMinutes": "60"
},
"Seams": {
"FieldEncryption": {
- "Key": "local-dev-field-encryption-key-not-for-production",
- "HashKey": "local-dev-field-hash-key-not-for-production"
+ "Key": "SET_VIA_USER_SECRETS_OR_ENV",
+ "HashKey": "SET_VIA_USER_SECRETS_OR_ENV"
+ },
+ "ObjectStorage": {
+ "RootPath": ""
+ },
+ "Geocoding": {
+ "ReturnNullCoordinates": false,
+ "LowConfidenceMarker": "NO_GEO",
+ "ResolvedConfidence": 0.9
}
},
"Cors": {
- "AllowedOrigins": [ "http://localhost:3000" ]
+ "AllowedOrigins": []
+ },
+ "ForwardedHeaders": {
+ "KnownProxies": [],
+ "KnownNetworks": []
+ },
+ "AllowedHosts": "*",
+ "Kestrel": {
+ "EndpointDefaults": {
+ "Protocols": "Http1AndHttp2"
+ }
}
}