offplan online
Plan · sales-app-react · CONV-30 · council-reviewed

Riviera buyer journey.

Full module sequence for the React track at os/design-system/sales-app/.

Land Nadezhda's IA in the React scaffold. Ten composing modules, one fixture grounded in real Mered + Herzog & de Meuron content, one Zustand store wired for the multi-app future, one navigable demo at /projects/riviera, one written CRM-CONTRACT.md covering ADGM compliance.

Status
Ratified · 2026-05-09
Owner
Roman + Claude
Stack
Next 16 · React 19 · Tailwind v4 · Zustand · Pannellum
Phases
5 sequential + 1 parallel brand track
Workstreams
6 · all P0

§ 01Goal

Land the full Nadezhda buyer-journey IA in the sales-app-react scaffold (os/design-system/sales-app/) — implement the 5 stub feature modules + add 7 missing IA modules + compose them into a navigable /projects/riviera page demo, with all content sourced from a single TypeScript fixture grounded in real Riviera Residences marketing material, all state through a Zustand store wired for multi-app future scale, and a written CRM-CONTRACT.md spec covering ADGM/PDPL compliance.

§ 02Approach (chosen)

Foundation-first, with parallel brand-language track + module-by-module design loop driven by Roman.

Phase 1 lands the load-bearing scaffold (types, Riviera fixture, Pannellum wrapper, Zustand store, design-system helpers) with no visible UI changes. None of it depends on accent colour, logo, or visual register — so it runs in parallel with the separate brand-language-and-identity workstream that resolves the unsettled visual language, logo, digital elements, and tone-of-voice. Phase 2 onwards waits for both — except brand can ship a minimum-viable-brief subset (palette + typography + status palette + accent system + density baseline) to unblock Phase 2; remaining dimensions can land later.

Every subsequent phase is pure composition on top of Phase 1's scaffold + the brand workstream's locked visual register. The state primitive (Zustand) is chosen for the multi-app future (buyer-app + admin-panel + broker-portal) rather than scoped to v1's ~12 modules.

Per-module design loop (Roman drives heavily)

Roman's directive: deep design involvement, module-by-module. Each feature module (Phases 2–5) follows this 4-step loop instead of going straight to code:

  1. Brief (5–10 min). Claude proposes what's on the screen, what's NOT on it, why. Roman reacts: add/remove/reorder info. Reference the 8-dimension design-brief from the brand workstream as shared vocabulary.
  2. Sketch or stub — routed by module type:
    • Photo-heavy modules (exterior-360, hotspot, poi-detail, layout-detail, saved-units, welcome, project-tour, buyer-profile chip, BuyerProfileSummary): skip wireframes. Real HTML stub or Storybook page with real assets.
    • Density modules (floorplate-selection filter rail, unit-detail price+info hierarchy, BuyerProfileCaptureModal form, PresenterBar): use /wireframe to produce 2–3 lo-fi variants. Roman picks/refines. Then code.
  3. Implement. Claude writes the React module against the chosen direction.
  4. Live iteration. Roman opens it in browser. Uses /inspect to point at specific elements. Optionally /make-tweakable for live colour/spacing/font sliders. Loop until Roman says good.

Per-module PAUSE checkboxes in workstream task lists make these /build-enforceable. Phase-end sign-off gates require Roman to walk all modules of the just-finished phase together before next phase starts. /handoff at a phase boundary writes Gate-passed: phase-N as evidence trail.

§ 03Inputs

Canonical

  1. Real Riviera content — Mered + Herzog & de Meuron, Al Reem Island Abu Dhabi (Oval Bay), 468 apartments + ocean villas + sky villas, 1BR ~700sqft from 2.1M AED, 2BR from 3M, 3BR from 5M, 60/40 payment plan, Q1 2029 handover, mother-of-pearl façade, bay-window signature rooms, podium amenities + sky garden, 4 pools / padel / spa / family zone / promenade.
  2. Locked visual register from brand-language-and-identity workstreamos/docs/design/visual-register.md + 8-dimension brief + final wordmark/icon system + photographic register + motion principles + status palette + tone-of-voice. Primary visual input, not the bundle directly. The bundle (os/design-system/bundle/) is the brand workstream's starting point, not its output.
  3. Existing scaffold conventions (TopBar.tsx + InfoPanel.tsx + globals.css @theme inline + per-project [data-project] cascade + Storybook decorator).
  4. Existing imagery: public/imagery/imagery/riviera/ (1 real 360° + 12 flat renders), public/imagery/floorplans/ (4 files).
  5. Path 1 reference: os/design-system/sales/02-exterior.html (Pannellum + scrim + grid topbar pattern; reference only, do not port code 1:1).
  6. Audit findings: os/docs/audit/SUMMARY.md.
  7. Nadezhda transcript: os/docs/research/client-feedback-nadezhda-2026-05-08.md.
  8. Roman's incoming interior 360° walkthrough panos — provided post-plan-ship; brief = equirectangular JPG/PNG, 4K min, sRGB colour space.

NOT canonical — explicitly skipped

§ 04Out of scope

§ 05Implementation Steps

Five sequential phases. Each ships a coherent deliverable. Per-module 4-step design loop (Phases 2–5) with PAUSE checkboxes in workstream task lists. Phase-end sign-off gates with /handoff evidence record.

Phase 1 · Foundation

Types, fixture, Zustand store, Pannellum wrapper, design-system helpers

Adds the load-bearing scaffold every later phase consumes. No UI changes visible to a user opening the app, but every subsequent phase is pure composition on top. Runs in parallel with the brand-language-and-identity workstream.

Tasks

  1. Extend src/types/index.ts — add Project, Building, Floor, Floorplate, Layout, Unit, PriceBand, Amenity, Architect, Developer, PaymentPlan, TourNode. UnitStatus and Aspect exported as discriminated unions (UnitStatus = 'available' | 'reserved' | 'sold' | 'on-hold' | 'anonymised'; Aspect = 'sea' | 'city' | 'lagoon' | 'pool'). <StatusPill> uses an exhaustive Record<UnitStatus, OxidisedToken> — TS errors if a status is added without a colour. Extend BuyerProfile with savedUnits: string[], lookupId?: string, mode: 'buyer' | 'broker-shortlist'.
  2. Create src/data/riviera.ts — Riviera Project value populated with content from real Mered + H&dM source material. ~30-unit subset with realistic prices/aspects/statuses. 4 floorplates (l2 + l6 real, 2 stub-tagged). 2 layouts. 6 POIs against riviera/360-aerial.jpg. tourNodes[] graph (only aerial activated; interior nodes structurally complete with panoramaUrl undefined — placeholders pending Roman's incoming files). Each voice-bearing string tagged [copy:provisional]; factual strings [copy:locked]. Project-level flags: welcomeDefault, demoMode: true, anonymiseStatuses: false. Single source of truth for all module content.
  3. Create src/lib/pannellum.tsx'use client' <Panorama> wrapper, dynamic import inside useEffect. Full anticipated prop surface from day one: panoramaUrl, panoramaType? (default 'equirect'), pois?, onPoiClick?, onSceneChange?, autoLoad?, compass?, loading?: ReactNode slot (Skeleton-White scrim with progress shimmer), onLoad?. Hide default Pannellum UI. Forward data-testid.
  4. Create src/lib/pannellum.stories.tsx — stories: Default, WithLoadingSlot, WithHotspots, WithFallback. Storybook 10/Vite fallback = next/dynamic in iframe story (cheap). Canvas polyfill is OUT — moves to follow-on sales-app-react-pannellum-spike if Roman wants. Phase 1 exit gate: Next dev /projects/riviera/exterior works in browser.
  5. Create src/lib/buyer-profile-store.tsZustand store with persist middleware. Add zustand (~3kB) + zod to deps. Persist config: { name: 'oo:buyer-profile:v1', version: 1, partialize, migrate, onRehydrateStorage }. Zod schema validates rehydrated shape; on mismatch, clear key + start anonymous. Hook useBuyerProfile() returns selected slice. RootLayout does NOT wrap in Provider — Zustand stores are global.
  6. Create src/lib/format.tsformatAed, formatArea. Consume Zustand store's currency + units slices.
  7. Create src/lib/asset-fallback.tsx<AssetOrPlaceholder src alt kind="render|panorama">. Skeleton-White card, Mono small caps. Render → "Render available on request"; panorama → "Interior walkthrough — available on request from your advisor". Same panorama copy reused in PoiDetail disabled-CTA tooltips.
  8. Create src/data/index.tsgetProject(slug): Project | null + <MissingProjectFallback>. Storybook decorator routes through getProject(toolbarSlug) so cote/velvet show the fallback rather than Riviera-content-under-cote-accent.
  9. Extract src/components/brand/Wordmark.tsx — single component the brand workstream replaces wholesale. TopBar consumes <Wordmark />, never inline SVG.
  10. Configure ESLint rule banning bare string literals >30 chars in src/features/** and src/components/**. Pair with Vitest assertion that fixture token (e.g. "mother-of-pearl") appears in rendered output.
  11. Configure Vitest scripts: "test", "test:storybook". Convention: *.test.tsx co-located.
  12. Configure Storybook story title taxonomy: Modules/<PascalGroup>/<ComponentName>. Document in AGENTS.md.
Eval pack
FieldValue
Artefact(a) Storybook URL ?path=/story/foundation-pannellum--default. (b) git diff of types + fixture. (c) package.json shows zustand + zod.
2-min checkOpen Storybook, drag panorama, wc -l src/data/riviera.ts >200 lines.
Self-checktsc --noEmit 0 errors. Smoke passes (incl. corrupt-JSON Zustand recovery). Gate-passed: phase-1.
Phase 2 · Anchor

exterior-360 + hotspot + poi-detail + minimal route

The screen Nadezhda's IA pivots on. Two-stage page build — Phase 2 ships a minimal Server Component route mounting <Exterior360> directly so phase-end sign-off is in dev. Phase 5 enriches it with JourneyShell. All three modules photo-heavy → skip wireframes.

Tasks

  1. src/features/hotspot/Hotspot.tsx — small clickable marker at Pannellum hotspot pixel position. Two states: 'in' (interior building parts) vs 'out' (surrounding POIs). Hover → opacity lift + shadcn Tooltip label.
  2. src/features/poi-detail/PoiDetail.tsx — slide-in detail (shadcn Sheet, w=420). Large image with <AssetOrPlaceholder> fallback. Disabled-CTA tooltip copy: "Interior walkthrough — available on request from your advisor".
  3. src/features/exterior-360/Exterior360.tsx — composes <Panorama> (with Phase 1 loading slot) + scrim gradient overlay + slot for <TopBar> + slot for <InfoPanel>.
  4. src/app/projects/[slug]/page.tsx (NEW minimal version) — Server Component, const { slug } = await params. getProject(slug). Mounts <Exterior360> directly. Cote/velvet 404 with <MissingProjectFallback>.
Eval pack
FieldValue
Artefact(a) Storybook combined story URL. (b) localhost:3000/projects/riviera walkable. (c) Screenshot of panorama + 6 hotspots + open PoiDetail.
2-min checkRoman drags 360°, clicks 2 hotspots, sees PoiDetail content match Riviera fixture, visual register matches brand-ratified register.
Self-checkAll three module stories load. Vitest smoke passes. Gate-passed: phase-2.
Phase 3 · Selection

floorplate-selection + layout-detail + unit-detail + status pill

The floorplate-first buyer flow Nadezhda demands — replaces dot-on-façade IA with filter-by-shape. FloorplateSelection + UnitDetail are density modules → use /wireframe first. LayoutDetail is photo-heavy → skip wireframe.

Tasks

  1. src/components/status-pill.tsx — consumes --color-status-* token NAMES only. Sole consumer — CI grep guard enforces. Used across all status-rendering modules.
  2. FloorplateSelection (density — wireframe first): Warm Stone surface. Filter rail: beds, baths, building (chip surfaced when Project has >1 — type-ready for Nadezhda's 15-building scale), aspect, budget. Layout grid as cards. Empty state: when filters return zero, render Warm-Stone empty card + Reset link + "Talk to a sales advisor" CTA wired to BuyerProfile chip.
  3. LayoutDetail (photo-heavy): large layout plan image + floor list with units + <StatusPill> chips.
  4. UnitDetail (density — wireframe first): unit code (Mono), price (display), bed/bath/aspect chips, "view from window" via <AssetOrPlaceholder>, bay-window-rooms note, payment plan. "♡ Save to my profile" — first-save NO modal. If mode === 'broker-shortlist': session shortlist + 3-second undo toast. Else (anonymous OR identified): addSavedUnit(unitId) + passive toast "Saved. Add your details to keep these across devices →". Modal triggers only via chip click / Email-offer CTA / 2nd anonymous save.
Eval pack
FieldValue
Artefact3 module URLs + screenshot showing filter → grid → unit detail with NO modal on first save (toast only).
2-min checkToggle bed-count chips → grid filters live. Click 2BR layout → floors. Click unit → UnitDetail. Click ♡ → toast (no modal). Empty filter shows the empty card with CTA.
Self-checkVitest filter logic + first-save no-modal flow. Status pills oxidised (JSDOM). Gate-passed: phase-3.
Phase 4 · Capture

buyer-profile chip + capture modal + summary + saved-units + offer summary

The missing-CTA layer the audit flagged. BuyerProfileCaptureModal is a density module → wireframe form-field hierarchy first. Server-side lookupId + magic-link cross-device persistence (replaces localStorage-only).

Tasks

  1. BuyerProfile chip — top-right TopBar slot. Anonymous (Save my session button) ↔ Identified (avatar + name + "(N saved)" badge). Subscribes via fine-grained Zustand selector.
  2. TopBar integration — right slot + sticky price chip when ?stage=unit active ("B-1402 · 2BR · AED 2.6M · Sea") + Reset-demo affordance + scrim-contrast verify against the chip.
  3. BuyerProfileCaptureModal (density — wireframe first): shadcn Dialog. firstname required, email required + HTML5 validation + positive cases (name+tag@domain.co.uk, a@b.co). Consent checkbox + privacy link required (ADGM compliance). On submit: localStorage write FIRST (Zustand persist), THEN fire-and-forget POST with 3s AbortController timeout. Stub returns { ok, lookupId, magicLinkSent }. Store lookupId in URL + Zustand for cross-device restore.
  4. BuyerProfileSummary — shadcn Sheet. Edit pencil + saved-units list (StatusPill) + "Generate offer summary →" CTA + "Clear my session" destructive action.
  5. SavedUnits screen — full-screen module rendered by /projects/[slug]/saved. Subscribed via useBuyerProfile(s => s.savedUnits). Grouped by status. Footer: "Email me this offer" → OfferSummary.
  6. OfferSummary artifact (NEW) — print-stylesheet/PDF-friendly. Broker name + project header + per-unit snapshot. Single-click Email / Download PDF / Copy shareable link. TS type in CRM-CONTRACT.md §b §f.
Eval pack
FieldValue
Artefact4 buyer-profile module URLs + saved-units + offer-summary + identified-chip with sticky price screenshot.
2-min checkChip + summary, capture-modal fill+submit, refresh persists, OfferSummary PDF renders, ?presenter=1 activates broker-shortlist mode (no PII modal during demo).
Self-checkValidation tests pass (incl. positive email cases). Cross-module test: save from UnitDetail → chip badge increments. Gate-passed: phase-4.
Phase 5 · Wrap

welcome + project-tour + JourneyShell + PresenterBar + API stub + CRM-CONTRACT.md + smoke

Closes the journey. JourneyShell + URL state via Server-Component-async-params + Client wrapper + Suspense (canonical Next 16 pattern). Broker walkthrough mode. Full ADGM/PDPL CRM-CONTRACT.md. Visual register reconciliation. Playwright smoke covering happy path + deep-link recovery + presenter mode.

Tasks

  1. JourneyShell + URL state — Server Component awaits params + searchParams, passes to <JourneyShell> Client Component wrapped in <Suspense>. JourneyShell owns stage routing via useSearchParams() + useRouter(). Deep-link validation: searchParams.unit validated against fixture; on miss redirect to ?stage=select with toast. Welcome routing: shows only on first visit + no ?profile=; returning identified buyers hit lookup endpoint and skip Welcome.
  2. Welcome module (photo-heavy) — Skeleton White, hero from fixture, "Skip intro" always visible, respects welcomeDefault.
  3. ProjectTour module (photo-heavy) — multi-node walkthrough reusing <Panorama>. Coverage gating: if real-pano coverage <3/5 nodes, gate behind a "Coming soon — interior walkthrough" full-module placeholder + "Notify me" CTA wired to capture modal. Drop-in for Roman's incoming panos via fixture-only edit.
  4. PresenterBar module (density — wireframe first; NEW) — bottom bar visible when ?presenter=1. Prev/Next stage + label + Jump-to combobox + keyboard arrow nav. "Copy presenter link" action. Sets BuyerProfile mode = 'broker-shortlist'.
  5. API stub (/api/buyer-profile-stub) — POST validates + redacts email/phone before console.log + returns { ok, lookupId, magicLinkSent }. GET ?email=… lookup endpoint. 60-req/min in-memory rate limiter as reference.
  6. /projects/[slug]/saved/page.tsx — sub-route renders <SavedUnits>.
  7. CRM-CONTRACT.md — full spec:
    • §a POST shape + status flow.
    • §b Idempotency on email + OfferSummary TS type.
    • §c Lookup contract (GET /buyer-profile?email=…).
    • §d Saved-units mutation (PATCH /buyer-profile/:lookupId/saved-units).
    • §e Per-session price-snapshot persistence (denormalised).
    • §f Auth: broker token in header.
    • §g CRITICAL ADGM Data Protection Regulations 2021 — separate from federal UAE PDPL, stricter, GDPR-aligned. Lawful basis = explicit consent; consent text in capture modal; data residency: UAE region only; retention TTL: 90 days default if no purchase; privacy policy link in modal.
    • §h CRITICAL Erasure endpoints: DELETE /buyer-profile/:id + DELETE /buyer-profile/by-email.
    • §i Origin allow-list / CSRF / rate-limit / bot mitigation.
    • §j Out-of-v1 list (real-time bi-directional CRM sync deferred).
    • §k v1 marker: subject to revision once backend implementer reviews and UAE legal advisor reviews ADGM/PDPL specifics.
  8. Visual register reconciliation pass — Roman walks all 12+ modules in Storybook side-by-side against os/docs/design/visual-register.md, files diffs as /inspect tweaks, applies via /apply-tweaks. Block phase-complete on this pass.
  9. Playwright smoke — happy path: Welcome → Exterior → Selection → Layout → Unit → Save × 2 (first no modal, second modal with consent) → Saved. Deep-link recovery: ?unit=DOES-NOT-EXIST redirects. Presenter mode: arrow keys navigate stages.
  10. Final AU English sweep as backstop.
Eval pack
FieldValue
Artefact(a) Live /projects/riviera demo. (b) Playwright HTML report. (c) CRM-CONTRACT.md. (d) Screenshot reel including Presenter mode. (e) OfferSummary PDF sample.
2-min checkpnpm dev → walk full Nadezhda flow without dead-ends. Toggle ?presenter=1 → PresenterBar. Open CRM-CONTRACT.md → ADGM/PDPL clauses present.
Self-checkPlaywright smoke passes (3 paths). All 12 module stories load. Visual reconciliation complete. AU English clean. Gate-passed: phase-5 + final demo URL recorded.

§ 06Files to Create / Modify

Phase 1 — Foundation (12 files)
  • src/types/index.ts (modify — additive)
  • src/data/riviera.ts (new)
  • src/data/index.ts (new)
  • src/lib/pannellum.tsx + pannellum.stories.tsx (new)
  • src/lib/buyer-profile-store.ts (new — Zustand)
  • src/lib/format.ts (new)
  • src/lib/asset-fallback.tsx (new)
  • src/components/brand/Wordmark.tsx (new — extracted)
  • eslint.config.mjs (modify)
  • package.json (modify — zustand + zod)
  • AGENTS.md (modify)
Phase 2 — Anchor (4 modules)
  • src/features/hotspot/{Hotspot.tsx,Hotspot.stories.tsx,README.md}
  • src/features/poi-detail/{PoiDetail.tsx,PoiDetail.stories.tsx,README.md}
  • src/features/exterior-360/{Exterior360.tsx,Exterior360.stories.tsx,README.md}
  • src/app/projects/[slug]/page.tsx (new minimal — Phase 5 enriches)
Phase 3 — Selection (4 modules)
  • src/features/floorplate-selection/{FloorplateSelection.tsx,*.stories.tsx,README.md}
  • src/features/layout-detail/{LayoutDetail.tsx,*.stories.tsx,README.md}
  • src/features/unit-detail/{UnitDetail.tsx,*.stories.tsx,README.md}
  • src/components/status-pill.tsx (sole consumer of --color-status-*)
Phase 4 — Capture (5 modules)
  • src/features/buyer-profile/{BuyerProfile.tsx,BuyerProfileCaptureModal.tsx,BuyerProfileSummary.tsx,*.stories.tsx,README.md}
  • src/features/saved-units/{SavedUnits.tsx,OfferSummary.tsx,*.stories.tsx,README.md}
  • src/features/top-bar/TopBar.tsx (modify — right slot, sticky price, reset-demo)
Phase 5 — Wrap (10 files)
  • src/features/welcome/{Welcome.tsx,*.stories.tsx,README.md} (new)
  • src/features/project-tour/{ProjectTour.tsx,*.stories.tsx,README.md} (rewrite stub)
  • src/features/presenter-bar/{PresenterBar.tsx,*.stories.tsx,README.md} (new — broker mode)
  • src/components/journey-shell.tsx (new)
  • src/app/projects/[slug]/page.tsx (modify — extend with JourneyShell)
  • src/app/projects/[slug]/saved/page.tsx (new)
  • src/app/api/buyer-profile-stub/route.ts (new)
  • os/design-system/sales-app/CRM-CONTRACT.md (new)
  • tests/e2e/riviera-journey.spec.ts (new)
  • os/design-system/sales-app/docs/next16-tailwind4-notes.md (modify — URL state in journey section)

§ 07Dependencies

All major already in package.json: Next 16.2.6, React 19.2.4, Tailwind v4.2.4, Pannellum 2.5.7, shadcn primitives, Vitest 4, Playwright.

AddSizeWhy
zustand~3kBCross-app state primitive (buyer-app + admin + broker portals share pattern)
zod~15kBRuntime validation of rehydrated localStorage state inside Zustand onRehydrateStorage

No other new packages without Roman approval.

§ 08Testing & Verification

§ 09Workstreams

NamePriDepends OnTasks
brand-language-and-identity P0 Separate workstream, separate /plan. Resolves visual language, logo/wordmark, icon system, photographic register, motion principles, status palette, micro-interactions, tone-of-voice. Output: os/docs/design/visual-register.md + 8-dim brief. Min-viable subset (palette + typography + status palette + accent system + density baseline) unblocks sales-app-react-anchor. Runs in PARALLEL with sales-app-react-foundation.
sales-app-react-foundation P0 Phase 1. Runs in parallel with brand workstream.
sales-app-react-anchor P0 sales-app-react-foundation, brand-language-and-identity (min-viable subset OK) Phase 2. 4-step design loop applies. Minimal route mounts Exterior360 directly.
sales-app-react-selection P0 sales-app-react-anchor Phase 3. FloorplateSelection + UnitDetail use /wireframe first.
sales-app-react-capture P0 sales-app-react-selection Phase 4. BuyerProfileCaptureModal uses /wireframe first.
sales-app-react-wrap P0 sales-app-react-capture Phase 5. PresenterBar uses /wireframe first.

Six P0 workstreams. brand-language-and-identity + sales-app-react-foundation in parallel; anchor onwards is sequential.

§ 10Risks & Edge Cases

RiskSeverityMitigation
Pannellum dynamic import fails in Storybook 10 (Vite/Rollup)HighPhase 1 fallback = next/dynamic iframe story. Canvas polyfill OUT — moves to follow-on sales-app-react-pannellum-spike if Roman wants. Phase 1 exit gate is Next dev /projects/riviera/exterior, not Storybook.
Brand workstream over-runs Phase 1 / lands incomplete briefMediumMin-viable-brief subset unblocks Phase 2; mood/display/exclusions can land per-module. Fixture content tagged [copy:locked]/[copy:provisional].
Asset placeholders look unfinished to NadezhdaMediumDeliberate copy ("available on request from your advisor"). ProjectTour gates behind "Coming soon" placeholder if <3/5 panos real.
Roman's interior 360°s arrive late / unexpected formatMediumTourNode shape supports drop-in. Brief suppliers: equirectangular JPG/PNG, 4K min, sRGB.
Cross-app state library scalingLowZustand chosen for multi-app future. Sibling slices already in v1; admin/broker portals consume same pattern.
ADGM/PDPL legal review gapHighCRM §g §h cover ADGM DPR 2021 + UAE PDPL. Sonar's citations are weak — separate UAE legal advisor pass before Nadezhda demo recommended.
localStorage collisions on shared browserLowVersioned key oo:buyer-profile:v1. Reset-demo affordance + idle auto-clear when fixture.demoMode: true.
Mid-plan priority interrupt arrivesLowSequential P0 workstreams; Roman pauses between phases. Fixture-first keeps codebase consistent at every boundary.
Sergei's atelier track diverges furtherLowPer feedback_design_inputs.md, atelier NOT canonical. Anchor to bundle + Nadezhda + brand workstream output.
CRM-CONTRACT.md drifts from backendMediumv1 marker "subject to revision". Roman shares with Ilya/backend-team for review before demo.
@base-ui/react component coverage gapLowPhase 1 audit needed components; fall back to Radix-shadcn if a primitive is missing.
Cote/velvet sub-brand mark scopeLowBrand-workstream open question. Storybook decorator's data-project toolbar shows MissingProjectFallback; documented as known visual gap.

§ 11Consolidation

Replaces

Dedup Check

Touch Radius

§ 12Council Review (CONV-30) — Decision Log

Reviewed 2026-05-09 by 10-persona internal council + Sonar Pro 2024–2026 best-practices research + Roman directive on multi-app future scale.

All CRITICAL and HIGH findings have been integrated into the phase descriptions above. This section is the audit trail showing what was found, what was decided, and the rationale.

Disposition tally

SeverityCountStatus
Critical7All integrated into Phase 1–5
High22All integrated into Phase 1–5
Medium7Cross-cutting integrated; remaining noted in Risks
Sonar verdicts82 Aligned · 4 Defensible-with-caveats · 2 Wrong (T4 + T5 — fixed)

Sonar Pro 2024–2026 verdicts (advisory)

#TopicVerdictStatus
1Pannellum 2.5.7 dynamic importDefensiblePhoto Sphere Viewer noted as v2 alternative; stay on Pannellum for v1.
2React state for buyer-profileDefensibleswitchedRoman scale-context override → Zustand. Done.
3Server/Client split for URL stateAlignedJourneyShell pattern. Done.
4localStorage validationWrong → fixedZustand persist + Zod. Done.
5ADGM consent + retentionWrong → fixedCRM §g §h. Done.
6Equirectangular for 360 deliveriesAlignedAsset spec to suppliers documented.
7shadcn/ui on @base-ui/reactDefensibleAudit needed components in Phase 1.
8Tailwind v4 [data-project] cascadeAlignedVerify cascade order in compiled CSS during Phase 1.
Caveat. Sonar Pro returned mostly secondhand blog URLs (flexpressai, strapi.io, dev.to, talent500.com, Vercel intro post) rather than primary docs (Apple developer docs, ADGM legislation, Tailwind docs, Pannellum repo). Substance independently verifiable, but before launch, recommend a separate UAE legal advisor pass on the ADGM DPR 2021 + PDPL specifics in CRM-CONTRACT.md §g §h.
Full decisions ledger (44 amendments)
IDDecisionSourceIn plan
A-SCALE-1Switch from React Context to Zustand for buyer-profile + presentation state.Roman directive + Sonar T2Phase 1 task 5
A-DEVUX-1Per-module 4-step design loop with PAUSE markers becomes /build-enforceable.ConcernApproach + workstream task lists
A-DEVUX-2 / A-SEQ-1brand-language-and-identity needs its own /plan ratification before sales-app-react-foundation Phase 1 task 2; runs in parallel.ConcernApproach + Workstreams
A-DEVUX-3Phase-gate handoff record — /handoff writes Gate-passed: phase-N.ConcernPer-phase eval packs
A-DEVUX-4AU English check at every phase's eval pack.ConcernPer-phase verification
A-RES-3 / S-T4localStorage validation via Zustand persist + version field + Zod rehydrate.Concern + Sonar promotedPhase 1 task 5
A-BUYUX-1First save = anonymous + toast; modal triggered only on chip click / Email-offer / 2nd anonymous save.ConcernPhase 3 task 4 + Phase 4 task 1
A-BUYUX-2Server-side lookupId + magic-link cross-device persistence.ConcernPhase 4 task 3 + Phase 5 task 5
A-BUYUX-3Sticky price chip in TopBar when ?stage=unit active.ConcernPhase 4 task 2
A-BUYUX-4Building filter dimension + Building type for multi-building projects.ConcernPhase 1 task 1 + Phase 3 task 2
A-BUYUX-5Welcome shown only on first-visit + no-?profile=; returning identified buyers skip it.ConcernPhase 5 task 1
A-SEC-1Clear-session UI affordance + Reset-demo + idle-clear when fixture.demoMode: true.ConcernPhase 1 task 2 + Phase 4 tasks 2+4
A-SEC-2 / S-T5ADGM DPR 2021 + UAE PDPL clauses — separate regimes, ADGM stricter, GDPR-aligned.Concern + Sonar promotedPhase 5 task 7 §g §h
A-SEC-3Email validation positive cases (name+tag@domain.co.uk etc.).ConcernPhase 4 task 7
A-SEC-4CRM auth: Origin allow-list + CSRF + rate-limit + bot mitigation.ConcernPhase 5 task 7 §i
A-SEC-5dangerouslySetInnerHTML forbidden in feature modules.ConcernConsolidation §Dedup Check
A-SALESUX-1New PresenterBar module for broker walkthrough mode.ConcernPhase 5 task 4
A-SALESUX-2New OfferSummary artifact (PDF/email/share).ConcernPhase 4 task 6
A-SALESUX-3BuyerProfile mode 'broker-shortlist'; 3-second undo toast.ConcernPhase 1 task 5 + Phase 3 task 4
A-SALESUX-5welcomeDefault per project + ?welcome= URL override.ConcernPhase 1 task 2 + Phase 5
A-IMPL-1 / S-T3Server Component + JourneyShell + Suspense for URL state.Sonar Aligned + ConcernPhase 5 task 1
A-IMPL-2Test convention *.test.tsx co-located.ConcernPhase 1 task 11
A-IMPL-3Storybook story title taxonomy.ConcernPhase 1 task 12
A-IMPL-4UnitStatus + Aspect as discriminated unions.ConcernPhase 1 task 1
A-SEQ-2Two-stage page build: Phase 2 minimal mount; Phase 5 enriches.ConcernPhase 2 task 4 + Phase 5 task 1
A-SEQ-3Phase 4 chip slot integration re-verifies scrim/contrast.ConcernPhase 4 task 2
A-SEQ-4UnitDetail Save action wired correctly from Phase 3 (no temp stub).ConcernPhase 3 task 4
A-RES-1Storybook Pannellum fallback = next/dynamic iframe story (not canvas polyfill).ConcernPhase 1 task 4
A-RES-2ProjectTour gates behind "Coming soon" placeholder if <3/5 panos real.ConcernPhase 5 task 3
A-RES-4JourneyShell validates searchParams against fixture; redirects on miss.ConcernPhase 5 task 1
A-RES-5localStorage write sync-first; POST fire-and-forget with 3s timeout.ConcernPhase 4 task 3
A-DEMO-1Panorama loading slot with Skeleton-White scrim + onLoad event.ConcernPhase 1 task 3
A-DEMO-2Filter empty-state card with Reset + advisor CTA.ConcernPhase 3 task 2
A-DEMO-3Disabled-CTA tooltip copy authored ("Interior walkthrough — available on request from your advisor").ConcernPhase 1 task 7 + reused
A-DEMO-4Visual register reconciliation pass before Playwright smoke.ConcernPhase 5 task 8
A-BRAND-1brand-language-and-identity adds tone-of-voice scope; fixture strings tagged [copy:locked]/[copy:provisional].Concernbrand workstream + Phase 1 task 2
A-BRAND-2StatusPill via token names only (no hex literals).ConcernPhase 3 task 1
A-BRAND-3Min-viable-brief subset gates Phase 2.ConcernWorkstreams + Risks
A-BRAND-4Wordmark.tsx extracted as swap point.ConcernPhase 1 task 9
A-BRAND-5Cote/velvet sub-brand mark scope is open question.Concernbrand workstream + Risks
A-HEALTH-1ESLint bare-string rule + Vitest fixture-token assertion.ConcernPhase 1 task 10
A-HEALTH-2Pannellum wrapper ships full prop surface in Phase 1.ConcernPhase 1 task 3
A-HEALTH-3savedUnits hydration default ?? [].ConcernPhase 1 task 5
A-HEALTH-4StatusPill is sole consumer of --color-status-*.ConcernPhase 3 task 1
A-HEALTH-5getProject + MissingProjectFallback for non-Riviera Storybook decorator.ConcernPhase 1 task 8