# Client Detail Redesign — Frontend Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Rebuild the Modules and Payments tabs of Client Detail — module details as a 50/50 read-only report with a single Edit per group, onboarding as a horizontal per-module status line, per-module Documents (incl. a bulk-SMS-purchase action), and a Payments Outstanding panel — plus a per-module template editor and a stalled-onboarding tile, verified pixel-tight on mobile via a red-team alignment pass. **Architecture:** React + Vite; all data via the typed `api.ts` calls. Styling is token-based CSS in `apps/hq-web/src/app.css` (Warm/Slate/Graphite/Zinc neutrals × 7 accents × light/dark) — new surfaces use existing tokens only. This plan consumes the backend from `2026-07-19-client-detail-redesign-backend.md` (ledger `outstanding`/`allocations`, per-module templates, `setMilestone(paymentId)`, `generateBulkSmsPurchaseQuote`, `stalledProjects`). **Tech Stack:** React 18, TypeScript (strict), Vite, `@sims/ui`. ## Global Constraints - **Prerequisite:** the backend plan is merged (its API shapes exist). If not, stop and do it first. - No new colour constants — style through CSS custom properties (`--accent`, `--border`, `--ok/--warn/--err`, `--bg-raised`, `--text`, `--text-dim`, `--radius`, `--mono`). Must work in **light and dark** and all four neutrals. - The page body never scrolls sideways; wide content scrolls inside its own `overflow-x:auto`. Numeric columns use `font-variant-numeric: tabular-nums`. - No frontend unit-test harness exists (tests are backend vitest). Each task's verification is: `npm run typecheck` clean, `npm run build --workspace @sims/hq-web` clean, and a **manual check in the running app** (backend on :5182 serving the built UI, or Vite dev on :5183). The final task is the responsive + red-team gate. - Run from repo root. Reference the approved mockup `scratchpad/client-detail-mockup.html` for exact layout/CSS. --- ## File Structure - `apps/hq-web/src/app.css` — MODIFY: `.split`/`.kv`, `.track`/`.node`, `.out` outstanding-panel, doc-label pill classes. - `apps/hq-web/src/pages/ClientDetail.tsx` — MODIFY: `ServiceDataCard` (50/50 + single edit), replace `MilestoneChecklist` with `StatusLine`, add `ModuleDocuments`, Payments tab (`OutstandingPanel` + Settled column), summary-table trim. - `apps/hq-web/src/pages/client-forms.tsx` — MODIFY: `AssignModuleForm` copy → "Add a module". - `apps/hq-web/src/api.ts` — MODIFY: add `generateBulkSmsPurchase`, `getStalledProjects`, `getModuleTemplate`/`setModuleTemplate`, and thread `paymentId` into `setMilestone`. - `apps/hq-web/src/pages/Modules.tsx` — MODIFY: per-module onboarding template editor. - `apps/hq-web/src/pages/Dashboard.tsx` — MODIFY: stalled-onboarding tile. Each new sub-component (`StatusLine`, `ModuleDocuments`, `OutstandingPanel`) is a local function component in `ClientDetail.tsx` next to the code it replaces (that file already houses `ServiceDataCard`, `MilestoneChecklist`, etc.). Keep them focused; if `ClientDetail.tsx` grows past readability, extract the module-block components into `pages/client-module-block.tsx` as a follow-up (note it, don't force it). --- ## Task 1: CSS foundation (port the mockup styles) **Files:** - Modify: `apps/hq-web/src/app.css` (append a new section) **Interfaces:** - Produces: classes `.split`, `.kv .k/.val`, `.track`/`.node` (+ `.done/.now`), `.out` (+ `.oh/.orow/.ofoot`), `.doc-pill` used by later tasks. These MUST match the class names the JSX in Tasks 2–5 references. - [ ] **Step 1: Append the styles** Add to `apps/hq-web/src/app.css` (values ported from `scratchpad/client-detail-mockup.html`, swapping the mockup's literal colours for tokens): ```css /* ---- Client Detail redesign (D-CDR) ---- */ /* 50/50 service sheet */ .cdr-split { display: grid; grid-template-columns: 1fr 1fr; gap: 0 30px; } .cdr-split > .col { padding: 13px 0; min-width: 0; } .cdr-split > .col + .col { border-left: 1px solid var(--border); padding-left: 30px; } .cdr-kv { display: flex; align-items: baseline; gap: 10px; padding: 6px 0; min-width: 0; } .cdr-kv + .cdr-kv { border-top: 1px dashed var(--border); } .cdr-kv > .k { flex: none; width: 120px; font-size: 10.5px; text-transform: uppercase; letter-spacing: .5px; color: var(--text-dim); font-weight: 600; padding-top: 2px; } .cdr-kv > .v { flex: 1; min-width: 0; display: flex; align-items: center; gap: 7px; flex-wrap: wrap; } .cdr-kv > .v .txt { overflow: hidden; text-overflow: ellipsis; } @media (max-width: 720px) { .cdr-split { grid-template-columns: 1fr; } .cdr-split > .col + .col { border-left: none; padding-left: 0; border-top: 1px solid var(--border); } } /* horizontal onboarding status line */ .cdr-track-wrap { overflow-x: auto; padding: 10px 2px 4px; } .cdr-track { display: flex; min-width: min-content; } .cdr-node { flex: 1 0 112px; display: flex; flex-direction: column; align-items: center; text-align: center; position: relative; padding: 0 4px; } .cdr-node .bar { position: absolute; top: 11px; right: 50%; width: 100%; height: 2px; background: var(--border); } .cdr-node.done .bar, .cdr-node.now .bar { background: var(--accent); } .cdr-node:first-child .bar { display: none; } .cdr-node .dot { position: relative; z-index: 1; width: 22px; height: 22px; border-radius: 50%; border: 2px solid var(--border-strong); background: var(--bg-raised); display: grid; place-items: center; font-size: 11px; color: transparent; cursor: pointer; } .cdr-node.done .dot { background: var(--ok); border-color: var(--ok); color: #fff; } .cdr-node.now .dot { border-color: var(--accent); color: var(--accent); box-shadow: 0 0 0 4px var(--accent-soft); } .cdr-node .nm { font-size: 11px; margin-top: 7px; max-width: 104px; line-height: 1.35; } .cdr-node.done .nm { color: var(--text-dim); } .cdr-node.now .nm { font-weight: 600; } .cdr-node .dt { font-size: 10px; color: var(--text-dim); font-family: var(--mono); margin-top: 3px; } /* outstanding panel */ .cdr-out { border: 1px solid color-mix(in srgb, var(--warn) 30%, var(--border)); border-radius: var(--radius); overflow: hidden; } .cdr-out .oh { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-bottom: 1px solid var(--border); } .cdr-out .orow { display: flex; align-items: center; gap: 12px; padding: 9px 14px; border-bottom: 1px solid var(--border); font-size: 12.5px; } .cdr-out .orow:last-child { border-bottom: none; } .cdr-out .orow .due { margin-left: auto; font-family: var(--mono); font-weight: 600; } .cdr-out .ofoot { display: flex; align-items: center; gap: 10px; padding: 10px 14px; background: var(--bg-inset); } /* document-type pill */ .doc-pill { font-size: 10.5px; font-weight: 600; letter-spacing: .3px; border-radius: 999px; padding: 1px 8px; border: 1px solid var(--border); color: var(--text-dim); background: var(--bg-inset); } ``` - [ ] **Step 2: Verify build** Run: `npm run build --workspace @sims/hq-web` Expected: build succeeds (CSS is valid, no selector errors). - [ ] **Step 3: Commit** ```bash git add apps/hq-web/src/app.css git commit -m "style(client-detail): CSS for 50/50 sheet, status line, outstanding panel" ``` --- ## Task 2: Module details → 50/50 report with single Edit per group **Files:** - Modify: `apps/hq-web/src/pages/ClientDetail.tsx` — `ServiceDataCard` (lines ~961–1103) and fold `ModuleFieldsForm` (lines ~1112–1209) presentation into it. **Interfaces:** - Consumes: existing `patchClientModule`, `setModuleFields`, `revealModulePassword`, `SecretField`, `cm.details`, `cm.fieldValues`, `props.fieldSpec`. - Produces: read-only 50/50 report; each group (`Access`, `Details`) has one **Edit** toggling that group's inputs; save = one audited call. - [ ] **Step 1: Replace the read view of `ServiceDataCard`** Rewrite the returned read view (the non-editing `return (...)` at ~1066) as the 50/50 report. Keep the existing edit-state machinery; change only what renders. The read view: ```tsx return (
Access
copy(cm.username ?? '', 'Username')} />
Password {revealed !== undefined ? <>{revealed} : cm.hasPassword ? <>••••••••{managerial && } : not set}
{cm.remark !== null && cm.remark !== '' && }
Details
{props.fieldSpec.filter((f) => f.type !== 'secret').map((f) => ( ))} {cm.details.map((d, i) => )} {props.fieldSpec.filter((f) => f.type === 'secret').map((f) => (
{f.label}
))}
) ``` Add a small `Kv` helper in the same file: ```tsx function Kv(props: { label: string; value: string | undefined; mono?: boolean; copyable?: boolean; onCopy?: () => void }) { const v = props.value return (
{props.label} {v !== undefined && v !== '' ? {v} : } {props.copyable === true && v !== undefined && v !== '' && }
) } ``` - [ ] **Step 2: Fold the fieldSpec edit into the Details Edit** In the editing branch of `ServiceDataCard` (~1028), extend the edit form to also render the non-secret `fieldSpec` inputs (move the control logic from `ModuleFieldsForm`), so one **Save** persists provider/username/password/remark + details + fieldValues. Remove the separate always-on `ModuleFieldsForm` render at line ~1095–1100 (secret fields stay as `SecretField` in the read view). Keep `saveEdit` calling `patchClientModule` for the D20 fields and `setModuleFields` for the fieldSpec values (two audited calls in sequence is acceptable; or combine if `patchClientModule` accepts field values — check its signature). - [ ] **Step 3: Verify** Run: `npm run typecheck && npm run build --workspace @sims/hq-web` Then run the app and open a client with SMS + RTGS: confirm the 50/50 read view, one Edit per group, save round-trips, secret reveal still audited, `—` for empty fields, no `*` markers. - [ ] **Step 4: Commit** ```bash git add apps/hq-web/src/pages/ClientDetail.tsx git commit -m "feat(client-detail): module details as 50/50 report with single edit per group" ``` --- ## Task 3: Onboarding → horizontal status line (+ payment-step link) **Files:** - Modify: `apps/hq-web/src/pages/ClientDetail.tsx` — replace `MilestoneChecklist` (~1270–1357) with `StatusLine`. - Modify: `apps/hq-web/src/api.ts` — thread `paymentId` into `setMilestone`. **Interfaces:** - Consumes: `getMilestones`, `setMilestone(clientModuleId, key, done, doneOn?, paymentId?)`, `addMilestone`, `Milestone` (now with `paymentId`). - Produces: `StatusLine` component rendering the horizontal `.cdr-track`. - [ ] **Step 1: Extend the web `setMilestone` call** In `apps/hq-web/src/api.ts`, update the milestone POST helper to accept an optional `paymentId` and include it in the body. Confirm the `Milestone` type includes `paymentId: string | null`. - [ ] **Step 2: Write `StatusLine`** Replace `MilestoneChecklist` with: ```tsx function StatusLine(props: { clientModuleId: string; name: string }) { const toast = useToast() const list = useData(() => getMilestones(props.clientModuleId), [props.clientModuleId]) const [busyKey, setBusyKey] = useState('') const [adding, setAdding] = useState(false) const [label, setLabel] = useState('') const toggle = (m: Milestone, done: boolean, doneOn?: string) => { if (busyKey !== '') return setBusyKey(m.key) setMilestone(props.clientModuleId, m.key, done, doneOn) .then(() => list.reload()).catch((e: Error) => toast.err(e.message)).finally(() => setBusyKey('')) } const addStep = () => { if (label.trim() === '') return addMilestone(props.clientModuleId, label.trim()) .then(() => { setLabel(''); setAdding(false); list.reload() }).catch((e: Error) => toast.err(e.message)) } const ms = list.data const done = ms?.filter((m) => m.done).length ?? 0 // current = first not-done const currentKey = ms?.find((m) => !m.done)?.key return (
{props.name} — status line {ms !== undefined && ms.length > 0 && {done}/{ms.length}} {!adding ? : ( <> setLabel(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') addStep() }} /> )}
{list.error !== undefined ? : ms === undefined ? : ms.length === 0 ? No steps yet. : (
{ms.map((m) => (
{m.label} {m.done ? (m.doneOn ?? '—') : m.key === currentKey ? 'now' : '—'}
))}
)}
) } ``` Update the modules-tab render (line ~316) to use `` and rename the heading if present. - [ ] **Step 3: Payment-step linking (advance/balance)** When a node whose key is `advance_payment` or `balance_payment` is ticked, open a small dialog to pick an existing payment (from the client ledger) or record a new one, then call `setMilestone(cmId, key, true, undefined, paymentId)`. Minimum viable: a dialog listing `ledger.payments` (date + amount) to pick from, plus a "Record new payment" shortcut that switches to the Payments tab. Wire the dialog state in `ClientDetail` and pass an `onTickPayment(key)` callback into `StatusLine` for those two keys. (If a lighter first cut is preferred, tick normally with today's date and leave linking as a follow-up — but the spec calls for the link; implement the picker.) - [ ] **Step 4: Verify** Run: `npm run typecheck && npm run build --workspace @sims/hq-web`. In-app: tick along the SMS line → dots fill, current ring moves, ticking Installation flips the header status to `installed`, ticking Go-live → `live` (backend Task 3); line scrolls on a narrow window; ticking Advance payment prompts to link a payment. - [ ] **Step 5: Commit** ```bash git add apps/hq-web/src/pages/ClientDetail.tsx apps/hq-web/src/api.ts git commit -m "feat(client-detail): onboarding as horizontal status line with payment-step linking" ``` --- ## Task 4: Per-module Documents + Bulk SMS purchase **Files:** - Modify: `apps/hq-web/src/pages/ClientDetail.tsx` — add `ModuleDocuments`; render inside each module block after `StatusLine`. - Modify: `apps/hq-web/src/api.ts` — add `generateBulkSmsPurchase(cmId, smsQty)`. **Interfaces:** - Consumes: `ledger.data.documents` (each `Doc.payload.lines[].itemId === moduleId`), `doc.source` (`module_renewal`/`bulk_sms_topup`), `nav`. - Produces: `ModuleDocuments` component; a Bulk SMS action for SMS-coded modules. - [ ] **Step 1: Add the web API call** In `apps/hq-web/src/api.ts`: ```ts export const generateBulkSmsPurchase = (clientModuleId: string, smsQty: number) => post<{ document: Doc }>(`/client-modules/${clientModuleId}/bulk-sms-quote`, { smsQty }).then((r) => r.document) ``` (Match the existing `post`/typed-call idiom in the file.) - [ ] **Step 2: Write `ModuleDocuments`** ```tsx const DOC_SOURCE_LABEL: Record = { module_renewal: 'Renewal', bulk_sms_topup: 'Bulk top-up', } function docLabel(d: Doc): string { if (DOC_SOURCE_LABEL[d.source] !== undefined) return DOC_SOURCE_LABEL[d.source]! return DOC_TYPE_LABEL[d.docType] ?? d.docType } function ModuleDocuments(props: { cm: ClientModule; docs: Doc[]; moduleCode: string; onCreated: () => void }) { const nav = useNavigate() const toast = useToast() const [busy, setBusy] = useState(false) const mine = props.docs .filter((d) => d.payload.lines.some((l) => l.itemId === props.cm.moduleId)) .sort((a, b) => b.docDate.localeCompare(a.docDate)) const isSms = props.moduleCode.toUpperCase() === 'SMS' const bulk = () => { const qty = Number(window.prompt('Bulk SMS quantity to purchase?', '100000') ?? '') if (!Number.isInteger(qty) || qty <= 0) return setBusy(true) generateBulkSmsPurchase(props.cm.id, qty) .then((doc) => { toast.ok('Bulk SMS proforma created'); nav(`/documents/${doc.id}`) }) .catch((e: Error) => { toast.err(e.message); setBusy(false) }) } return (
Documents {mine.length} {isSms && }
{mine.length === 0 ?
No documents for this module yet.
: mine.map((d) => (
nav(`/documents/${d.id}`)} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '7px 0', borderTop: '1px solid var(--border)', cursor: 'pointer', fontSize: 12.5 }}> {d.docNo ?? 'draft'} {docLabel(d)} {d.docDate} {inr(d.payablePaise)} {d.status}
))}
) } ``` - [ ] **Step 3: Render it in each module block** In the modules-tab `.map` (~300–319), inside `.mod-section-body`, after `` add: ```tsx m.id === cm.moduleId)?.code ?? ''} onCreated={() => ledger.reload()} /> ``` - [ ] **Step 4: Verify** Run: `npm run typecheck && npm run build --workspace @sims/hq-web`. In-app: SMS block lists its quotes/proformas with correct pills (Renewal / Bulk top-up / Quote); "Bulk SMS purchase" creates a proforma and opens it; it then appears in the list and (Task 5) in Outstanding. - [ ] **Step 5: Commit** ```bash git add apps/hq-web/src/pages/ClientDetail.tsx apps/hq-web/src/api.ts git commit -m "feat(client-detail): per-module documents list + bulk SMS purchase action" ``` --- ## Task 5: Payments tab — Outstanding panel + Settled column **Files:** - Modify: `apps/hq-web/src/pages/ClientDetail.tsx` — Payments tab (~398–475). **Interfaces:** - Consumes: `ledger.data.outstanding`, `ledger.data.payments[].allocations` (backend Task 6). - [ ] **Step 1: Add the Outstanding panel** At the top of the `tab === 'payments'` block, before ``, render: ```tsx {ledger.data !== undefined && ledger.data.outstanding.length > 0 && (

Outstanding {inr(ledger.data.outstanding.reduce((s, o) => s + o.outstandingPaise, 0))} due

Open documents — oldest settles first
{ledger.data.outstanding.map((o) => (
nav(`/documents/${o.docId}`)} style={{ cursor: 'pointer' }}> {o.docNo ?? 'draft'} {DOC_TYPE_LABEL[o.docType as keyof typeof DOC_TYPE_LABEL] ?? o.docType} {o.label} {inr(o.outstandingPaise)}
))}
Advance on account {formatINR(ledger.data.advancePaise)}
)} ``` - [ ] **Step 2: Add the Settled column to the payments table** In the payments `DataTable` (~407), add a `settled` column between `ref` and `amount`: ```tsx { key: 'settled', label: 'Settled' }, // in the row map: settled: p.allocations.length === 0 ? advance : → {p.allocations.map((a) => a.docNo).join(', ')}, ``` - [ ] **Step 3: Verify** Run: `npm run typecheck && npm run build --workspace @sims/hq-web`. In-app: Payments tab shows Outstanding with correct due totals + advance; each payment shows the doc(s) it settled or "advance"; recording a payment updates both. - [ ] **Step 4: Commit** ```bash git add apps/hq-web/src/pages/ClientDetail.tsx git commit -m "feat(payments): outstanding panel + settled-to-document column" ``` --- ## Task 6: Summary-table trim + "Add a module" **Files:** - Modify: `apps/hq-web/src/pages/ClientDetail.tsx` (modules summary table ~275–298) - Modify: `apps/hq-web/src/pages/client-forms.tsx` (`AssignModuleForm` labels/copy ~285+) **Interfaces:** none new. - [ ] **Step 1: Trim the summary table** Change the summary `DataTable` columns to `Module · Kind · Status · Billed · Settled` (drop `installed`/`completed`/`trained`; dates now live in the status line). Keep `status` as the `ClientModuleStatusSelect` (still editable), keep Billed/Settled numeric/right-aligned. - [ ] **Step 2: Reword the assign form** In `client-forms.tsx` `AssignModuleForm`: change the button/heading text from "Assign module" to **"Add a module"**, and add a one-line hint: `Starts the module at "quoted" and opens its onboarding status line.` - [ ] **Step 3: Verify + commit** Run: `npm run typecheck && npm run build --workspace @sims/hq-web`. In-app: summary table is tighter and aligned; "Add a module" reads right. ```bash git add apps/hq-web/src/pages/ClientDetail.tsx apps/hq-web/src/pages/client-forms.tsx git commit -m "feat(client-detail): trim module summary table; reword assign as 'Add a module'" ``` --- ## Task 7: Per-module onboarding template editor (Modules page) **Files:** - Modify: `apps/hq-web/src/pages/Modules.tsx` - Modify: `apps/hq-web/src/api.ts` — `getModuleTemplate(code)`, `setModuleTemplate(code, steps)`. - Modify: `apps/hq/src/api.ts` — GET/PUT routes for `project.milestone_template:` (audited). **Interfaces:** - Produces: `getModuleTemplate(code): Promise<{ key: string; label: string }[]>`, `setModuleTemplate(code, steps): Promise`. - [ ] **Step 1: Backend routes** In `apps/hq/src/api.ts` add owner-only routes: - `GET /modules/:code/onboarding-template` → `milestoneTemplate(db, code)`. - `PUT /modules/:code/onboarding-template` → validate `steps: {key,label}[]` (non-empty labels; unique keys; slugify missing keys), write the `setting` row `project.milestone_template:`, audited. - [ ] **Step 2: Web calls + editor UI** Add the two api calls, then in `Modules.tsx` add an "Onboarding steps" editor per module row (owner-only): a list of `{label}` rows with add/remove/reorder, Save calls `setModuleTemplate`. Keys auto-derive from labels on save (server slugifies). Note in the UI that changes reach existing projects additively (new steps appear; existing ticks untouched) and that a full re-order/rename for existing projects needs the migration script. - [ ] **Step 3: Verify + commit** Run: `npm run typecheck && npm test --workspace @sims/hq && npm run build --workspace @sims/hq-web`. In-app (owner): edit SMS steps, save, reload a client's SMS block → new step appears. ```bash git add apps/hq-web/src/pages/Modules.tsx apps/hq-web/src/api.ts apps/hq/src/api.ts git commit -m "feat(modules): owner editor for per-module onboarding templates" ``` --- ## Task 8: Stalled-onboarding dashboard tile **Files:** - Modify: `apps/hq-web/src/api.ts` — `getStalledProjects()`. - Modify: `apps/hq-web/src/pages/Dashboard.tsx` — a tile listing stalled projects. **Interfaces:** - Consumes: `GET /projects/stalled` (backend Task 8) → `{ clientModuleId, clientId, clientName, moduleCode, moduleName }[]`. - [ ] **Step 1: Web call + tile** Add `getStalledProjects`. In `Dashboard.tsx`, add a read-only card "Onboarding stalled" listing each stalled project as a link to `/clients/:clientId?tab=modules`, matching the existing dashboard card pattern (`.dash-card`/`.dash-row`). Empty → hide the card or show a positive empty state. - [ ] **Step 2: Verify + commit** Run: `npm run typecheck && npm run build --workspace @sims/hq-web`. In-app: a project parked past the threshold appears; clicking opens its Modules tab. ```bash git add apps/hq-web/src/api.ts apps/hq-web/src/pages/Dashboard.tsx git commit -m "feat(dashboard): stalled-onboarding tile" ``` --- ## Task 9: Responsive + red-team alignment pass (landing gate) **Files:** any of the above, as fixes require. This task does not land new features — it hardens what Tasks 1–8 built. **Do not skip.** - [ ] **Step 1: Build + serve the real app** ```bash npm run build --workspace @sims/hq node apps/hq/dist/server.cjs # serves the built UI on :5182 ``` Seed demo data if needed (`apps/hq/scripts/demo-seed.ts`) so a client has SMS + RTGS + Mobile App, several documents, payments, and mid-onboarding milestones. - [ ] **Step 2: Adversarial visual sweep (use the QA/design tooling)** Use the browser QA tooling (the `browse` / `qa` / `design-review` skills, or chrome-devtools MCP) to load `/clients/:id?tab=modules` and `?tab=payments` at **375px, 768px, and desktop**, in **light and dark** and at least two neutral palettes (Warm + Graphite). Actively hunt for: - 50/50 sheet: collapses to one column < 720px; labels never clip values; nothing runs together on any width. - Status line: scrolls horizontally with no clipped nodes/labels; dots stay on the connector; the "now" ring isn't cut at the edge. - Outstanding panel / payments table / summary table / documents: each scrolls inside its own container; **page body never scrolls sideways**; numeric columns right-aligned with tabular figures. - No overlap, no collapsed/again-crowded rows, no theme-contrast failures (labels/badges legible on every ground). - [ ] **Step 3: Fix every finding, then re-check** Fix in the relevant file, rebuild, and re-run Step 2 until the sweep is clean at all three widths × both themes. Record the widths/themes checked in the commit body. - [ ] **Step 4: Full green + commit** ```bash npm run typecheck && npm test --workspace @sims/hq && npm run build --workspace @sims/hq-web git add -A git commit -m "fix(client-detail): responsive + red-team alignment pass across widths and themes" ``` --- ## Frontend plan self-review - **Spec coverage:** §1 50/50 report → F2; §2 status line → F3; §4 per-module docs → F4; §4a bulk SMS → F4; §5 payments outstanding/settled → F5; §6 summary+assign → F6; §2 template editor → F7; §7.4 stalled tile → F8; responsive+red-team gate → F9; CSS foundation → F1. §7.1 (status auto-drive) and §7.2 (payment link) are backend-driven; F3 consumes them and adds the payment picker UI. - **Type consistency:** class names in F1 (`cdr-split`, `cdr-kv`, `cdr-track`/`cdr-node`, `cdr-out`, `doc-pill`) match the JSX in F2–F5; `generateBulkSmsPurchase`, `getStalledProjects`, `getModuleTemplate`/`setModuleTemplate`, `setMilestone(..., paymentId?)`, `ledger.outstanding`, `payment.allocations` match the backend plan's exports. - **Placeholder scan:** F3 Step 3 and F7 Step 2 describe interactive UI at component level with the exact calls to make; the payment-picker dialog and the template-editor rows follow the file's existing dialog/list idioms (e.g. `BranchList`, `PlanDialog`) — the implementer mirrors those concrete patterns rather than inventing new ones. - **Ordering:** F1 (CSS) first so F2–F5 render correctly; F9 last as the gate. Each task is independently reviewable and ends green.