[object Object]

← back to Dw War Room

initial scaffold (gitify-all 2026-05-06)

8d94312193fa4c7169a37a7fc2ae0f5501e54eb9 · 2026-05-06 10:25:17 -0700 · Steve Abrams

Files touched

Diff

commit 8d94312193fa4c7169a37a7fc2ae0f5501e54eb9
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed May 6 10:25:17 2026 -0700

    initial scaffold (gitify-all 2026-05-06)
---
 .gitignore                                         |   12 +
 CLAUDE.md                                          |  164 ++
 ...2026-03-02-figma-threejs-photorealism-design.md |  167 ++
 .../2026-03-02-figma-threejs-photorealism-plan.md  | 1623 ++++++++++++++++++++
 ecosystem.config.cjs                               |   13 +
 index.html                                         |   30 +
 package-lock.json                                  | 1186 ++++++++++++++
 package.json                                       |   19 +
 serve.cjs                                          |  185 +++
 showroom.html                                      |   17 +
 src/data/ApiClient.ts                              |   78 +
 src/data/WebSocketClient.ts                        |   75 +
 src/effects/Particles.ts                           |  308 ++++
 src/effects/PostProcessing.ts                      |  193 +++
 src/main.ts                                        |  237 +++
 src/panels/HudOverlay.ts                           |  897 +++++++++++
 src/scene/AgentNodes.ts                            |  256 +++
 src/scene/CommandTable.ts                          |  270 ++++
 src/scene/DataWalls.ts                             |  387 +++++
 src/scene/Environment.ts                           |  183 +++
 src/scene/Lighting.ts                              |   60 +
 src/scene/WarRoom.ts                               |  198 +++
 src/showroom/FigmaTransformer.ts                   |  128 ++
 src/showroom/ProceduralMaps.ts                     |  188 +++
 src/showroom/SearchOverlay.ts                      |  732 +++++++++
 src/showroom/Showroom.ts                           |  206 +++
 src/showroom/ShowroomEnvironment.ts                |   93 ++
 src/showroom/ShowroomLayout.ts                     |  343 +++++
 src/showroom/ShowroomMaterials.ts                  |  104 ++
 src/showroom/ShowroomPostProcess.ts                |  181 +++
 src/showroom/TextureCrop.ts                        |  159 ++
 src/showroom/index.ts                              |   50 +
 src/showroom/types.ts                              |   50 +
 src/types.ts                                       |   84 +
 tsconfig.json                                      |   18 +
 vite.config.ts                                     |  107 ++
 36 files changed, 9001 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d80acea
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+node_modules/
+dist/
+.env
+.env.local
+.env.*.local
+.env.*
+tmp/
+*.log
+.DS_Store
+build/
+.next/
+*.bak
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..a3c4c61
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,164 @@
+# 3D War Room — Room Architecture & Rendering Specs
+
+## Build System
+- Source modules in `/public/js/modules/` (15 MAIN + 1 STANDALONE)
+- Build: `node build-warroom.js` → concatenates into single IIFE `warroom.js` (~24,810 lines)
+- Validate: `node validate-warroom.js [--verbose] [--json]` — 20 checks across build/navigation/culling/scene/css/performance
+- Module order: wr-core → wr-audio → wr-ui → wr-scene → wr-animate → wr-effects → wr-effects2 → wr-effects3 → wr-effects4 → wr-office → wr-rooms → wr-doors → wr-meeting → wr-nav → wr-lincoln → (standalone) wr-wallpicker
+- HTML loads ONLY `/js/warroom.js?v=251` — modular files are NOT loaded by browser
+- **After ANY code change**: edit module → `node build-warroom.js` → `node validate-warroom.js` → bump cache version in index.html
+- Global state exposed via `window._*` pattern (e.g., `window._camera`, `window._controls`)
+- Variable names MUST be room-prefixed to avoid IIFE scope collisions (e.g., `djVinyl` not `vinyl`)
+
+---
+
+## Boardroom (Main Conference Room)
+- **Dimensions**: 24w x 18d x 4.2h (ROOM_W=24, ROOM_D=18, WALL_H=4.2)
+- **Table**: center (0, 0.95, 0), 10w x 3.2d x 0.95h
+- **Podium**: (-8.5, 0, 0), facing table (rotation Math.PI/2)
+- **Back wall**: (0, 2.1, -9), 24w x 4.2h
+- **Front wall**: (0, 2.1, 9), 24w x 4.2h — has 5 transparisteel windows
+- **Left wall**: (-12, 2.1, 0), 18d x 4.2h
+- **Right wall**: (12, 2.1, 0), 18d x 4.2h
+- **Seating**: 50 positions (16 at table, 2 head/foot, 20 back rows, 12 side walls)
+- **Executives only**: EXEC_IDS = Set(['ceo', 'cfo', 'coo', 'cto', 'vp-ops'])
+
+## Garcia Room
+- **Center**: (0, 0, -42), width 14, depth 10, height 4.2
+- **Front wall**: z = -37 (guitar at x=3.5, SYF at x=-3.5, volume slider at x=5.5)
+- **Back wall**: z = -47 (neon GARCIA sign, NOW PLAYING display, SYF banner)
+- **Left wall**: x = -7 (Puppy TV — full wall screen)
+- **Right wall**: x = +7 (Met Museum art)
+- **Turntable**: (3.5, 1.0, -44.5) with GD/JGB/G&G collection buttons below
+- **Eames chair**: (0, 0, -41)
+- **Camera start**: (0, 1.6, -46) looking at (0, 1.8, -40) — back of room facing forward
+- **Key globals**: `window._garciaHitbox`, `window._garciaGuitar`, `window._garciaVolumeSlider`, `window._garciaSYF`, `window._garciaJukebox`, `window._garciaNowPlayingCanvas`
+
+## Showroom / Gallery Wings
+- **Showroom**: cam (22, 2, -20), center (22, 0, -22)
+- **Gallery 2**: cam (42, 2, -20), center (42, 0, -22)
+- **Gallery 3**: cam (62, 2, -20), center (62, 0, -22)
+- **Lazy loaded**: Products built on first navigation visit
+- **Culling**: `_setShowroomVisibility(roomIdx)` hides distant gallery boards
+
+## MET Gallery
+- **Center**: (0, 0, -22), width 14, depth 8, height 4.2
+- **Location**: Between breakout rooms corridor and Garcia
+- **Features**: 100 contemporary paintings (procedural canvas textures), museum bench, black grasscloth walls
+- **Key globals**: `window._metPaintings`, `window._metGalleryGroup`
+
+## Claudette's Lab
+- **Center**: (0, 0, -56), width 16, depth 14, height 4.2
+- **Location**: South of Garcia via 3.6m corridor through back wall
+- **Features**: 3D printer (wireframe IcosahedronGeometry), holographic globe with orbital rings, neural network visualization (4 layers of spheres + cylinder connections), Matrix code rain (canvas), server rack with LED indicators, floating thought bubbles, workstation desk with terminal, energy orb ceiling fixture
+- **Floor**: Circuit board (MeshStandardMaterial + procedural CanvasTexture)
+- **Animations**: All registered in `window._claudetteAnimations[]`, called from wr-animate.js every 2 frames
+- **Lighting**: 4 PointLights (cyan, purple, green, warm) + 4 tech sconces
+
+---
+
+## Room Navigation (ROOM_NAV)
+
+| Room | Camera Position | Look Target |
+|------|----------------|-------------|
+| podium | (-8.5, 3, 5) | (-8.5, 1.2, 0) |
+| boardroom | (0, 3.2, 10) | (0, 1.2, 0) |
+| lounge | (-8, 3, -13) | (-8, 1, -18) |
+| cafe | (8, 3, -13) | (8, 1, -18) |
+| dancehall | (-8, 3, -23) | (-8, 1, -28) |
+| thinktank | (8, 3, -23) | (8, 1, -28) |
+| garcia | (0, 2.5, -46) | (0, 1.8, -40) |
+| showroom | (22, 2, -20) | (22, 1.5, -28) |
+| metgallery | (0, 2.5, -30) | (0, 1.5, -22) |
+| claudettelab | (0, 2.5, -48) | (0, 1.5, -56) |
+
+## Camera Presets (Keys 1-8, 0 to reset)
+1=Wide, 2=Executive, 3=Bird's Eye, 4=Ground, 5=Front Row, 6=Head of Table, 7=Balcony, 8=Over Shoulder
+
+---
+
+## Avatar System
+- 50 humanoid robot avatars, HIDDEN by default (zero draw calls)
+- Only execs materialize for meetings (5 agents)
+- FSM states: seated-idle, seated-speaking, standing-up, walking, moonwalking, sitting-down, chatting, on-break, at-lunch, dancing
+- Dance party: `startWarRoomDanceParty()` materializes ALL agents
+- Agent count display: `document.getElementById('agent-count')`
+
+## Meeting System
+- Trigger: `triggerMeeting('adhoc'|'standup'|'operations'|'eod')`
+- Halt: `haltMeeting()`
+- Backend: boardroom-agent at port 9815 (proxied via `/api/proxy/meeting/trigger/:type`)
+- Ad-hoc: visual assembly only, user-driven
+- Std/Ops/EOD: backend-driven with TTS speaking turns
+
+## Performance Budget
+- Target: 20+ FPS
+- Default draw calls: ~67 (no agents, lazy showroom)
+- With 5 execs: ~267 draw calls (+200)
+- All 50 agents: ~2067 draw calls
+- Auto-rotate pauses on user interaction (8s timeout)
+
+---
+
+## Figma-Driven Design System Rules
+
+### Design Token Architecture
+All UI styling uses CSS custom properties defined in `/public/css/style.css` `:root` section.
+IMPORTANT: Never hardcode colors, fonts, spacing, or shadows in inline styles. Always use `var(--ds-*)` tokens.
+
+| Category | Token Pattern | Example |
+|----------|--------------|---------|
+| Typography | `--ds-font`, `--ds-text-{xs..3xl}`, `--ds-weight-{normal..bold}` | `font-family:var(--ds-font)` |
+| Colors | `--ds-gray-{50..900}`, `--ds-{success,warning,error,info}` | `color:var(--ds-gray-700)` |
+| Surfaces | `--ds-glass`, `--ds-glass-strong`, `--ds-glass-subtle` | `background:var(--ds-glass)` |
+| Blur | `--ds-blur`, `--ds-blur-sm`, `--ds-blur-lg` | `backdrop-filter:var(--ds-blur)` |
+| Borders | `--ds-border`, `--ds-border-strong`, `--ds-border-interactive` | `border:1px solid var(--ds-border)` |
+| Radius | `--ds-radius-{xs..full}` | `border-radius:var(--ds-radius-md)` |
+| Shadows | `--ds-shadow-{xs..xl}` | `box-shadow:var(--ds-shadow-md)` |
+| Spacing | `--ds-space-{1..12}` (4px base) | `padding:var(--ds-space-4)` |
+| Transitions | `--ds-ease`, `--ds-duration-{fast,normal,slow}` | `transition:all var(--ds-duration-fast) var(--ds-ease)` |
+
+### Glass Morphism Pattern (all overlays, modals, toolbars)
+```css
+background: var(--ds-glass-strong);
+backdrop-filter: var(--ds-blur);
+border: 1px solid var(--ds-border);
+border-radius: var(--ds-radius-lg);
+box-shadow: var(--ds-shadow-lg);
+```
+
+### Component Styling Rules
+- IMPORTANT: System font stack: `-apple-system, BlinkMacSystemFont, Inter, sans-serif` — never use Georgia or serif
+- IMPORTANT: Primary action buttons use `--ds-gray-900` bg + white text, secondary use `--ds-gray-50` bg + `--ds-border-strong`
+- IMPORTANT: Muted/secondary text uses `--ds-gray-500`, not brown tones
+- Hover states: buttons lighten to `--ds-gray-700` (primary) or `--ds-gray-100` (secondary)
+- Active/selected items: `--ds-gray-100` background
+- Toast success/warning/error use `--ds-success`/`--ds-warning`/`--ds-error` for accent colors on white glass bg
+
+### Canvas2D Text (3D labels, plaques, signs)
+Font string: `-apple-system, BlinkMacSystemFont, Inter, sans-serif` (NO quotes around Inter — breaks JS string escaping)
+Light backgrounds: `#f5f0eb` (warm gallery white)
+Dark text: `#1a1a1a` (primary), `#666` (secondary), `#888` (tertiary), `#999` (muted)
+
+### PBR Material Standards
+- Wing panels: `MeshStandardMaterial({ roughness: 0.35, metalness: 0.0, envMapIntensity: 0.5 })`
+- Frames: `MeshStandardMaterial({ roughness: 0.3, metalness: 0.4 })` — brushed aluminum look
+- Spine: `MeshStandardMaterial({ roughness: 0.2, metalness: 0.6 })` — chrome-like
+- Environment map: Procedural PMREMGenerator from canvas gradient (no external files)
+- Tone mapping: ACES Filmic, exposure 1.6 in showroom rooms, 1.2 elsewhere
+
+### Showroom Texture Pipeline
+- Canvas slices: 512x768 (2:3 aspect, center-cropped)
+- Fallback texture: light `#f0ebe4` with system font product name
+- Texture queue: 4 concurrent loaders, generation-stamped for page-change safety
+- Cache: Map keyed by product handle/SKU
+
+### CSS Vignette
+`#canvas-container::after` applies a radial gradient vignette for photographic depth (zero GPU cost)
+
+### Forbidden Patterns
+- NEVER use `Georgia, serif` font anywhere
+- NEVER use gold `#d4a574` for UI text or backgrounds (reserved only for 3D material highlight: `wingsFrameHoverMat`)
+- NEVER use dark brown `#1a0f08` backgrounds
+- NEVER hardcode hex colors in inline styles — use CSS variable tokens
+- NEVER use `rgba(42,26,10,*)` — this is the old dark theme
diff --git a/docs/plans/2026-03-02-figma-threejs-photorealism-design.md b/docs/plans/2026-03-02-figma-threejs-photorealism-design.md
new file mode 100644
index 0000000..3707409
--- /dev/null
+++ b/docs/plans/2026-03-02-figma-threejs-photorealism-design.md
@@ -0,0 +1,167 @@
+# Figma + Three.js Photorealistic Showroom Pipeline
+
+**Date:** 2026-03-02
+**Status:** Approved
+**Target Systems:** War Room (port 4060) + Boardroom-3D Showroom (port 7681)
+
+---
+
+## Vision
+
+Transform the existing 3D showroom into a photorealistic architectural trade showroom with:
+- Endless vendor wings (50 wings, lazy-loaded)
+- Exposed brick corridor with track lighting
+- PBR materials with normal/roughness maps
+- Full post-processing pipeline (SSAO, DOF, bloom, color grading)
+- Figma MCP-driven layout pipeline
+
+## Architecture
+
+### Pipeline Flow
+```
+Figma Design → MCP get_design_context → Claude Transform → TypeScript Modules → Vite Build → Live 3D
+```
+
+### Approach: MCP-Native
+- Uses existing Figma MCP tools in Claude Code
+- Zero custom infrastructure
+- Real-time design iteration
+
+---
+
+## Showroom Layout: Endless Wings Corridor
+
+### Structure
+```
+Entry → Central Corridor → 50 Vendor Wings (extending in +X)
+                         → Side Walls: wallpaper samples + book displays
+                         → Overhead: industrial track lighting
+```
+
+### Key Elements
+
+1. **Central Corridor** — Long exposed-brick hallway, polished concrete floor, industrial ceiling with exposed ductwork
+2. **50 Vendor Wings** — Perpendicular from corridor. Each = 1 vendor. Clicking opens to reveal 20-50 product panels. Lazy-loaded on first visit.
+3. **Side Walls** — Between wings: large-format wallpaper samples (4ft x 8ft mounted panels) + glass-front sample book shelves
+4. **Track Lighting** — Continuous rail along corridor ceiling + dedicated spots per wing bay entrance
+5. **Wing Interior** — Each vendor wing: 3 walls of product panels, sample table, vendor branding, warm accent lighting
+6. **Navigation** — WASD walking + click-to-teleport. Minimap shows corridor + wing positions.
+
+### Performance: Endless Illusion
+- Render only 3 wings at a time (current + adjacent)
+- Fog/atmosphere fades distant wings
+- Frustum culling hides everything beyond visible range
+- Lazy-load product textures only when wing approached
+
+---
+
+## Photorealism Rendering Pipeline
+
+### Material System
+
+| Surface | Color | Metalness | Roughness | Normal Map | Notes |
+|---------|-------|-----------|-----------|------------|-------|
+| Polished concrete floor | #8a8580 | 0.15 | 0.55 | Procedural micro-texture | Contact shadows |
+| Brick walls | #8b4513 | 0.0 | 0.85 | Procedural brick pattern | Mortar darkening via SSAO |
+| Steel ceiling/ducts | #2a2a2a | 0.6 | 0.3 | Metal panel grid | Exposed duct geometry |
+| Wing frames | #c0c0c0 | 0.7 | 0.25 | Brushed aluminum | Sharp reflections |
+| Product panels | Texture | 0.0 | 0.4 | Fabric/paper subtle | Matte wallcovering |
+| Book shelves | #8b6914 | 0.0 | 0.65 | Wood grain | Glass fronts (transmission 0.9) |
+| Track lights | #ffffff | 0.9 | 0.1 | Chrome | Emissive + SpotLight |
+
+### Post-Processing Stack
+```
+RenderPass → SSAOPass → UnrealBloomPass → BokehPass → ColorGradingPass → FilmGrainPass → Output
+```
+
+1. **SSAO** — radius 0.5, bias 0.025, intensity 1.2
+2. **Bloom** — strength 0.25, radius 0.5, threshold 0.65
+3. **Depth of Field** — focus on look target, aperture 0.02
+4. **Color Grading** — warm architectural tones, contrast boost
+5. **Film Grain** — opacity 0.008, temporal
+
+### Environment Lighting
+- HDR environment map (Polyhaven CC0 gallery/studio HDRI)
+- RGBELoader → PMREMGenerator for IBL
+- ACES Filmic tone mapping, exposure 1.4 (corridor) / 1.6 (wing interior)
+- Shadows: PCF soft, 2048x2048 from main directional
+- Track lights: SpotLight with cone + penumbra
+- Volumetric light cones (additive-blend geometry + dust particles)
+
+---
+
+## MCP Integration Flow
+
+### Step 1: Figma Design Creation
+- Create showroom floor plan in Figma
+- Define vendor wings, walls, lighting positions
+- Set material swatches as image fills
+- Use Figma variables for design tokens
+
+### Step 2: MCP Extraction
+```typescript
+// Extract design context
+get_design_context(nodeId, fileKey) → structured layout data
+
+// Extract design tokens
+get_variable_defs(nodeId, fileKey) → color/spacing tokens
+```
+
+### Step 3: Claude Transform
+- Map Figma frames → Three.js scene graph (positions, dimensions)
+- Map image fills → TextureLoader URLs
+- Map text nodes → canvas-rendered labels
+- Map component instances → reusable objects
+
+### Step 4: TypeScript Module Generation
+```
+src/showroom/
+├── ShowroomLayout.ts      — corridor, wings, walls geometry
+├── ShowroomMaterials.ts   — PBR material definitions + normal maps
+├── ShowroomLighting.ts    — track lights, spots, ambient, HDRI
+├── ShowroomTextures.ts    — texture pipeline (lazy-load, fallback)
+├── ShowroomNavigation.ts  — WASD, teleport, wing selection
+├── ShowroomCulling.ts     — visibility management, LOD
+└── ShowroomPostProcess.ts — SSAO + DOF + bloom + grading
+```
+
+### Step 5: Integration
+- War Room (4060): new showroom room accessible via navigation
+- Boardroom-3D (7681): upgraded gallery rendering with shared materials
+
+---
+
+## Performance Budget
+
+| Metric | Target |
+|--------|--------|
+| FPS | 30+ at 1080p |
+| Draw calls (base) | < 100 |
+| Draw calls (wing open) | < 300 |
+| Texture memory | < 256MB |
+| Initial load | < 3 seconds |
+| Wing load (lazy) | < 1 second |
+
+---
+
+## Implementation Phases
+
+### Phase 1: Figma Design + MCP Pipeline
+- Create showroom layout in Figma
+- Build MCP extraction → transform pipeline
+- Generate base TypeScript modules
+
+### Phase 2: Photorealism Rendering
+- Implement SSAO, HDR env map, normal maps
+- Add DOF, color grading, volumetric lights
+- Upgrade materials to full PBR with detail maps
+
+### Phase 3: Endless Wings System
+- Build lazy-loaded wing architecture
+- Implement culling and LOD
+- Connect to product database for real textures
+
+### Phase 4: Integration
+- Merge into War Room (4060)
+- Upgrade Boardroom-3D (7681)
+- Performance optimization pass
diff --git a/docs/plans/2026-03-02-figma-threejs-photorealism-plan.md b/docs/plans/2026-03-02-figma-threejs-photorealism-plan.md
new file mode 100644
index 0000000..9dda415
--- /dev/null
+++ b/docs/plans/2026-03-02-figma-threejs-photorealism-plan.md
@@ -0,0 +1,1623 @@
+# Figma + Three.js Photorealistic Showroom — Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Build a Figma MCP-driven pipeline that generates photorealistic endless-wings showroom scenes for both the War Room (port 4060, TypeScript/Vite) and Boardroom-3D (port 7681, vanilla JS), featuring architectural trade showroom aesthetics with exposed brick, track lighting, and PBR materials.
+
+**Architecture:** Figma design → MCP `get_design_context` extraction → Claude transform → TypeScript/JS modules. The showroom uses an endless corridor with 50 vendor wings (lazy-loaded), exposed brick walls with sample book displays, polished concrete floors, and industrial track lighting. Rendering upgraded with SSAO, HDR environment maps, procedural normal maps, depth of field, and color grading.
+
+**Tech Stack:** Three.js r170 (war room) / r128 CDN (boardroom-3d), Vite 6, TypeScript 5.7, Figma MCP tools, procedural canvas normal/roughness maps, EffectComposer post-processing pipeline.
+
+---
+
+## Phase 1: Figma Showroom Design + MCP Pipeline
+
+### Task 1: Create Figma Showroom Floor Plan
+
+**Files:**
+- Create: `/root/Projects/dw-war-room/src/showroom/figma-layout.json` (extracted layout data)
+
+**Step 1: Create the Figma design using generate_figma_design**
+
+Use the Figma MCP `generate_figma_design` tool to create a new Figma file with the showroom floor plan. The design should include:
+- Main corridor frame (40ft x 20ft, exposed brick fill)
+- 6 vendor wing bay frames extending perpendicular (each 12ft x 10ft)
+- Central display table rectangle
+- Track lighting rail lines along ceiling
+- Product display wall rectangles (4ft x 8ft each, wallpaper texture fills)
+- Glass entry frame
+- Color tokens: brick (#8b4513), concrete (#8a8580), aluminum (#c0c0c0), warm wood (#8b6914)
+
+**Step 2: Extract design context via MCP**
+
+```typescript
+// Use get_design_context to extract the showroom layout
+const designContext = await mcp.get_design_context({
+  nodeId: '<root-frame-id>',
+  fileKey: '<file-key>'
+});
+```
+
+Save extracted JSON to `figma-layout.json`.
+
+**Step 3: Extract design tokens via MCP**
+
+```typescript
+const tokens = await mcp.get_variable_defs({
+  nodeId: '<root-frame-id>',
+  fileKey: '<file-key>'
+});
+```
+
+**Step 4: Commit**
+
+```bash
+git add src/showroom/figma-layout.json
+git commit -m "feat(showroom): extract Figma showroom layout via MCP"
+```
+
+---
+
+### Task 2: Build Figma-to-SceneGraph Transformer
+
+**Files:**
+- Create: `/root/Projects/dw-war-room/src/showroom/FigmaTransformer.ts`
+
+**Step 1: Write the transformer module**
+
+```typescript
+// FigmaTransformer.ts — Converts Figma design context into Three.js scene graph config
+import type { FigmaNode, SceneConfig, WallConfig, WingConfig, LightConfig } from './types';
+
+export interface SceneConfig {
+  corridor: {
+    width: number;    // world units (1 unit = 1 foot)
+    depth: number;
+    height: number;
+    wallMaterial: 'brick' | 'concrete' | 'charcoal';
+    floorMaterial: 'polished-concrete' | 'wood' | 'marble';
+  };
+  wings: WingConfig[];
+  walls: WallConfig[];
+  lights: LightConfig[];
+  furniture: FurnitureConfig[];
+  entry: { position: [number, number, number]; signText: string };
+}
+
+export interface WingConfig {
+  index: number;
+  vendor: string;
+  position: [number, number, number];
+  rotation: number;
+  panelCount: number;
+  accentColor: string;   // hex from Figma fill
+}
+
+export interface WallConfig {
+  position: [number, number, number];
+  dimensions: [number, number];  // width, height
+  material: string;
+  hasBookshelf: boolean;
+  hasSamplePanels: boolean;
+  sampleCount: number;
+}
+
+export interface LightConfig {
+  type: 'spot' | 'track' | 'ambient' | 'point';
+  position: [number, number, number];
+  target?: [number, number, number];
+  color: string;
+  intensity: number;
+  angle?: number;    // SpotLight cone angle
+  penumbra?: number;
+}
+
+export interface FurnitureConfig {
+  type: 'display-table' | 'stool' | 'bookshelf' | 'sample-rack';
+  position: [number, number, number];
+  dimensions: [number, number, number];
+}
+
+/**
+ * Transform Figma design JSON into Three.js scene configuration.
+ * Maps Figma frames → positions, rectangles → geometry, fills → materials.
+ */
+export function transformFigmaToScene(figmaData: any): SceneConfig {
+  const root = figmaData.document || figmaData;
+  const config: SceneConfig = {
+    corridor: { width: 40, depth: 20, height: 12, wallMaterial: 'brick', floorMaterial: 'polished-concrete' },
+    wings: [],
+    walls: [],
+    lights: [],
+    furniture: [],
+    entry: { position: [0, 0, 10], signText: 'DESIGNER WALLCOVERINGS' },
+  };
+
+  // Traverse Figma node tree
+  function traverse(node: any, depth = 0) {
+    if (!node) return;
+
+    // Map FRAME nodes to wings or rooms based on name
+    if (node.type === 'FRAME' && node.name?.toLowerCase().includes('wing')) {
+      const bbox = node.absoluteBoundingBox || {};
+      config.wings.push({
+        index: config.wings.length,
+        vendor: node.name.replace(/wing[-_\s]*/i, '').trim(),
+        position: [bbox.x / 10 || config.wings.length * 4, 0, bbox.y / 10 || 0],
+        rotation: 0,
+        panelCount: (node.children || []).filter((c: any) => c.type === 'RECTANGLE').length || 10,
+        accentColor: extractFillColor(node) || '#c0c0c0',
+      });
+    }
+
+    // Map RECTANGLE nodes with IMAGE fills to wall sample panels
+    if (node.type === 'RECTANGLE' && node.fills?.some((f: any) => f.type === 'IMAGE')) {
+      const bbox = node.absoluteBoundingBox || {};
+      config.walls.push({
+        position: [bbox.x / 10 || 0, bbox.height / 20 || 2, bbox.y / 10 || 0],
+        dimensions: [bbox.width / 10 || 4, bbox.height / 10 || 8],
+        material: 'brick',
+        hasBookshelf: false,
+        hasSamplePanels: true,
+        sampleCount: 1,
+      });
+    }
+
+    // Map circles/ellipses to spotlights
+    if (node.type === 'ELLIPSE' && node.name?.toLowerCase().includes('light')) {
+      const bbox = node.absoluteBoundingBox || {};
+      config.lights.push({
+        type: 'spot',
+        position: [bbox.x / 10 || 0, 10, bbox.y / 10 || 0],
+        target: [bbox.x / 10 || 0, 0, bbox.y / 10 || 0],
+        color: extractFillColor(node) || '#ffffff',
+        intensity: 1.5,
+        angle: Math.PI / 6,
+        penumbra: 0.5,
+      });
+    }
+
+    if (node.children) node.children.forEach((c: any) => traverse(c, depth + 1));
+  }
+
+  traverse(root);
+
+  // Add default track lighting if none extracted
+  if (config.lights.length === 0) {
+    for (let i = 0; i < 8; i++) {
+      config.lights.push({
+        type: 'spot',
+        position: [i * 5 - 17.5, 11, 0],
+        target: [i * 5 - 17.5, 0, 0],
+        color: '#f5e6d0',
+        intensity: 1.2,
+        angle: Math.PI / 5,
+        penumbra: 0.4,
+      });
+    }
+  }
+
+  return config;
+}
+
+function extractFillColor(node: any): string | null {
+  const fill = (node.fills || []).find((f: any) => f.type === 'SOLID' && f.visible !== false);
+  if (!fill?.color) return null;
+  const r = Math.round(fill.color.r * 255);
+  const g = Math.round(fill.color.g * 255);
+  const b = Math.round(fill.color.b * 255);
+  return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
+}
+```
+
+**Step 2: Commit**
+
+```bash
+git add src/showroom/FigmaTransformer.ts
+git commit -m "feat(showroom): Figma-to-SceneGraph transformer"
+```
+
+---
+
+## Phase 2: Photorealistic Material System
+
+### Task 3: Procedural Normal Map Generator
+
+**Files:**
+- Create: `/root/Projects/dw-war-room/src/showroom/ProceduralMaps.ts`
+
+**Step 1: Write procedural normal/roughness map generators**
+
+```typescript
+// ProceduralMaps.ts — Canvas-based procedural normal and roughness maps
+// Zero external file dependencies — all generated at runtime
+import * as THREE from 'three';
+
+/**
+ * Generate a brick wall normal map using canvas.
+ * Creates realistic mortar lines and brick surface variation.
+ */
+export function generateBrickNormalMap(width = 1024, height = 1024): THREE.CanvasTexture {
+  const canvas = document.createElement('canvas');
+  canvas.width = width;
+  canvas.height = height;
+  const ctx = canvas.getContext('2d')!;
+
+  // Base neutral normal (pointing straight out: RGB 128,128,255)
+  ctx.fillStyle = 'rgb(128, 128, 255)';
+  ctx.fillRect(0, 0, width, height);
+
+  const brickW = width / 8;
+  const brickH = height / 16;
+  const mortarW = 3;
+
+  for (let row = 0; row < 16; row++) {
+    const offset = (row % 2) * (brickW / 2); // stagger rows
+    for (let col = -1; col < 9; col++) {
+      const x = col * brickW + offset;
+      const y = row * brickH;
+
+      // Mortar lines (recessed = normal pointing inward)
+      // Horizontal mortar
+      ctx.fillStyle = 'rgb(128, 100, 255)'; // slight downward normal
+      ctx.fillRect(0, y, width, mortarW);
+
+      // Vertical mortar
+      ctx.fillStyle = 'rgb(100, 128, 255)'; // slight leftward normal
+      ctx.fillRect(x, y, mortarW, brickH);
+
+      // Brick surface noise (subtle variation)
+      for (let i = 0; i < 20; i++) {
+        const nx = x + Math.random() * brickW;
+        const ny = y + mortarW + Math.random() * (brickH - mortarW * 2);
+        const r = 128 + (Math.random() - 0.5) * 12;
+        const g = 128 + (Math.random() - 0.5) * 12;
+        ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
+        ctx.fillRect(nx, ny, 3 + Math.random() * 6, 2 + Math.random() * 4);
+      }
+    }
+  }
+
+  const tex = new THREE.CanvasTexture(canvas);
+  tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
+  tex.repeat.set(2, 2);
+  return tex;
+}
+
+/**
+ * Generate a polished concrete normal map.
+ * Subtle micro-texture with occasional aggregate patches.
+ */
+export function generateConcreteNormalMap(width = 1024, height = 1024): THREE.CanvasTexture {
+  const canvas = document.createElement('canvas');
+  canvas.width = width;
+  canvas.height = height;
+  const ctx = canvas.getContext('2d')!;
+
+  // Base neutral normal
+  ctx.fillStyle = 'rgb(128, 128, 255)';
+  ctx.fillRect(0, 0, width, height);
+
+  // Micro-texture noise — polished concrete has subtle surface variation
+  for (let i = 0; i < 5000; i++) {
+    const x = Math.random() * width;
+    const y = Math.random() * height;
+    const r = 128 + (Math.random() - 0.5) * 6;
+    const g = 128 + (Math.random() - 0.5) * 6;
+    ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
+    ctx.fillRect(x, y, 1 + Math.random() * 3, 1 + Math.random() * 3);
+  }
+
+  // Aggregate patches (larger bumps)
+  for (let i = 0; i < 80; i++) {
+    const x = Math.random() * width;
+    const y = Math.random() * height;
+    const radius = 4 + Math.random() * 12;
+    const r = 128 + (Math.random() - 0.5) * 18;
+    const g = 128 + (Math.random() - 0.5) * 18;
+    ctx.beginPath();
+    ctx.arc(x, y, radius, 0, Math.PI * 2);
+    ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
+    ctx.fill();
+  }
+
+  const tex = new THREE.CanvasTexture(canvas);
+  tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
+  tex.repeat.set(3, 3);
+  return tex;
+}
+
+/**
+ * Generate a brushed aluminum normal map.
+ * Directional brushing lines for metallic surfaces.
+ */
+export function generateBrushedAluminumNormalMap(width = 512, height = 512): THREE.CanvasTexture {
+  const canvas = document.createElement('canvas');
+  canvas.width = width;
+  canvas.height = height;
+  const ctx = canvas.getContext('2d')!;
+
+  ctx.fillStyle = 'rgb(128, 128, 255)';
+  ctx.fillRect(0, 0, width, height);
+
+  // Horizontal brush strokes
+  for (let y = 0; y < height; y++) {
+    const r = 128 + (Math.random() - 0.5) * 4;
+    const g = 128 + Math.sin(y * 0.3) * 3 + (Math.random() - 0.5) * 2;
+    ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
+    ctx.fillRect(0, y, width, 1);
+  }
+
+  const tex = new THREE.CanvasTexture(canvas);
+  tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
+  return tex;
+}
+
+/**
+ * Generate a wood grain normal map for shelving.
+ */
+export function generateWoodNormalMap(width = 512, height = 512): THREE.CanvasTexture {
+  const canvas = document.createElement('canvas');
+  canvas.width = width;
+  canvas.height = height;
+  const ctx = canvas.getContext('2d')!;
+
+  ctx.fillStyle = 'rgb(128, 128, 255)';
+  ctx.fillRect(0, 0, width, height);
+
+  // Wood grain lines (vertical, slightly wavy)
+  for (let x = 0; x < width; x += 4 + Math.random() * 8) {
+    ctx.beginPath();
+    ctx.moveTo(x, 0);
+    for (let y = 0; y < height; y += 10) {
+      ctx.lineTo(x + Math.sin(y * 0.02 + x * 0.1) * 3, y);
+    }
+    const r = 128 + (Math.random() - 0.5) * 10;
+    ctx.strokeStyle = `rgb(${r}, 128, 255)`;
+    ctx.lineWidth = 1 + Math.random() * 2;
+    ctx.stroke();
+  }
+
+  const tex = new THREE.CanvasTexture(canvas);
+  tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
+  return tex;
+}
+
+/**
+ * Generate a fabric/paper subtle normal map for wallcovering product panels.
+ * Very subtle — just enough to catch light at grazing angles.
+ */
+export function generateFabricNormalMap(width = 256, height = 256): THREE.CanvasTexture {
+  const canvas = document.createElement('canvas');
+  canvas.width = width;
+  canvas.height = height;
+  const ctx = canvas.getContext('2d')!;
+
+  ctx.fillStyle = 'rgb(128, 128, 255)';
+  ctx.fillRect(0, 0, width, height);
+
+  // Woven texture: alternating horizontal and vertical micro-lines
+  for (let y = 0; y < height; y += 2) {
+    for (let x = 0; x < width; x += 2) {
+      const horizontal = (y / 2) % 2 === 0;
+      const r = 128 + (horizontal ? 2 : -2) + (Math.random() - 0.5) * 3;
+      const g = 128 + (horizontal ? -2 : 2) + (Math.random() - 0.5) * 3;
+      ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
+      ctx.fillRect(x, y, 2, 2);
+    }
+  }
+
+  const tex = new THREE.CanvasTexture(canvas);
+  tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
+  tex.repeat.set(4, 4);
+  return tex;
+}
+```
+
+**Step 2: Commit**
+
+```bash
+git add src/showroom/ProceduralMaps.ts
+git commit -m "feat(showroom): procedural normal map generators (brick, concrete, aluminum, wood, fabric)"
+```
+
+---
+
+### Task 4: PBR Material Factory
+
+**Files:**
+- Create: `/root/Projects/dw-war-room/src/showroom/ShowroomMaterials.ts`
+
+**Step 1: Write the material factory**
+
+```typescript
+// ShowroomMaterials.ts — PBR material definitions for photorealistic showroom
+import * as THREE from 'three';
+import {
+  generateBrickNormalMap,
+  generateConcreteNormalMap,
+  generateBrushedAluminumNormalMap,
+  generateWoodNormalMap,
+  generateFabricNormalMap,
+} from './ProceduralMaps';
+
+export interface MaterialLibrary {
+  brick: THREE.MeshStandardMaterial;
+  concrete: THREE.MeshStandardMaterial;
+  aluminum: THREE.MeshStandardMaterial;
+  wood: THREE.MeshStandardMaterial;
+  fabric: THREE.MeshStandardMaterial;
+  steel: THREE.MeshStandardMaterial;
+  glass: THREE.MeshStandardMaterial;
+  trackLight: THREE.MeshStandardMaterial;
+}
+
+/**
+ * Create the full PBR material library for the showroom.
+ * Each material has proper normal maps for photorealistic surface detail.
+ */
+export function createMaterialLibrary(): MaterialLibrary {
+  return {
+    brick: new THREE.MeshStandardMaterial({
+      color: 0x8b4513,
+      metalness: 0.0,
+      roughness: 0.85,
+      normalMap: generateBrickNormalMap(),
+      normalScale: new THREE.Vector2(1.2, 1.2),
+    }),
+
+    concrete: new THREE.MeshStandardMaterial({
+      color: 0x8a8580,
+      metalness: 0.15,
+      roughness: 0.55,
+      normalMap: generateConcreteNormalMap(),
+      normalScale: new THREE.Vector2(0.5, 0.5),
+    }),
+
+    aluminum: new THREE.MeshStandardMaterial({
+      color: 0xc0c0c0,
+      metalness: 0.7,
+      roughness: 0.25,
+      normalMap: generateBrushedAluminumNormalMap(),
+      normalScale: new THREE.Vector2(0.3, 0.3),
+      envMapIntensity: 1.5,
+    }),
+
+    wood: new THREE.MeshStandardMaterial({
+      color: 0x8b6914,
+      metalness: 0.0,
+      roughness: 0.65,
+      normalMap: generateWoodNormalMap(),
+      normalScale: new THREE.Vector2(0.8, 0.8),
+    }),
+
+    fabric: new THREE.MeshStandardMaterial({
+      color: 0xf0ebe4,
+      metalness: 0.0,
+      roughness: 0.4,
+      normalMap: generateFabricNormalMap(),
+      normalScale: new THREE.Vector2(0.2, 0.2),
+      envMapIntensity: 0.3,
+    }),
+
+    steel: new THREE.MeshStandardMaterial({
+      color: 0x2a2a2a,
+      metalness: 0.6,
+      roughness: 0.3,
+      envMapIntensity: 1.0,
+    }),
+
+    glass: new THREE.MeshStandardMaterial({
+      color: 0xffffff,
+      metalness: 0.0,
+      roughness: 0.05,
+      transparent: true,
+      opacity: 0.15,
+      envMapIntensity: 2.0,
+    }),
+
+    trackLight: new THREE.MeshStandardMaterial({
+      color: 0xffffff,
+      metalness: 0.9,
+      roughness: 0.1,
+      emissive: new THREE.Color(0xf5e6d0),
+      emissiveIntensity: 0.8,
+    }),
+  };
+}
+
+/**
+ * Dispose all materials and their textures.
+ */
+export function disposeMaterialLibrary(lib: MaterialLibrary): void {
+  Object.values(lib).forEach(mat => {
+    if (mat.normalMap) mat.normalMap.dispose();
+    mat.dispose();
+  });
+}
+```
+
+**Step 2: Commit**
+
+```bash
+git add src/showroom/ShowroomMaterials.ts
+git commit -m "feat(showroom): PBR material factory with normal maps"
+```
+
+---
+
+## Phase 3: HDR Environment + Post-Processing Upgrade
+
+### Task 5: HDR Environment Map Loader
+
+**Files:**
+- Modify: `/root/Projects/dw-war-room/src/scene/WarRoom.ts:95-123`
+- Create: `/root/Projects/dw-war-room/src/showroom/ShowroomEnvironment.ts`
+
+**Step 1: Create showroom-specific environment module**
+
+```typescript
+// ShowroomEnvironment.ts — HDR environment map for photorealistic IBL
+import * as THREE from 'three';
+
+/**
+ * Generate a high-quality procedural environment map for gallery/showroom IBL.
+ * Creates a warm gallery interior environment rather than the cold command-center gradient.
+ *
+ * This procedural approach avoids needing an external HDRI file download.
+ * It simulates a well-lit interior with warm ceiling lights and neutral walls.
+ */
+export function createShowroomEnvMap(renderer: THREE.WebGLRenderer): THREE.Texture {
+  const pmrem = new THREE.PMREMGenerator(renderer);
+  pmrem.compileEquirectangularShader();
+
+  const envScene = new THREE.Scene();
+
+  // Sky dome — warm neutral ceiling (gallery white)
+  const skyGeo = new THREE.SphereGeometry(50, 32, 16);
+  const skyMat = new THREE.MeshBasicMaterial({
+    color: 0x1a1815,  // warm dark interior
+    side: THREE.BackSide,
+  });
+  envScene.add(new THREE.Mesh(skyGeo, skyMat));
+
+  // Ceiling highlight — warm white overhead panels (gallery lighting)
+  const ceilGeo = new THREE.SphereGeometry(40, 16, 8, 0, Math.PI * 2, 0, Math.PI * 0.2);
+  const ceilMat = new THREE.MeshBasicMaterial({
+    color: 0xf5e6d0,  // warm white (3200K-ish)
+    side: THREE.BackSide,
+  });
+  const ceil = new THREE.Mesh(ceilGeo, ceilMat);
+  ceil.rotation.x = Math.PI;
+  envScene.add(ceil);
+
+  // Floor reflection — dark polished concrete
+  const floorGeo = new THREE.SphereGeometry(40, 16, 8, 0, Math.PI * 2, Math.PI * 0.75, Math.PI * 0.25);
+  const floorMat = new THREE.MeshBasicMaterial({
+    color: 0x0d0c0a,
+    side: THREE.BackSide,
+  });
+  envScene.add(new THREE.Mesh(floorGeo, floorMat));
+
+  // Wall bands — warm brick tones from sides
+  for (let i = 0; i < 4; i++) {
+    const wallGeo = new THREE.PlaneGeometry(30, 15);
+    const wallMat = new THREE.MeshBasicMaterial({
+      color: 0x3a2820,  // dark warm brick
+      side: THREE.DoubleSide,
+    });
+    const wall = new THREE.Mesh(wallGeo, wallMat);
+    const angle = (i / 4) * Math.PI * 2;
+    wall.position.set(Math.cos(angle) * 35, 5, Math.sin(angle) * 35);
+    wall.lookAt(0, 5, 0);
+    envScene.add(wall);
+  }
+
+  // Track light spots — bright warm points at ceiling level
+  for (let i = 0; i < 6; i++) {
+    const spotGeo = new THREE.SphereGeometry(2, 8, 8);
+    const spotMat = new THREE.MeshBasicMaterial({ color: 0xffe8c0 });
+    const spot = new THREE.Mesh(spotGeo, spotMat);
+    const angle = (i / 6) * Math.PI * 2;
+    spot.position.set(Math.cos(angle) * 15, 18, Math.sin(angle) * 15);
+    envScene.add(spot);
+  }
+
+  const envRenderTarget = pmrem.fromScene(envScene as any, 0.02);
+  const texture = envRenderTarget.texture;
+
+  // Cleanup
+  pmrem.dispose();
+  skyMat.dispose();
+  ceilMat.dispose();
+  floorMat.dispose();
+
+  return texture;
+}
+```
+
+**Step 2: Commit**
+
+```bash
+git add src/showroom/ShowroomEnvironment.ts
+git commit -m "feat(showroom): warm gallery HDR environment map for IBL"
+```
+
+---
+
+### Task 6: SSAO + Depth of Field + Color Grading
+
+**Files:**
+- Create: `/root/Projects/dw-war-room/src/showroom/ShowroomPostProcess.ts`
+
+**Step 1: Write the enhanced post-processing pipeline**
+
+```typescript
+// ShowroomPostProcess.ts — Photorealistic post-processing for the showroom
+// Adds SSAO, DOF, and color grading on top of existing bloom/grain pipeline
+import * as THREE from 'three';
+import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
+import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
+import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
+import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
+import { SSAOPass } from 'three/examples/jsm/postprocessing/SSAOPass.js';
+import { BokehPass } from 'three/examples/jsm/postprocessing/BokehPass.js';
+
+// ── Color Grading Shader ─────────────────────────────────────────────────────
+// Warm architectural tones: slight orange lift in shadows, cool highlight compression
+const ColorGradingShader = {
+  name: 'ColorGradingShader',
+  uniforms: {
+    tDiffuse: { value: null as THREE.Texture | null },
+    uSaturation: { value: 1.1 },     // slight boost
+    uContrast: { value: 1.08 },      // subtle contrast
+    uBrightness: { value: 0.02 },    // tiny lift
+    uWarmth: { value: 0.04 },        // warm shift
+  },
+  vertexShader: /* glsl */ `
+    varying vec2 vUv;
+    void main() {
+      vUv = uv;
+      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+    }
+  `,
+  fragmentShader: /* glsl */ `
+    uniform sampler2D tDiffuse;
+    uniform float uSaturation;
+    uniform float uContrast;
+    uniform float uBrightness;
+    uniform float uWarmth;
+    varying vec2 vUv;
+
+    void main() {
+      vec4 color = texture2D(tDiffuse, vUv);
+
+      // Saturation adjustment
+      float luminance = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));
+      color.rgb = mix(vec3(luminance), color.rgb, uSaturation);
+
+      // Contrast (pivot at mid-gray)
+      color.rgb = (color.rgb - 0.5) * uContrast + 0.5;
+
+      // Brightness lift
+      color.rgb += uBrightness;
+
+      // Warm tint — add slight orange to shadows, cool highlights slightly
+      float shadowMask = 1.0 - smoothstep(0.0, 0.5, luminance);
+      color.r += uWarmth * shadowMask;
+      color.g += uWarmth * 0.5 * shadowMask;
+
+      // Vignette — darkens corners for photographic depth
+      vec2 vUvCenter = vUv - 0.5;
+      float vignette = 1.0 - dot(vUvCenter, vUvCenter) * 0.5;
+      color.rgb *= vignette;
+
+      gl_FragColor = color;
+    }
+  `,
+};
+
+// ── Film Grain (reused from war room) ────────────────────────────────────────
+const FilmGrainShader = {
+  name: 'FilmGrainShader',
+  uniforms: {
+    tDiffuse: { value: null as THREE.Texture | null },
+    uTime: { value: 0.0 },
+    uOpacity: { value: 0.006 },
+  },
+  vertexShader: /* glsl */ `
+    varying vec2 vUv;
+    void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
+  `,
+  fragmentShader: /* glsl */ `
+    uniform sampler2D tDiffuse;
+    uniform float uTime;
+    uniform float uOpacity;
+    varying vec2 vUv;
+    float hash(vec2 p) { p = fract(p * vec2(234.34, 435.345)); p += dot(p, p + 34.23); return fract(p.x * p.y); }
+    void main() {
+      vec4 color = texture2D(tDiffuse, vUv);
+      float grain = hash(vUv + fract(uTime * 0.1));
+      color.rgb += (grain - 0.5) * 2.0 * uOpacity;
+      gl_FragColor = color;
+    }
+  `,
+};
+
+export class ShowroomPostProcess {
+  private composer: EffectComposer;
+  private ssaoPass: SSAOPass;
+  private bloomPass: UnrealBloomPass;
+  private bokehPass: BokehPass;
+  private colorGradingPass: ShaderPass;
+  private filmGrainPass: ShaderPass;
+  private clock: THREE.Clock;
+
+  constructor(
+    renderer: THREE.WebGLRenderer,
+    scene: THREE.Scene,
+    camera: THREE.PerspectiveCamera
+  ) {
+    this.clock = new THREE.Clock();
+    const size = renderer.getSize(new THREE.Vector2());
+
+    this.composer = new EffectComposer(renderer);
+
+    // Pass 1: Render
+    this.composer.addPass(new RenderPass(scene, camera));
+
+    // Pass 2: SSAO — ambient occlusion for depth
+    this.ssaoPass = new SSAOPass(scene, camera, size.x, size.y);
+    this.ssaoPass.kernelRadius = 0.5;
+    this.ssaoPass.minDistance = 0.001;
+    this.ssaoPass.maxDistance = 0.1;
+    (this.ssaoPass as any).output = SSAOPass.OUTPUT.Default;
+    this.composer.addPass(this.ssaoPass);
+
+    // Pass 3: Bloom — subtle architectural glow
+    this.bloomPass = new UnrealBloomPass(
+      new THREE.Vector2(size.x, size.y),
+      0.2,   // strength (slightly less than war room — showroom is brighter)
+      0.4,   // radius
+      0.7    // threshold
+    );
+    this.composer.addPass(this.bloomPass);
+
+    // Pass 4: Depth of Field — cinematic focus
+    this.bokehPass = new BokehPass(scene, camera, {
+      focus: 15.0,     // focus distance (adjustable)
+      aperture: 0.002, // subtle DOF
+      maxblur: 0.005,  // max blur radius
+    });
+    this.composer.addPass(this.bokehPass);
+
+    // Pass 5: Color Grading — warm architectural tones
+    this.colorGradingPass = new ShaderPass(ColorGradingShader);
+    this.composer.addPass(this.colorGradingPass);
+
+    // Pass 6: Film Grain — organic texture
+    this.filmGrainPass = new ShaderPass(FilmGrainShader);
+    this.composer.addPass(this.filmGrainPass);
+  }
+
+  render(): void {
+    this.filmGrainPass.uniforms['uTime'].value = this.clock.getElapsedTime();
+    this.composer.render();
+  }
+
+  setFocusDistance(distance: number): void {
+    (this.bokehPass as any).uniforms['focus'].value = distance;
+  }
+
+  resize(width: number, height: number): void {
+    this.composer.setSize(width, height);
+    this.bloomPass.resolution.set(width, height);
+    this.ssaoPass.setSize(width, height);
+  }
+
+  getComposer(): EffectComposer {
+    return this.composer;
+  }
+}
+```
+
+**Step 2: Commit**
+
+```bash
+git add src/showroom/ShowroomPostProcess.ts
+git commit -m "feat(showroom): SSAO + DOF + color grading post-processing pipeline"
+```
+
+---
+
+## Phase 4: Showroom Scene Builder
+
+### Task 7: Showroom Layout Builder (Corridor + Wings)
+
+**Files:**
+- Create: `/root/Projects/dw-war-room/src/showroom/ShowroomLayout.ts`
+
+**Step 1: Write the main showroom scene builder**
+
+This module creates the architectural corridor with endless wings, using the material library from Task 4 and scene config from Task 2.
+
+```typescript
+// ShowroomLayout.ts — Builds the photorealistic showroom corridor and wing bays
+import * as THREE from 'three';
+import type { SceneConfig, WingConfig, WallConfig, LightConfig } from './FigmaTransformer';
+import type { MaterialLibrary } from './ShowroomMaterials';
+
+export class ShowroomLayout {
+  private scene: THREE.Scene;
+  private materials: MaterialLibrary;
+  private corridorGroup: THREE.Group;
+  private wingGroups: THREE.Group[] = [];
+  private spotLights: THREE.SpotLight[] = [];
+
+  constructor(scene: THREE.Scene, materials: MaterialLibrary) {
+    this.scene = scene;
+    this.materials = materials;
+    this.corridorGroup = new THREE.Group();
+    this.corridorGroup.name = 'showroom-corridor';
+    scene.add(this.corridorGroup);
+  }
+
+  /**
+   * Build the showroom from a SceneConfig (output of FigmaTransformer).
+   */
+  buildFromConfig(config: SceneConfig): void {
+    this.buildCorridor(config.corridor);
+    this.buildWings(config.wings);
+    this.buildWallDisplays(config.walls);
+    this.buildLighting(config.lights);
+    this.buildFurniture(config.furniture);
+    this.buildEntry(config.entry);
+  }
+
+  private buildCorridor(corridor: SceneConfig['corridor']): void {
+    const { width, depth, height } = corridor;
+    const halfW = width / 2;
+    const halfD = depth / 2;
+
+    // Floor — polished concrete with normal map
+    const floorGeo = new THREE.PlaneGeometry(width, depth, 1, 1);
+    const floor = new THREE.Mesh(floorGeo, this.materials.concrete);
+    floor.rotation.x = -Math.PI / 2;
+    floor.receiveShadow = true;
+    this.corridorGroup.add(floor);
+
+    // Left wall — exposed brick
+    const leftWallGeo = new THREE.PlaneGeometry(depth, height);
+    const leftWall = new THREE.Mesh(leftWallGeo, this.materials.brick);
+    leftWall.position.set(-halfW, height / 2, 0);
+    leftWall.rotation.y = Math.PI / 2;
+    leftWall.receiveShadow = true;
+    this.corridorGroup.add(leftWall);
+
+    // Right wall — exposed brick
+    const rightWall = new THREE.Mesh(leftWallGeo, this.materials.brick);
+    rightWall.position.set(halfW, height / 2, 0);
+    rightWall.rotation.y = -Math.PI / 2;
+    rightWall.receiveShadow = true;
+    this.corridorGroup.add(rightWall);
+
+    // Back wall — dark charcoal
+    const backWallGeo = new THREE.PlaneGeometry(width, height);
+    const backWallMat = this.materials.steel.clone();
+    backWallMat.color.setHex(0x1a1a1a);
+    const backWall = new THREE.Mesh(backWallGeo, backWallMat);
+    backWall.position.set(0, height / 2, -halfD);
+    backWall.receiveShadow = true;
+    this.corridorGroup.add(backWall);
+
+    // Ceiling — steel with exposed duct geometry
+    const ceilGeo = new THREE.PlaneGeometry(width, depth);
+    const ceiling = new THREE.Mesh(ceilGeo, this.materials.steel);
+    ceiling.rotation.x = Math.PI / 2;
+    ceiling.position.y = height;
+    this.corridorGroup.add(ceiling);
+
+    // Exposed ducts — cylindrical pipes along ceiling
+    const ductMat = this.materials.steel.clone();
+    ductMat.metalness = 0.5;
+    for (let i = 0; i < 3; i++) {
+      const ductGeo = new THREE.CylinderGeometry(0.15, 0.15, depth, 8);
+      const duct = new THREE.Mesh(ductGeo, ductMat);
+      duct.rotation.x = Math.PI / 2;
+      duct.position.set(-halfW / 2 + i * (halfW / 2), height - 0.3, 0);
+      this.corridorGroup.add(duct);
+    }
+
+    // Baseboards — warm wood trim
+    const baseGeo = new THREE.BoxGeometry(depth, 0.15, 0.04);
+    for (const xPos of [-halfW + 0.02, halfW - 0.02]) {
+      const base = new THREE.Mesh(baseGeo, this.materials.wood);
+      base.position.set(xPos, 0.075, 0);
+      base.rotation.y = Math.PI / 2;
+      this.corridorGroup.add(base);
+    }
+  }
+
+  private buildWings(wings: WingConfig[]): void {
+    wings.forEach((wingConfig, i) => {
+      const group = new THREE.Group();
+      group.name = `wing-${i}-${wingConfig.vendor}`;
+      group.userData = { vendor: wingConfig.vendor, index: i, loaded: false };
+
+      // Wing entrance frame — brushed aluminum
+      const frameGeo = new THREE.BoxGeometry(0.08, 3.5, 0.08);
+      const leftFrame = new THREE.Mesh(frameGeo, this.materials.aluminum);
+      leftFrame.position.set(-1.5, 1.75, 0);
+      group.add(leftFrame);
+
+      const rightFrame = new THREE.Mesh(frameGeo, this.materials.aluminum);
+      rightFrame.position.set(1.5, 1.75, 0);
+      group.add(rightFrame);
+
+      // Header bar
+      const headerGeo = new THREE.BoxGeometry(3.08, 0.08, 0.08);
+      const header = new THREE.Mesh(headerGeo, this.materials.aluminum);
+      header.position.set(0, 3.5, 0);
+      group.add(header);
+
+      // Vendor label (canvas texture)
+      const labelCanvas = document.createElement('canvas');
+      labelCanvas.width = 512;
+      labelCanvas.height = 64;
+      const ctx = labelCanvas.getContext('2d')!;
+      ctx.fillStyle = '#1a1a1a';
+      ctx.fillRect(0, 0, 512, 64);
+      ctx.fillStyle = '#f0ebe4';
+      ctx.font = 'bold 28px Inter, -apple-system, sans-serif';
+      ctx.textAlign = 'center';
+      ctx.fillText(wingConfig.vendor.toUpperCase(), 256, 42);
+      const labelTex = new THREE.CanvasTexture(labelCanvas);
+      const labelGeo = new THREE.PlaneGeometry(2.5, 0.3);
+      const labelMat = new THREE.MeshBasicMaterial({ map: labelTex });
+      const label = new THREE.Mesh(labelGeo, labelMat);
+      label.position.set(0, 3.7, 0.01);
+      group.add(label);
+
+      // Position the wing in the corridor
+      group.position.set(
+        wingConfig.position[0],
+        wingConfig.position[1],
+        wingConfig.position[2]
+      );
+      group.rotation.y = wingConfig.rotation;
+
+      this.wingGroups.push(group);
+      this.corridorGroup.add(group);
+    });
+  }
+
+  private buildWallDisplays(walls: WallConfig[]): void {
+    walls.forEach(wall => {
+      if (wall.hasSamplePanels) {
+        // Large mounted sample panel
+        const panelGeo = new THREE.PlaneGeometry(wall.dimensions[0], wall.dimensions[1]);
+        const panel = new THREE.Mesh(panelGeo, this.materials.fabric);
+        panel.position.set(...wall.position);
+        this.corridorGroup.add(panel);
+
+        // Aluminum frame around panel
+        const frameThickness = 0.03;
+        const frameDepth = 0.04;
+        const frameMat = this.materials.aluminum;
+
+        // Top frame
+        const topGeo = new THREE.BoxGeometry(wall.dimensions[0] + frameThickness * 2, frameThickness, frameDepth);
+        const top = new THREE.Mesh(topGeo, frameMat);
+        top.position.set(wall.position[0], wall.position[1] + wall.dimensions[1] / 2, wall.position[2] + 0.02);
+        this.corridorGroup.add(top);
+
+        // Bottom frame
+        const bottom = top.clone();
+        bottom.position.y = wall.position[1] - wall.dimensions[1] / 2;
+        this.corridorGroup.add(bottom);
+      }
+
+      if (wall.hasBookshelf) {
+        // Glass-front bookshelf
+        const shelfGeo = new THREE.BoxGeometry(wall.dimensions[0], wall.dimensions[1], 0.3);
+        const shelf = new THREE.Mesh(shelfGeo, this.materials.wood);
+        shelf.position.set(...wall.position);
+        this.corridorGroup.add(shelf);
+
+        // Glass front
+        const glassGeo = new THREE.PlaneGeometry(wall.dimensions[0] - 0.05, wall.dimensions[1] - 0.05);
+        const glass = new THREE.Mesh(glassGeo, this.materials.glass);
+        glass.position.set(wall.position[0], wall.position[1], wall.position[2] + 0.16);
+        this.corridorGroup.add(glass);
+      }
+    });
+  }
+
+  private buildLighting(lights: LightConfig[]): void {
+    lights.forEach(lightConfig => {
+      if (lightConfig.type === 'spot') {
+        const spot = new THREE.SpotLight(
+          new THREE.Color(lightConfig.color),
+          lightConfig.intensity,
+          30,  // distance
+          lightConfig.angle || Math.PI / 6,
+          lightConfig.penumbra || 0.4,
+          1    // decay
+        );
+        spot.position.set(...lightConfig.position);
+        if (lightConfig.target) {
+          spot.target.position.set(...lightConfig.target);
+          this.scene.add(spot.target);
+        }
+        spot.castShadow = true;
+        spot.shadow.mapSize.set(1024, 1024);
+        this.spotLights.push(spot);
+        this.corridorGroup.add(spot);
+
+        // Track light housing (small chrome cylinder)
+        const housingGeo = new THREE.CylinderGeometry(0.08, 0.12, 0.15, 8);
+        const housing = new THREE.Mesh(housingGeo, this.materials.trackLight);
+        housing.position.set(...lightConfig.position);
+        this.corridorGroup.add(housing);
+
+        // Volumetric light cone (subtle)
+        const coneH = lightConfig.position[1] - (lightConfig.target?.[1] || 0);
+        const coneR = Math.tan(lightConfig.angle || Math.PI / 6) * coneH;
+        const coneGeo = new THREE.ConeGeometry(coneR, coneH, 16, 1, true);
+        const coneMat = new THREE.MeshBasicMaterial({
+          color: lightConfig.color,
+          transparent: true,
+          opacity: 0.015,
+          blending: THREE.AdditiveBlending,
+          side: THREE.DoubleSide,
+          depthWrite: false,
+        });
+        const cone = new THREE.Mesh(coneGeo, coneMat);
+        cone.position.set(
+          lightConfig.position[0],
+          lightConfig.position[1] - coneH / 2,
+          lightConfig.position[2]
+        );
+        this.corridorGroup.add(cone);
+      }
+    });
+
+    // Track rails (continuous aluminum rail along ceiling)
+    const railGeo = new THREE.BoxGeometry(40, 0.03, 0.05);
+    const rail = new THREE.Mesh(railGeo, this.materials.aluminum);
+    rail.position.set(0, 11.5, 0);
+    this.corridorGroup.add(rail);
+  }
+
+  private buildFurniture(furniture: SceneConfig['furniture']): void {
+    furniture.forEach(item => {
+      if (item.type === 'display-table') {
+        const tableGeo = new THREE.BoxGeometry(...item.dimensions);
+        const table = new THREE.Mesh(tableGeo, this.materials.wood);
+        table.position.set(...item.position);
+        table.castShadow = true;
+        this.corridorGroup.add(table);
+      }
+    });
+  }
+
+  private buildEntry(entry: SceneConfig['entry']): void {
+    // Entry signage
+    const signCanvas = document.createElement('canvas');
+    signCanvas.width = 1024;
+    signCanvas.height = 128;
+    const ctx = signCanvas.getContext('2d')!;
+    ctx.fillStyle = '#1a1a1a';
+    ctx.fillRect(0, 0, 1024, 128);
+    ctx.fillStyle = '#f0ebe4';
+    ctx.font = 'bold 36px Inter, -apple-system, sans-serif';
+    ctx.textAlign = 'center';
+    ctx.fillText(entry.signText, 512, 70);
+    ctx.font = '18px Inter, -apple-system, sans-serif';
+    ctx.fillStyle = '#888888';
+    ctx.fillText('ARCHITECTURAL TRADE SHOWROOM', 512, 105);
+
+    const signTex = new THREE.CanvasTexture(signCanvas);
+    const signGeo = new THREE.PlaneGeometry(6, 0.75);
+    const signMat = new THREE.MeshStandardMaterial({
+      map: signTex,
+      emissive: new THREE.Color(0xffffff),
+      emissiveMap: signTex,
+      emissiveIntensity: 0.3,
+    });
+    const sign = new THREE.Mesh(signGeo, signMat);
+    sign.position.set(entry.position[0], 10, entry.position[2]);
+    this.corridorGroup.add(sign);
+  }
+
+  /**
+   * Get wing groups for culling/visibility management.
+   */
+  getWings(): THREE.Group[] {
+    return this.wingGroups;
+  }
+
+  /**
+   * Set visibility of wings by distance from camera for performance.
+   */
+  updateVisibility(cameraPosition: THREE.Vector3, maxDistance = 30): void {
+    this.wingGroups.forEach(wing => {
+      const dist = wing.position.distanceTo(cameraPosition);
+      wing.visible = dist < maxDistance;
+    });
+  }
+
+  dispose(): void {
+    this.corridorGroup.traverse(child => {
+      if (child instanceof THREE.Mesh) {
+        child.geometry.dispose();
+      }
+    });
+    this.scene.remove(this.corridorGroup);
+  }
+}
+```
+
+**Step 2: Commit**
+
+```bash
+git add src/showroom/ShowroomLayout.ts
+git commit -m "feat(showroom): corridor + wings scene builder with PBR materials"
+```
+
+---
+
+### Task 8: Showroom Scene Compositor
+
+**Files:**
+- Create: `/root/Projects/dw-war-room/src/showroom/Showroom.ts`
+
+**Step 1: Write the main Showroom class that ties everything together**
+
+```typescript
+// Showroom.ts — Main photorealistic showroom compositor
+// Ties together: FigmaTransformer + Materials + Environment + Layout + PostProcess
+import * as THREE from 'three';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+import { createMaterialLibrary, disposeMaterialLibrary } from './ShowroomMaterials';
+import { createShowroomEnvMap } from './ShowroomEnvironment';
+import { ShowroomLayout } from './ShowroomLayout';
+import { ShowroomPostProcess } from './ShowroomPostProcess';
+import { transformFigmaToScene, SceneConfig } from './FigmaTransformer';
+import type { MaterialLibrary } from './ShowroomMaterials';
+
+export class Showroom {
+  scene: THREE.Scene;
+  camera: THREE.PerspectiveCamera;
+  renderer: THREE.WebGLRenderer;
+  controls: OrbitControls;
+
+  private clock: THREE.Clock;
+  private materials: MaterialLibrary;
+  private layout: ShowroomLayout;
+  private postProcess: ShowroomPostProcess;
+  private dustParticles: THREE.Points;
+
+  constructor(container: HTMLElement) {
+    // Scene
+    this.scene = new THREE.Scene();
+    this.scene.background = new THREE.Color(0x0d0c0a);
+    this.scene.fog = new THREE.Fog(0x0d0c0a, 20, 60);
+
+    // Camera — human eye level
+    this.camera = new THREE.PerspectiveCamera(
+      55, container.clientWidth / container.clientHeight, 0.1, 100
+    );
+    this.camera.position.set(0, 1.7, 8);  // standing at entry
+    this.camera.lookAt(0, 1.5, 0);
+
+    // Renderer — photorealistic pipeline
+    this.renderer = new THREE.WebGLRenderer({ antialias: true });
+    this.renderer.setSize(container.clientWidth, container.clientHeight);
+    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+    this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
+    this.renderer.toneMappingExposure = 1.4;
+    this.renderer.outputColorSpace = THREE.SRGBColorSpace;
+    this.renderer.shadowMap.enabled = true;
+    this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
+    container.appendChild(this.renderer.domElement);
+
+    // Controls — walking speed
+    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
+    this.controls.enableDamping = true;
+    this.controls.dampingFactor = 0.08;
+    this.controls.maxPolarAngle = Math.PI / 2;
+    this.controls.minDistance = 1;
+    this.controls.maxDistance = 50;
+
+    // Clock
+    this.clock = new THREE.Clock();
+
+    // Materials
+    this.materials = createMaterialLibrary();
+
+    // Environment map (warm gallery IBL)
+    this.renderer.environment = createShowroomEnvMap(this.renderer) as THREE.Texture;
+
+    // Ambient + key lighting
+    this.addBaseLighting();
+
+    // Layout builder
+    this.layout = new ShowroomLayout(this.scene, this.materials);
+
+    // Atmospheric dust particles
+    this.dustParticles = this.createDustMotes();
+    this.scene.add(this.dustParticles);
+
+    // Post-processing
+    this.postProcess = new ShowroomPostProcess(this.renderer, this.scene, this.camera);
+
+    // Resize
+    window.addEventListener('resize', () => this.onResize(container));
+  }
+
+  /**
+   * Load showroom from Figma design data.
+   */
+  loadFromFigma(figmaData: any): void {
+    const config = transformFigmaToScene(figmaData);
+    this.layout.buildFromConfig(config);
+  }
+
+  /**
+   * Load showroom from a pre-built SceneConfig.
+   */
+  loadFromConfig(config: SceneConfig): void {
+    this.layout.buildFromConfig(config);
+  }
+
+  private addBaseLighting(): void {
+    // Ambient — warm neutral
+    const ambient = new THREE.AmbientLight(0x2a2520, 0.4);
+    this.scene.add(ambient);
+
+    // Hemisphere — sky/ground color contrast
+    const hemi = new THREE.HemisphereLight(0xf5e6d0, 0x1a1510, 0.3);
+    this.scene.add(hemi);
+
+    // Main key light — warm directional
+    const key = new THREE.DirectionalLight(0xf5e6d0, 0.8);
+    key.position.set(5, 15, 10);
+    key.castShadow = true;
+    key.shadow.mapSize.set(2048, 2048);
+    key.shadow.camera.near = 0.5;
+    key.shadow.camera.far = 50;
+    key.shadow.camera.left = -25;
+    key.shadow.camera.right = 25;
+    key.shadow.camera.top = 15;
+    key.shadow.camera.bottom = -15;
+    key.shadow.bias = -0.0003;
+    this.scene.add(key);
+  }
+
+  private createDustMotes(): THREE.Points {
+    const count = 200;
+    const positions = new Float32Array(count * 3);
+    for (let i = 0; i < count * 3; i += 3) {
+      positions[i] = (Math.random() - 0.5) * 40;     // x
+      positions[i + 1] = Math.random() * 12;          // y
+      positions[i + 2] = (Math.random() - 0.5) * 20;  // z
+    }
+    const geo = new THREE.BufferGeometry();
+    geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
+    const mat = new THREE.PointsMaterial({
+      color: 0xf5e6d0,
+      size: 0.02,
+      transparent: true,
+      opacity: 0.3,
+      blending: THREE.AdditiveBlending,
+      depthWrite: false,
+    });
+    return new THREE.Points(geo, mat);
+  }
+
+  animate(): void {
+    requestAnimationFrame(() => this.animate());
+    const dt = Math.min(this.clock.getDelta(), 0.1);
+    const elapsed = this.clock.getElapsedTime();
+
+    this.controls.update();
+
+    // Animate dust motes
+    const dustPos = this.dustParticles.geometry.attributes.position;
+    for (let i = 0; i < dustPos.count; i++) {
+      const y = dustPos.getY(i) + Math.sin(elapsed * 0.3 + i) * 0.001;
+      dustPos.setY(i, y > 12 ? 0 : y);
+    }
+    dustPos.needsUpdate = true;
+
+    // Update wing visibility based on camera distance
+    this.layout.updateVisibility(this.camera.position);
+
+    // Update DOF focus to orbit target distance
+    const focusDist = this.camera.position.distanceTo(this.controls.target);
+    this.postProcess.setFocusDistance(focusDist);
+
+    // Render through post-processing pipeline
+    this.postProcess.render();
+  }
+
+  private onResize(container: HTMLElement): void {
+    const w = container.clientWidth;
+    const h = container.clientHeight;
+    this.camera.aspect = w / h;
+    this.camera.updateProjectionMatrix();
+    this.renderer.setSize(w, h);
+    this.postProcess.resize(w, h);
+  }
+
+  dispose(): void {
+    this.layout.dispose();
+    disposeMaterialLibrary(this.materials);
+    this.dustParticles.geometry.dispose();
+    (this.dustParticles.material as THREE.PointsMaterial).dispose();
+    this.renderer.dispose();
+  }
+}
+```
+
+**Step 2: Commit**
+
+```bash
+git add src/showroom/Showroom.ts
+git commit -m "feat(showroom): main Showroom compositor with dust particles and DOF"
+```
+
+---
+
+### Task 9: Showroom Entry Point + HTML Page
+
+**Files:**
+- Create: `/root/Projects/dw-war-room/src/showroom/index.ts`
+- Create: `/root/Projects/dw-war-room/showroom.html`
+
+**Step 1: Write entry point**
+
+```typescript
+// src/showroom/index.ts — Showroom standalone entry point
+import { Showroom } from './Showroom';
+import type { SceneConfig } from './FigmaTransformer';
+
+// Default showroom config (used when no Figma data available)
+const DEFAULT_CONFIG: SceneConfig = {
+  corridor: { width: 40, depth: 20, height: 12, wallMaterial: 'brick', floorMaterial: 'polished-concrete' },
+  wings: Array.from({ length: 6 }, (_, i) => ({
+    index: i,
+    vendor: ['Thibaut', 'Schumacher', 'Arte', 'Scalamandre', 'Elitis', 'Phillip Jeffries'][i],
+    position: [-12 + i * 5, 0, -8] as [number, number, number],
+    rotation: 0,
+    panelCount: 12,
+    accentColor: ['#1a4b8c', '#c41e3a', '#d4a574', '#2d4a3e', '#e67e22', '#8b4513'][i],
+  })),
+  walls: [
+    { position: [-18, 4, 0], dimensions: [4, 8], material: 'brick', hasBookshelf: true, hasSamplePanels: false, sampleCount: 0 },
+    { position: [18, 4, 0], dimensions: [4, 8], material: 'brick', hasBookshelf: false, hasSamplePanels: true, sampleCount: 3 },
+  ],
+  lights: Array.from({ length: 8 }, (_, i) => ({
+    type: 'spot' as const,
+    position: [-14 + i * 4, 11, 0] as [number, number, number],
+    target: [-14 + i * 4, 0, 0] as [number, number, number],
+    color: '#f5e6d0',
+    intensity: 1.2,
+    angle: Math.PI / 5,
+    penumbra: 0.4,
+  })),
+  furniture: [
+    { type: 'display-table' as const, position: [0, 0.45, 0] as [number, number, number], dimensions: [3, 0.9, 1.5] as [number, number, number] },
+  ],
+  entry: { position: [0, 0, 10], signText: 'DESIGNER WALLCOVERINGS' },
+};
+
+// Boot
+const container = document.getElementById('showroom-container');
+if (container) {
+  const showroom = new Showroom(container);
+  showroom.loadFromConfig(DEFAULT_CONFIG);
+  showroom.animate();
+
+  // Expose for MCP/debug
+  (window as any).__showroom = showroom;
+}
+```
+
+**Step 2: Write HTML page**
+
+```html
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>DW Showroom — Photorealistic 3D</title>
+  <style>
+    * { margin: 0; padding: 0; box-sizing: border-box; }
+    body { overflow: hidden; background: #0d0c0a; font-family: Inter, -apple-system, sans-serif; }
+    #showroom-container { width: 100vw; height: 100vh; }
+  </style>
+</head>
+<body>
+  <div id="showroom-container"></div>
+  <script type="module" src="/src/showroom/index.ts"></script>
+</body>
+</html>
+```
+
+**Step 3: Add Vite route for showroom page**
+
+Modify `vite.config.ts` to add the showroom entry:
+
+```typescript
+import { defineConfig } from 'vite';
+import { resolve } from 'path';
+
+export default defineConfig({
+  server: { port: 4060, host: '0.0.0.0' },
+  preview: { port: 4060, host: '0.0.0.0' },
+  build: {
+    rollupOptions: {
+      input: {
+        main: resolve(__dirname, 'index.html'),
+        showroom: resolve(__dirname, 'showroom.html'),
+      },
+    },
+  },
+});
+```
+
+**Step 4: Commit**
+
+```bash
+git add src/showroom/index.ts showroom.html vite.config.ts
+git commit -m "feat(showroom): standalone showroom page with default 6-vendor config"
+```
+
+---
+
+## Phase 5: Boardroom-3D Integration (Port 7681)
+
+### Task 10: Backport Photorealistic Materials to wr-wings.js
+
+**Files:**
+- Modify: `/root/DW-Agents/boardroom-3d/public/js/modules/wr-wings.js`
+- Create: `/root/DW-Agents/boardroom-3d/public/js/modules/wr-pbr-materials.js`
+
+**Step 1: Create PBR materials module for the vanilla JS system**
+
+Write a new module `wr-pbr-materials.js` that provides the same procedural normal maps and material factory, adapted for the older Three.js CDN version (r128). This module follows the existing `window.WR` namespace pattern.
+
+Key adaptations:
+- Use `THREE.MeshStandardMaterial` (available in r128+)
+- Canvas normal maps generated identically to the TypeScript version
+- Exposed via `window.DWS.materials` namespace
+
+**Step 2: Integrate into wr-wings.js wing panel materials**
+
+Replace the existing `MeshStandardMaterial({ roughness: 0.35, metalness: 0.0 })` wing panel material with the new PBR material that includes normal maps.
+
+**Step 3: Build and test**
+
+```bash
+cd /root/DW-Agents/boardroom-3d
+node build-warroom.js
+node validate-warroom.js
+```
+
+**Step 4: Commit**
+
+```bash
+git add public/js/modules/wr-pbr-materials.js public/js/modules/wr-wings.js
+git commit -m "feat(boardroom-3d): PBR materials with procedural normal maps for showroom wings"
+```
+
+---
+
+### Task 11: Backport Post-Processing to wr-effects.js
+
+**Files:**
+- Modify: `/root/DW-Agents/boardroom-3d/public/js/modules/wr-effects.js`
+
+**Step 1: Add SSAO and color grading to the existing effects pipeline**
+
+The boardroom-3d system currently has bloom disabled by default (performance). Add optional SSAO and color grading that can be toggled:
+
+```javascript
+// In wr-effects.js — add after existing toggleBloom function
+window.toggleSSAO = function() { /* ... */ };
+window.toggleColorGrading = function() { /* ... */ };
+window.setQualityPreset = function(level) {
+  // 'performance' = no post-processing
+  // 'balanced' = bloom only
+  // 'quality' = bloom + color grading
+  // 'ultra' = bloom + SSAO + color grading + grain
+};
+```
+
+**Step 2: Build and validate**
+
+```bash
+node build-warroom.js && node validate-warroom.js
+```
+
+**Step 3: Commit**
+
+```bash
+git commit -am "feat(boardroom-3d): quality presets with SSAO and color grading"
+```
+
+---
+
+## Phase 6: Testing + Polish
+
+### Task 12: Visual Verification
+
+**Step 1: Start dev server and verify war room showroom**
+
+```bash
+cd /root/Projects/dw-war-room && npm run dev
+```
+
+Navigate to `http://45.61.58.125:4060/showroom.html` and verify:
+- Brick walls have visible mortar normal map detail
+- Polished concrete floor reflects environment
+- Track lights cast visible spot cones
+- SSAO darkens wall-floor junctions
+- DOF blurs background when looking at near wing
+- Color grading gives warm architectural tone
+- 30+ FPS maintained
+
+**Step 2: Verify boardroom-3d showroom**
+
+```bash
+cd /root/DW-Agents/boardroom-3d && pm2 restart boardroom-3d
+```
+
+Navigate to showroom wings at `http://45.61.58.125:7681/` and verify PBR materials apply.
+
+**Step 3: Performance audit**
+
+Check draw calls, FPS, texture memory via browser dev tools:
+- Target: <300 draw calls, 30+ FPS, <256MB textures
+
+---
+
+### Task 13: Final Commit + Documentation
+
+**Step 1: Update CLAUDE.md with showroom architecture**
+
+Add showroom module documentation to `/root/Projects/dw-war-room/CLAUDE.md`.
+
+**Step 2: Final commit**
+
+```bash
+cd /root/Projects/dw-war-room
+git add -A
+git commit -m "feat(showroom): complete Figma + Three.js photorealistic showroom pipeline"
+```
+
+**Step 3: Push to GitHub**
+
+```bash
+git push origin main
+```
+
+---
+
+## Summary
+
+| Phase | Tasks | Key Deliverables |
+|-------|-------|-----------------|
+| 1 | Tasks 1-2 | Figma design + MCP extraction pipeline |
+| 2 | Tasks 3-4 | Procedural normal maps + PBR material factory |
+| 3 | Tasks 5-6 | HDR environment + SSAO/DOF/color grading |
+| 4 | Tasks 7-9 | Showroom layout builder + entry point |
+| 5 | Tasks 10-11 | Boardroom-3D backport |
+| 6 | Tasks 12-13 | Testing + documentation |
+
+**Total new files:** ~8 TypeScript modules + 1 HTML page + 1 JS module
+**Estimated draw calls:** ~150 base corridor, ~250 with 3 wings visible
+**Target FPS:** 30+ at 1080p on desktop GPU
diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs
new file mode 100644
index 0000000..a00bbf0
--- /dev/null
+++ b/ecosystem.config.cjs
@@ -0,0 +1,13 @@
+module.exports = {
+  apps: [
+    {
+      name: 'dw-war-room',
+      script: './serve.cjs',
+      cwd: '/root/Projects/dw-war-room',
+      env: {
+        NODE_ENV: 'production',
+      },
+      max_memory_restart: '100M',
+    }
+  ]
+};
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..76e6953
--- /dev/null
+++ b/index.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>DW War Room</title>
+  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
+  <style>
+    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+    html, body {
+      width: 100%; height: 100%; overflow: hidden;
+      font-family: 'Inter', -apple-system, system-ui, sans-serif;
+      background: #050510;
+      color: #e8eaf0;
+    }
+    #war-room {
+      width: 100vw; height: 100vh;
+      position: relative;
+    }
+    canvas { display: block; }
+    ::-webkit-scrollbar { width: 4px; }
+    ::-webkit-scrollbar-track { background: transparent; }
+    ::-webkit-scrollbar-thumb { background: #252540; border-radius: 2px; }
+  </style>
+</head>
+<body>
+  <div id="war-room"></div>
+  <script type="module" src="/src/main.ts"></script>
+</body>
+</html>
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..ea92931
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1186 @@
+{
+  "name": "dw-war-room",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "dw-war-room",
+      "version": "1.0.0",
+      "dependencies": {
+        "three": "^0.170.0"
+      },
+      "devDependencies": {
+        "@types/three": "^0.170.0",
+        "typescript": "^5.7.2",
+        "vite": "^6.0.3"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+      "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+      "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+      "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+      "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+      "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+      "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+      "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+      "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+      "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+      "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+      "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+      "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+      "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+      "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+      "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+      "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+      "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+      "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+      "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+      "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+      "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+      "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+      "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz",
+      "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz",
+      "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz",
+      "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz",
+      "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz",
+      "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz",
+      "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz",
+      "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz",
+      "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz",
+      "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz",
+      "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-gnu": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz",
+      "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-musl": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz",
+      "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz",
+      "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-musl": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz",
+      "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz",
+      "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz",
+      "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz",
+      "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz",
+      "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz",
+      "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openbsd-x64": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz",
+      "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz",
+      "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz",
+      "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz",
+      "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-gnu": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz",
+      "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz",
+      "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@tweenjs/tween.js": {
+      "version": "23.1.3",
+      "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
+      "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/stats.js": {
+      "version": "0.17.4",
+      "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
+      "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/three": {
+      "version": "0.170.0",
+      "resolved": "https://registry.npmjs.org/@types/three/-/three-0.170.0.tgz",
+      "integrity": "sha512-CUm2uckq+zkCY7ZbFpviRttY+6f9fvwm6YqSqPfA5K22s9w7R4VnA3rzJse8kHVvuzLcTx+CjNCs2NYe0QFAyg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@tweenjs/tween.js": "~23.1.3",
+        "@types/stats.js": "*",
+        "@types/webxr": "*",
+        "@webgpu/types": "*",
+        "fflate": "~0.8.2",
+        "meshoptimizer": "~0.18.1"
+      }
+    },
+    "node_modules/@types/webxr": {
+      "version": "0.5.24",
+      "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
+      "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@webgpu/types": {
+      "version": "0.1.69",
+      "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz",
+      "integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==",
+      "dev": true,
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/esbuild": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+      "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.25.12",
+        "@esbuild/android-arm": "0.25.12",
+        "@esbuild/android-arm64": "0.25.12",
+        "@esbuild/android-x64": "0.25.12",
+        "@esbuild/darwin-arm64": "0.25.12",
+        "@esbuild/darwin-x64": "0.25.12",
+        "@esbuild/freebsd-arm64": "0.25.12",
+        "@esbuild/freebsd-x64": "0.25.12",
+        "@esbuild/linux-arm": "0.25.12",
+        "@esbuild/linux-arm64": "0.25.12",
+        "@esbuild/linux-ia32": "0.25.12",
+        "@esbuild/linux-loong64": "0.25.12",
+        "@esbuild/linux-mips64el": "0.25.12",
+        "@esbuild/linux-ppc64": "0.25.12",
+        "@esbuild/linux-riscv64": "0.25.12",
+        "@esbuild/linux-s390x": "0.25.12",
+        "@esbuild/linux-x64": "0.25.12",
+        "@esbuild/netbsd-arm64": "0.25.12",
+        "@esbuild/netbsd-x64": "0.25.12",
+        "@esbuild/openbsd-arm64": "0.25.12",
+        "@esbuild/openbsd-x64": "0.25.12",
+        "@esbuild/openharmony-arm64": "0.25.12",
+        "@esbuild/sunos-x64": "0.25.12",
+        "@esbuild/win32-arm64": "0.25.12",
+        "@esbuild/win32-ia32": "0.25.12",
+        "@esbuild/win32-x64": "0.25.12"
+      }
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/fflate": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+      "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/meshoptimizer": {
+      "version": "0.18.1",
+      "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
+      "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+      "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.6",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+      "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "4.60.0",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz",
+      "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.8"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.60.0",
+        "@rollup/rollup-android-arm64": "4.60.0",
+        "@rollup/rollup-darwin-arm64": "4.60.0",
+        "@rollup/rollup-darwin-x64": "4.60.0",
+        "@rollup/rollup-freebsd-arm64": "4.60.0",
+        "@rollup/rollup-freebsd-x64": "4.60.0",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.60.0",
+        "@rollup/rollup-linux-arm-musleabihf": "4.60.0",
+        "@rollup/rollup-linux-arm64-gnu": "4.60.0",
+        "@rollup/rollup-linux-arm64-musl": "4.60.0",
+        "@rollup/rollup-linux-loong64-gnu": "4.60.0",
+        "@rollup/rollup-linux-loong64-musl": "4.60.0",
+        "@rollup/rollup-linux-ppc64-gnu": "4.60.0",
+        "@rollup/rollup-linux-ppc64-musl": "4.60.0",
+        "@rollup/rollup-linux-riscv64-gnu": "4.60.0",
+        "@rollup/rollup-linux-riscv64-musl": "4.60.0",
+        "@rollup/rollup-linux-s390x-gnu": "4.60.0",
+        "@rollup/rollup-linux-x64-gnu": "4.60.0",
+        "@rollup/rollup-linux-x64-musl": "4.60.0",
+        "@rollup/rollup-openbsd-x64": "4.60.0",
+        "@rollup/rollup-openharmony-arm64": "4.60.0",
+        "@rollup/rollup-win32-arm64-msvc": "4.60.0",
+        "@rollup/rollup-win32-ia32-msvc": "4.60.0",
+        "@rollup/rollup-win32-x64-gnu": "4.60.0",
+        "@rollup/rollup-win32-x64-msvc": "4.60.0",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/three": {
+      "version": "0.170.0",
+      "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz",
+      "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==",
+      "license": "MIT"
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.15",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+      "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/vite": {
+      "version": "6.4.1",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
+      "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.25.0",
+        "fdir": "^6.4.4",
+        "picomatch": "^4.0.2",
+        "postcss": "^8.5.3",
+        "rollup": "^4.34.9",
+        "tinyglobby": "^0.2.13"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+        "jiti": ">=1.21.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "sass-embedded": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..4675458
--- /dev/null
+++ b/package.json
@@ -0,0 +1,19 @@
+{
+  "name": "dw-war-room",
+  "private": true,
+  "version": "1.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "three": "^0.170.0"
+  },
+  "devDependencies": {
+    "@types/three": "^0.170.0",
+    "typescript": "^5.7.2",
+    "vite": "^6.0.3"
+  }
+}
diff --git a/serve.cjs b/serve.cjs
new file mode 100644
index 0000000..ca3bbcc
--- /dev/null
+++ b/serve.cjs
@@ -0,0 +1,185 @@
+// Simple static server with Basic Auth for War Room
+// Includes Shopify search proxy endpoint for showroom live search
+// Uses Shopify GraphQL Admin API for fuzzy product search
+const http = require('http');
+const https = require('https');
+const fs = require('fs');
+const path = require('path');
+const url = require('url');
+
+const PORT = 4060;
+const USER = 'admin';
+const PASS = 'DWSecure2024!';
+const DIST = path.join(__dirname, 'dist');
+
+// Shopify API config (Admin API — server-side only)
+const SHOPIFY_STORE = 'designer-laboratory-sandbox.myshopify.com';
+const SHOPIFY_TOKEN = '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';
+const SHOPIFY_API_VERSION = '2024-01';
+
+const MIME = {
+  '.html': 'text/html',
+  '.js': 'application/javascript',
+  '.css': 'text/css',
+  '.json': 'application/json',
+  '.png': 'image/png',
+  '.jpg': 'image/jpeg',
+  '.svg': 'image/svg+xml',
+  '.woff2': 'font/woff2',
+  '.woff': 'font/woff',
+};
+
+/**
+ * Proxy Shopify product search via GraphQL — keeps API token server-side.
+ * Uses GraphQL for proper fuzzy search (REST title= requires exact match).
+ * GET /api/shopify/search?q=QUERY&limit=10
+ * Returns: { products: [{ id, title, vendor, product_type, handle, images, variants, tags }] }
+ */
+function handleShopifySearch(query, limit, res) {
+  if (!query || query.length < 2) {
+    res.writeHead(400, { 'Content-Type': 'application/json' });
+    return res.end(JSON.stringify({ error: 'Query must be at least 2 characters' }));
+  }
+
+  limit = Math.min(parseInt(limit, 10) || 10, 20);
+
+  // GraphQL query — searches across title, vendor, tags, etc.
+  const gqlQuery = JSON.stringify({
+    query: `{
+      products(first: ${limit}, query: "${query.replace(/"/g, '\\"')}") {
+        edges {
+          node {
+            id
+            title
+            vendor
+            productType
+            handle
+            tags
+            featuredImage {
+              url
+              altText
+            }
+            images(first: 1) {
+              edges {
+                node {
+                  url
+                  altText
+                }
+              }
+            }
+            variants(first: 1) {
+              edges {
+                node {
+                  price
+                  sku
+                }
+              }
+            }
+          }
+        }
+      }
+    }`
+  });
+
+  const options = {
+    hostname: SHOPIFY_STORE,
+    path: `/admin/api/${SHOPIFY_API_VERSION}/graphql.json`,
+    method: 'POST',
+    headers: {
+      'X-Shopify-Access-Token': SHOPIFY_TOKEN,
+      'Content-Type': 'application/json',
+      'Content-Length': Buffer.byteLength(gqlQuery),
+    },
+  };
+
+  const req2 = https.request(options, (shopifyRes) => {
+    let data = '';
+    shopifyRes.on('data', (chunk) => { data += chunk; });
+    shopifyRes.on('end', () => {
+      try {
+        const gqlResponse = JSON.parse(data);
+        const edges = gqlResponse?.data?.products?.edges || [];
+
+        // Transform GraphQL response to REST-like format for frontend compatibility
+        const products = edges.map(({ node }) => ({
+          id: parseInt(node.id.replace('gid://shopify/Product/', ''), 10),
+          title: node.title,
+          vendor: node.vendor,
+          product_type: node.productType,
+          handle: node.handle,
+          tags: node.tags ? node.tags.join(', ') : '',
+          images: node.images?.edges?.length
+            ? node.images.edges.map(e => ({ src: e.node.url }))
+            : node.featuredImage
+              ? [{ src: node.featuredImage.url }]
+              : [],
+          variants: node.variants?.edges?.length
+            ? node.variants.edges.map(e => ({ price: e.node.price, sku: e.node.sku || '' }))
+            : [{ price: '0.00', sku: '' }],
+        }));
+
+        res.writeHead(200, {
+          'Content-Type': 'application/json',
+          'Cache-Control': 'max-age=30',
+        });
+        res.end(JSON.stringify({ products }));
+      } catch (parseErr) {
+        console.error('[Shopify Search Proxy] Parse error:', parseErr.message);
+        res.writeHead(502, { 'Content-Type': 'application/json' });
+        res.end(JSON.stringify({ error: 'Failed to parse Shopify response', products: [] }));
+      }
+    });
+  });
+
+  req2.on('error', (err) => {
+    console.error('[Shopify Search Proxy] Error:', err.message);
+    res.writeHead(502, { 'Content-Type': 'application/json' });
+    res.end(JSON.stringify({ error: 'Shopify API request failed', products: [] }));
+  });
+
+  req2.write(gqlQuery);
+  req2.end();
+}
+
+const server = http.createServer((req, res) => {
+  // Basic Auth check
+  const auth = req.headers.authorization;
+  if (!auth || !auth.startsWith('Basic ')) {
+    res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="DW War Room"' });
+    return res.end('Unauthorized');
+  }
+  const [u, p] = Buffer.from(auth.slice(6), 'base64').toString().split(':');
+  if (u !== USER || p !== PASS) {
+    res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="DW War Room"' });
+    return res.end('Unauthorized');
+  }
+
+  // Parse URL
+  const parsedUrl = url.parse(req.url, true);
+  const pathname = parsedUrl.pathname;
+
+  // API routes — handle before static file serving
+  if (pathname === '/api/shopify/search' && req.method === 'GET') {
+    return handleShopifySearch(parsedUrl.query.q, parsedUrl.query.limit, res);
+  }
+
+  // Serve static files
+  let filePath = path.join(DIST, pathname === '/' ? 'index.html' : pathname);
+  if (!fs.existsSync(filePath)) filePath = path.join(DIST, 'index.html');
+
+  const ext = path.extname(filePath);
+  const mime = MIME[ext] || 'application/octet-stream';
+
+  try {
+    const content = fs.readFileSync(filePath);
+    res.writeHead(200, { 'Content-Type': mime });
+    res.end(content);
+  } catch {
+    res.writeHead(404);
+    res.end('Not found');
+  }
+});
+
+server.listen(PORT, '0.0.0.0', () => {
+  console.log(`[War Room] Serving on port ${PORT} with auth + Shopify search proxy (GraphQL)`);
+});
diff --git a/showroom.html b/showroom.html
new file mode 100644
index 0000000..2fcc9b1
--- /dev/null
+++ b/showroom.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>DW Showroom — Photorealistic 3D</title>
+  <style>
+    * { margin: 0; padding: 0; box-sizing: border-box; }
+    body { overflow: hidden; background: #0d0c0a; font-family: Inter, -apple-system, sans-serif; }
+    #showroom-container { width: 100vw; height: 100vh; }
+  </style>
+</head>
+<body>
+  <div id="showroom-container"></div>
+  <script type="module" src="/src/showroom/index.ts"></script>
+</body>
+</html>
diff --git a/src/data/ApiClient.ts b/src/data/ApiClient.ts
new file mode 100644
index 0000000..73e9776
--- /dev/null
+++ b/src/data/ApiClient.ts
@@ -0,0 +1,78 @@
+// ═══════════════════════════════════════════════
+// DW War Room 3D — API Client
+// Fetches from both Governance (4020) and Boardroom (4040)
+// ═══════════════════════════════════════════════
+
+import type { DecisionStats, EscalationAlert, GovernanceStatus, MeetingState } from '../types';
+
+const GOV_URL = import.meta.env.VITE_GOVERNANCE_URL || 'http://45.61.58.125:4020';
+const BR_URL = import.meta.env.VITE_BOARDROOM_URL || 'http://45.61.58.125:4040';
+const AUTH = import.meta.env.VITE_AUTH || btoa('admin:DWSecure2024!');
+
+const headers: Record<string, string> = {
+  'Content-Type': 'application/json',
+  'Authorization': `Basic ${AUTH}`,
+};
+
+async function fetchJson<T>(url: string): Promise<T> {
+  const res = await fetch(url, { headers });
+  if (!res.ok) throw new Error(`${res.status}`);
+  return res.json();
+}
+
+// Governance API (port 4020)
+export async function getGovernanceStatus(): Promise<GovernanceStatus> {
+  return fetchJson<GovernanceStatus>(`${GOV_URL}/api/governance/status`);
+}
+
+export async function getDecisionStats(): Promise<DecisionStats> {
+  return fetchJson<DecisionStats>(`${GOV_URL}/api/decisions/stats`);
+}
+
+export async function getActiveEscalations(): Promise<EscalationAlert[]> {
+  return fetchJson<EscalationAlert[]>(`${GOV_URL}/api/escalations/active`);
+}
+
+export async function getPendingDecisions(): Promise<any[]> {
+  return fetchJson<any[]>(`${GOV_URL}/api/decisions?status=pending`);
+}
+
+// Boardroom API (port 4040)
+export async function getBoardroomStatus(): Promise<any> {
+  return fetchJson<any>(`${BR_URL}/api/status`);
+}
+
+export async function getActiveMeeting(): Promise<MeetingState> {
+  const data = await fetchJson<any>(`${BR_URL}/api/meetings/active`);
+  return {
+    active: data.active,
+    id: data.meeting?.id,
+    type: data.meeting?.meeting_type,
+    phase: data.meeting?.phase,
+    status: data.meeting?.status,
+    messages: data.messages || [],
+  };
+}
+
+export async function getAgents(): Promise<any> {
+  return fetchJson<any>(`${BR_URL}/api/agents`);
+}
+
+export async function getTeams(): Promise<any> {
+  return fetchJson<any>(`${BR_URL}/api/agents/teams`);
+}
+
+// System health from boardroom proxy
+export async function getSystemHealth(): Promise<any> {
+  return fetchJson<any>(`${BR_URL}/api/system-health`);
+}
+
+// Activity feed from boardroom proxy
+export async function getFeed(): Promise<any[]> {
+  return fetchJson<any[]>(`${BR_URL}/api/feed`);
+}
+
+// Agent schedules / activity
+export async function getAgentActivity(): Promise<any[]> {
+  return fetchJson<any[]>(`${BR_URL}/api/agent-activity`);
+}
diff --git a/src/data/WebSocketClient.ts b/src/data/WebSocketClient.ts
new file mode 100644
index 0000000..1d0ecbe
--- /dev/null
+++ b/src/data/WebSocketClient.ts
@@ -0,0 +1,75 @@
+// ═══════════════════════════════════════════════
+// DW War Room 3D — Dual WebSocket Client
+// Connects to both Governance and Boardroom WS servers
+// ═══════════════════════════════════════════════
+
+import type { WSEvent } from '../types';
+
+type EventHandler = (event: WSEvent, source: 'governance' | 'boardroom') => void;
+
+export class DualWebSocketClient {
+  private govWs: WebSocket | null = null;
+  private brWs: WebSocket | null = null;
+  private handlers: EventHandler[] = [];
+  private reconnectTimers: { gov?: number; br?: number } = {};
+
+  constructor(
+    private govUrl: string = import.meta.env.VITE_GOVERNANCE_WS || 'ws://45.61.58.125:4020/ws',
+    private brUrl: string = import.meta.env.VITE_BOARDROOM_WS || 'ws://45.61.58.125:4040/ws'
+  ) {}
+
+  onEvent(handler: EventHandler): void {
+    this.handlers.push(handler);
+  }
+
+  connect(): void {
+    this.connectGov();
+    this.connectBr();
+  }
+
+  disconnect(): void {
+    if (this.govWs) this.govWs.close();
+    if (this.brWs) this.brWs.close();
+    if (this.reconnectTimers.gov) clearTimeout(this.reconnectTimers.gov);
+    if (this.reconnectTimers.br) clearTimeout(this.reconnectTimers.br);
+  }
+
+  private connectGov(): void {
+    try {
+      this.govWs = new WebSocket(this.govUrl);
+      this.govWs.onmessage = (evt) => this.dispatch(evt.data, 'governance');
+      this.govWs.onclose = () => {
+        this.reconnectTimers.gov = window.setTimeout(() => this.connectGov(), 5000);
+      };
+      this.govWs.onerror = () => this.govWs?.close();
+    } catch {}
+  }
+
+  private connectBr(): void {
+    try {
+      this.brWs = new WebSocket(this.brUrl);
+      this.brWs.onmessage = (evt) => this.dispatch(evt.data, 'boardroom');
+      this.brWs.onclose = () => {
+        this.reconnectTimers.br = window.setTimeout(() => this.connectBr(), 5000);
+      };
+      this.brWs.onerror = () => this.brWs?.close();
+    } catch {}
+  }
+
+  private dispatch(data: string, source: 'governance' | 'boardroom'): void {
+    try {
+      const event: WSEvent = JSON.parse(data);
+      for (const handler of this.handlers) {
+        handler(event, source);
+      }
+    } catch {}
+  }
+
+  get govConnected(): boolean {
+    return this.govWs?.readyState === WebSocket.OPEN;
+  }
+
+  get brConnected(): boolean {
+    return this.brWs?.readyState === WebSocket.OPEN;
+  }
+}
diff --git a/src/effects/Particles.ts b/src/effects/Particles.ts
new file mode 100644
index 0000000..cf44bfb
--- /dev/null
+++ b/src/effects/Particles.ts
@@ -0,0 +1,308 @@
+// ═══════════════════════════════════════════════
+// DW War Room 3D — Ambient Particle System
+// Data flow visualization: dust motes, data
+// streams, and connection pulse dots
+// ═══════════════════════════════════════════════
+
+import * as THREE from 'three';
+
+// ─── Constants ────────────────────────────────
+const DUST_COUNT       = 300;
+const STREAM_COUNT     = 150;
+const PULSE_COUNT      = 50;
+
+// ─── Internal state per particle group ────────
+
+interface DustMote {
+  baseX: number;
+  baseY: number;
+  baseZ: number;
+  phaseX: number;
+  phaseY: number;
+  phaseZ: number;
+  speed: number;
+  amplitude: number;
+}
+
+interface StreamParticle {
+  x: number;
+  z: number;
+  y: number;
+  speed: number;
+  spiralRadius: number;
+  spiralPhase: number;
+  spiralSpeed: number;
+}
+
+interface PulseDot {
+  startX: number;
+  startZ: number;
+  endX: number;
+  endZ: number;
+  progress: number;       // 0 → 1 → 0 (ping-pong toward center and back)
+  speed: number;
+  direction: number;      // +1 = toward center, -1 = away from center
+}
+
+// ─── ParticleSystem ───────────────────────────
+
+export class ParticleSystem {
+  private scene: THREE.Scene;
+
+  // Group 1 — Ambient dust motes
+  private dustPoints: THREE.Points;
+  private dustPositions: Float32Array;
+  private dustMeta: DustMote[] = [];
+
+  // Group 2 — Data stream particles
+  private streamPoints: THREE.Points;
+  private streamPositions: Float32Array;
+  private streamMeta: StreamParticle[] = [];
+
+  // Group 3 — Connection pulse dots
+  private pulsePoints: THREE.Points;
+  private pulsePositions: Float32Array;
+  private pulseMeta: PulseDot[] = [];
+
+  constructor(scene: THREE.Scene) {
+    this.scene = scene;
+
+    this.dustPoints   = this.buildDustMotes();
+    this.streamPoints = this.buildDataStreams();
+    this.pulsePoints  = this.buildPulseDots();
+
+    this.scene.add(this.dustPoints);
+    this.scene.add(this.streamPoints);
+    this.scene.add(this.pulsePoints);
+  }
+
+  // ── Group 1: Ambient dust motes ──────────────
+
+  private buildDustMotes(): THREE.Points {
+    const positions = new Float32Array(DUST_COUNT * 3);
+
+    for (let i = 0; i < DUST_COUNT; i++) {
+      const x = (Math.random() - 0.5) * 40;   // -20 to 20
+      const y = Math.random() * 15;            //  0  to 15
+      const z = (Math.random() - 0.5) * 40;   // -20 to 20
+
+      positions[i * 3]     = x;
+      positions[i * 3 + 1] = y;
+      positions[i * 3 + 2] = z;
+
+      this.dustMeta.push({
+        baseX:     x,
+        baseY:     y,
+        baseZ:     z,
+        phaseX:    Math.random() * Math.PI * 2,
+        phaseY:    Math.random() * Math.PI * 2,
+        phaseZ:    Math.random() * Math.PI * 2,
+        speed:     0.0003 + Math.random() * 0.0004,
+        amplitude: 0.15 + Math.random() * 0.25,
+      });
+    }
+
+    this.dustPositions = positions;
+
+    const geo = new THREE.BufferGeometry();
+    geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
+
+    const mat = new THREE.PointsMaterial({
+      color:           0x8b5cf6,
+      size:            0.03,
+      sizeAttenuation: true,
+      transparent:     true,
+      opacity:         0.3,
+      depthWrite:      false,
+    });
+
+    return new THREE.Points(geo, mat);
+  }
+
+  // ── Group 2: Data stream particles ───────────
+
+  private buildDataStreams(): THREE.Points {
+    const positions = new Float32Array(STREAM_COUNT * 3);
+
+    for (let i = 0; i < STREAM_COUNT; i++) {
+      // Random angle and radius within a circle of radius 6 around origin
+      const angle  = Math.random() * Math.PI * 2;
+      const radius = Math.random() * 6;
+      const x      = Math.cos(angle) * radius;
+      const z      = Math.sin(angle) * radius;
+      const y      = 0.5 + Math.random() * 7.5;  // spread from 0.5 to 8
+
+      positions[i * 3]     = x;
+      positions[i * 3 + 1] = y;
+      positions[i * 3 + 2] = z;
+
+      this.streamMeta.push({
+        x,
+        z,
+        y,
+        speed:        0.003 + Math.random() * 0.008,
+        spiralRadius: 0.1 + Math.random() * 0.4,
+        spiralPhase:  Math.random() * Math.PI * 2,
+        spiralSpeed:  0.5 + Math.random() * 1.0,
+      });
+    }
+
+    this.streamPositions = positions;
+
+    const geo = new THREE.BufferGeometry();
+    geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
+
+    const mat = new THREE.PointsMaterial({
+      color:           0x93c5fd,
+      size:            0.05,
+      sizeAttenuation: true,
+      transparent:     true,
+      opacity:         0.7,
+      depthWrite:      false,
+    });
+
+    return new THREE.Points(geo, mat);
+  }
+
+  // ── Group 3: Connection pulse dots ───────────
+
+  private buildPulseDots(): THREE.Points {
+    const positions = new Float32Array(PULSE_COUNT * 3);
+
+    for (let i = 0; i < PULSE_COUNT; i++) {
+      const { startX, startZ, endX, endZ } = this.randomPulsePath();
+      const progress = Math.random();   // stagger initial positions
+
+      positions[i * 3]     = THREE.MathUtils.lerp(startX, endX, progress);
+      positions[i * 3 + 1] = 3.5;
+      positions[i * 3 + 2] = THREE.MathUtils.lerp(startZ, endZ, progress);
+
+      this.pulseMeta.push({
+        startX,
+        startZ,
+        endX,
+        endZ,
+        progress,
+        speed:     0.004 + Math.random() * 0.006,
+        direction: Math.random() > 0.5 ? 1 : -1,
+      });
+    }
+
+    this.pulsePositions = positions;
+
+    const geo = new THREE.BufferGeometry();
+    geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
+
+    const mat = new THREE.PointsMaterial({
+      color:           0x22c55e,
+      size:            0.06,
+      sizeAttenuation: true,
+      transparent:     true,
+      opacity:         0.4,
+      depthWrite:      false,
+    });
+
+    return new THREE.Points(geo, mat);
+  }
+
+  // Pick an edge start and a center-region end for a pulse path.
+  // "Edge" = between radius 8 and 14 on the horizontal plane.
+  private randomPulsePath(): { startX: number; startZ: number; endX: number; endZ: number } {
+    const edgeAngle  = Math.random() * Math.PI * 2;
+    const edgeRadius = 8 + Math.random() * 6;        // 8–14
+
+    const startX = Math.cos(edgeAngle) * edgeRadius;
+    const startZ = Math.sin(edgeAngle) * edgeRadius;
+
+    // End near the center table (within radius 3)
+    const centerAngle  = Math.random() * Math.PI * 2;
+    const centerRadius = Math.random() * 3;
+    const endX         = Math.cos(centerAngle) * centerRadius;
+    const endZ         = Math.sin(centerAngle) * centerRadius;
+
+    return { startX, startZ, endX, endZ };
+  }
+
+  // ── animate ──────────────────────────────────
+  //
+  // elapsed: milliseconds from THREE.Clock.getElapsedTime() * 1000
+  // (matches the convention used in WarRoom.ts)
+
+  animate(elapsed: number): void {
+    const t = elapsed * 0.001;   // convert to seconds for trig functions
+    this.animateDust(t);
+    this.animateStreams(t);
+    this.animatePulse();
+  }
+
+  private animateDust(t: number): void {
+    for (let i = 0; i < DUST_COUNT; i++) {
+      const m = this.dustMeta[i];
+      const base = i * 3;
+
+      this.dustPositions[base]     = m.baseX + Math.cos(t * m.speed * 1000 + m.phaseX) * m.amplitude;
+      this.dustPositions[base + 1] = m.baseY + Math.sin(t * m.speed * 700  + m.phaseY) * m.amplitude * 0.5;
+      this.dustPositions[base + 2] = m.baseZ + Math.cos(t * m.speed * 800  + m.phaseZ) * m.amplitude;
+    }
+
+    (this.dustPoints.geometry.attributes.position as THREE.BufferAttribute).needsUpdate = true;
+  }
+
+  private animateStreams(t: number): void {
+    for (let i = 0; i < STREAM_COUNT; i++) {
+      const m    = this.streamMeta[i];
+      const base = i * 3;
+
+      // Rise upward
+      m.y += m.speed;
+
+      // Respawn at bottom when reaching ceiling
+      if (m.y >= 8) {
+        const angle  = Math.random() * Math.PI * 2;
+        const radius = Math.random() * 6;
+        m.x = Math.cos(angle) * radius;
+        m.z = Math.sin(angle) * radius;
+        m.y = 0.5;
+        m.spiralPhase = Math.random() * Math.PI * 2;
+      }
+
+      // Spiral offset around the base x/z position
+      const spiralAngle = t * m.spiralSpeed + m.spiralPhase;
+      this.streamPositions[base]     = m.x + Math.cos(spiralAngle) * m.spiralRadius;
+      this.streamPositions[base + 1] = m.y;
+      this.streamPositions[base + 2] = m.z + Math.sin(spiralAngle) * m.spiralRadius;
+    }
+
+    (this.streamPoints.geometry.attributes.position as THREE.BufferAttribute).needsUpdate = true;
+  }
+
+  private animatePulse(): void {
+    for (let i = 0; i < PULSE_COUNT; i++) {
+      const m    = this.pulseMeta[i];
+      const base = i * 3;
+
+      m.progress += m.speed * m.direction;
+
+      // Bounce: reverse direction at each end, then pick a new path
+      if (m.progress >= 1) {
+        m.progress  = 1;
+        m.direction = -1;
+      } else if (m.progress <= 0) {
+        m.progress  = 0;
+        m.direction = 1;
+        // Assign a fresh path so pulses don't all follow the same lines
+        const fresh = this.randomPulsePath();
+        m.startX = fresh.startX;
+        m.startZ = fresh.startZ;
+        m.endX   = fresh.endX;
+        m.endZ   = fresh.endZ;
+      }
+
+      this.pulsePositions[base]     = THREE.MathUtils.lerp(m.startX, m.endX, m.progress);
+      this.pulsePositions[base + 1] = 3.5;
+      this.pulsePositions[base + 2] = THREE.MathUtils.lerp(m.startZ, m.endZ, m.progress);
+    }
+
+    (this.pulsePoints.geometry.attributes.position as THREE.BufferAttribute).needsUpdate = true;
+  }
+}
diff --git a/src/effects/PostProcessing.ts b/src/effects/PostProcessing.ts
new file mode 100644
index 0000000..4533ed9
--- /dev/null
+++ b/src/effects/PostProcessing.ts
@@ -0,0 +1,193 @@
+// ═══════════════════════════════════════════════
+// DW War Room 3D — Post-Processing Effects Pipeline
+// Sci-fi command center visual upgrade:
+//   - Unreal Bloom (subtle glow on emissive elements)
+//   - Scanlines overlay (barely perceptible CRT feel)
+//   - Film grain (organic noise, not a rave)
+// ═══════════════════════════════════════════════
+
+import * as THREE from 'three';
+import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
+import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
+import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
+import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
+
+// ─── Scanlines Shader ────────────────────────────────────────────────────────
+// Horizontal lines with 0.04 opacity — enough to hint at a CRT monitor without
+// actually making anyone squint.
+
+const ScanlinesShader = {
+  name: 'ScanlinesShader',
+
+  uniforms: {
+    tDiffuse: { value: null as THREE.Texture | null },
+    uResolution: { value: new THREE.Vector2(1, 1) },
+    uOpacity: { value: 0.0 },
+    uLineSpacing: { value: 3.0 }, // pixels between scanlines
+  },
+
+  vertexShader: /* glsl */ `
+    varying vec2 vUv;
+    void main() {
+      vUv = uv;
+      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+    }
+  `,
+
+  fragmentShader: /* glsl */ `
+    uniform sampler2D tDiffuse;
+    uniform vec2 uResolution;
+    uniform float uOpacity;
+    uniform float uLineSpacing;
+
+    varying vec2 vUv;
+
+    void main() {
+      vec4 color = texture2D(tDiffuse, vUv);
+
+      // Compute screen-space Y pixel coordinate
+      float screenY = vUv.y * uResolution.y;
+
+      // Every uLineSpacing pixels draw a dark scanline band
+      float line = mod(floor(screenY), uLineSpacing);
+      float scanline = step(uLineSpacing - 1.0, line); // 1.0 on the dark stripe, 0.0 elsewhere
+
+      // Darken by uOpacity on scanline rows, pass through elsewhere
+      color.rgb *= 1.0 - (scanline * uOpacity);
+
+      gl_FragColor = color;
+    }
+  `,
+};
+
+// ─── Film Grain Shader ────────────────────────────────────────────────────────
+// Temporal noise seeded by a time uniform so grains shift every frame.
+// Opacity 0.03 — you'd notice its absence more than its presence.
+
+const FilmGrainShader = {
+  name: 'FilmGrainShader',
+
+  uniforms: {
+    tDiffuse: { value: null as THREE.Texture | null },
+    uTime: { value: 0.0 },
+    uOpacity: { value: 0.008 },
+  },
+
+  vertexShader: /* glsl */ `
+    varying vec2 vUv;
+    void main() {
+      vUv = uv;
+      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+    }
+  `,
+
+  fragmentShader: /* glsl */ `
+    uniform sampler2D tDiffuse;
+    uniform float uTime;
+    uniform float uOpacity;
+
+    varying vec2 vUv;
+
+    // Fast pseudo-random hash
+    float hash(vec2 p) {
+      p = fract(p * vec2(234.34, 435.345));
+      p += dot(p, p + 34.23);
+      return fract(p.x * p.y);
+    }
+
+    void main() {
+      vec4 color = texture2D(tDiffuse, vUv);
+
+      // Seed changes per frame so grain is temporal, not static burn-in
+      float grain = hash(vUv + fract(uTime * 0.1));
+
+      // Remap from [0,1] → [-1,1] then attenuate heavily
+      float noise = (grain - 0.5) * 2.0 * uOpacity;
+
+      color.rgb += noise;
+
+      gl_FragColor = color;
+    }
+  `,
+};
+
+// ─── PostProcessingPipeline ───────────────────────────────────────────────────
+
+export class PostProcessingPipeline {
+  private composer: EffectComposer;
+  private bloomPass: UnrealBloomPass;
+  private scanlinesPass: ShaderPass;
+  private filmGrainPass: ShaderPass;
+
+  private clock: THREE.Clock;
+
+  /**
+   * Usage:
+   *   this.postProcessing = new PostProcessingPipeline(renderer, scene, camera);
+   *   // In animate():
+   *   this.postProcessing.render(this.scene, this.camera);
+   *   // In onResize():
+   *   this.postProcessing.resize(w, h);
+   */
+  constructor(
+    renderer: THREE.WebGLRenderer,
+    scene: THREE.Scene,
+    camera: THREE.Camera
+  ) {
+    this.clock = new THREE.Clock();
+
+    const { width, height } = renderer.getSize(new THREE.Vector2());
+
+    // ── Composer ───────────────────────────────────────────────────────────
+    this.composer = new EffectComposer(renderer);
+
+    // ── Pass 1: Standard render ────────────────────────────────────────────
+    const renderPass = new RenderPass(scene, camera);
+    this.composer.addPass(renderPass);
+
+    // ── Pass 2: Bloom ──────────────────────────────────────────────────────
+    // strength 0.25 — subtle professional glow, not arcade neon
+    // radius  0.5  — tighter spread for a cleaner look
+    // threshold 0.65 — catches more emissive surfaces without blowing out
+    this.bloomPass = new UnrealBloomPass(
+      new THREE.Vector2(width, height),
+      0.25,  // strength — reduced from 0.4 for architectural subtlety
+      0.5,   // radius
+      0.65   // threshold — lowered from 0.85 for controlled professional bloom
+    );
+    this.composer.addPass(this.bloomPass);
+
+    // ── Pass 3: Scanlines ──────────────────────────────────────────────────
+    this.scanlinesPass = new ShaderPass(ScanlinesShader);
+    this.scanlinesPass.uniforms['uResolution'].value.set(width, height);
+    this.composer.addPass(this.scanlinesPass);
+
+    // ── Pass 4: Film grain ─────────────────────────────────────────────────
+    this.filmGrainPass = new ShaderPass(FilmGrainShader);
+    this.composer.addPass(this.filmGrainPass);
+  }
+
+  /**
+   * Call this instead of renderer.render(scene, camera) in your animation loop.
+   * The grain shader receives the elapsed time so each frame has unique noise.
+   */
+  render(_scene: THREE.Scene, _camera: THREE.Camera): void {
+    // Advance grain time uniform so noise shifts every frame
+    this.filmGrainPass.uniforms['uTime'].value = this.clock.getElapsedTime();
+    this.composer.render();
+  }
+
+  /**
+   * Call from onResize() to keep the render target and bloom resolution in sync.
+   */
+  resize(width: number, height: number): void {
+    this.composer.setSize(width, height);
+    this.bloomPass.resolution.set(width, height);
+    this.scanlinesPass.uniforms['uResolution'].value.set(width, height);
+  }
+
+  /** Expose composer for debugging / extending in the future */
+  getComposer(): EffectComposer {
+    return this.composer;
+  }
+}
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..55ef01e
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,237 @@
+// ═══════════════════════════════════════════════
+// DW War Room 3D — Main Entry (Enhanced)
+// Three.js command center with full data wiring
+// System health, governance, feed, agent click
+// ═══════════════════════════════════════════════
+
+import { WarRoom } from './scene/WarRoom';
+import { HudOverlay } from './panels/HudOverlay';
+import { DualWebSocketClient } from './data/WebSocketClient';
+import * as Api from './data/ApiClient';
+
+async function main() {
+  const container = document.getElementById('war-room')!;
+
+  // Initialize 3D scene
+  const warRoom = new WarRoom(container);
+
+  // Initialize HUD overlay
+  const hud = new HudOverlay(container);
+
+  // Initialize WebSocket connections
+  const ws = new DualWebSocketClient();
+
+  // Agent click → show tooltip
+  warRoom.onAgentClick = (agent, screenX, screenY) => {
+    hud.showAgentTooltip(agent, screenX, screenY);
+  };
+
+  // Load initial data (all in parallel)
+  try {
+    const [agentData, decisionStats, escalations, meetingState, governanceStatus] = await Promise.all([
+      Api.getAgents(),
+      Api.getDecisionStats(),
+      Api.getActiveEscalations(),
+      Api.getActiveMeeting(),
+      Api.getGovernanceStatus().catch(() => null),
+    ]);
+
+    // Build agent hierarchy
+    warRoom.agentNodes.buildHierarchy(agentData.agents, agentData.executives);
+    const allAgents = warRoom.agentNodes.getAllAgents();
+    hud.updateAgentCount(allAgents.length, 0);
+
+    // Update HUD panels
+    hud.updateDecisions(decisionStats);
+    hud.updateEscalations(escalations);
+    hud.updateMeeting(meetingState);
+    if (governanceStatus) hud.updateGovernance(governanceStatus);
+
+    // Wire data to 3D DataWalls
+    warRoom.dataWalls.updatePanelData('decisions', {
+      Pending: String(decisionStats.pending || 0),
+      Approved: String(decisionStats.approved || 0),
+      Rejected: String(decisionStats.rejected || 0),
+      Deferred: String(decisionStats.deferred || 0),
+    });
+
+    warRoom.dataWalls.updatePanelData('escalations', {
+      Active: String(escalations.length),
+      'Avg Age': escalations.length > 0
+        ? `${(escalations.reduce((s, e) => s + e.hours_elapsed, 0) / escalations.length).toFixed(1)}h`
+        : '0h',
+      Critical: String(escalations.filter(e => e.hours_elapsed > 24).length),
+      Resolved: '\u2014',
+    });
+
+    warRoom.dataWalls.updatePanelData('meeting', {
+      Status: meetingState.active ? 'ACTIVE' : 'IDLE',
+      Phase: meetingState.phase || '\u2014',
+      Agents: String(allAgents.length),
+      Duration: '\u2014',
+    });
+
+    // Set escalation pulse
+    warRoom.setEscalationPulse(escalations.length > 0);
+  } catch (err) {
+    console.error('[WarRoom] Initial data load failed:', err);
+  }
+
+  // Load system health (separate try-catch — may not be available)
+  try {
+    const health = await Api.getSystemHealth();
+    if (health) {
+      hud.updateSystemHealth(health);
+      warRoom.dataWalls.updatePanelData('status', {
+        CPU: health.cpu || '\u2014',
+        Memory: health.memory || '\u2014',
+        Services: String(health.services || 0),
+        Uptime: health.uptime || '\u2014',
+      });
+    }
+  } catch {
+    console.warn('[WarRoom] System health not available');
+  }
+
+  // Load activity feed (separate try-catch — may not be available)
+  try {
+    const feed = await Api.getFeed();
+    if (feed && feed.length > 0) {
+      hud.loadFeed(feed);
+    }
+  } catch {
+    console.warn('[WarRoom] Feed not available');
+  }
+
+  // WebSocket event handling
+  ws.onEvent((event) => {
+    switch (event.type) {
+      case 'meeting_message': {
+        const msg = event.payload;
+        hud.addFeedMessage(msg.agent_name, msg.message);
+        warRoom.agentNodes.setSpeaking(msg.agent_id, true);
+        setTimeout(() => warRoom.agentNodes.setSpeaking(msg.agent_id, false), 3000);
+        // Update speaking count
+        const agents = warRoom.agentNodes.getAllAgents();
+        hud.updateAgentCount(agents.length, agents.filter(a => a.speaking).length);
+        break;
+      }
+
+      case 'meeting_started':
+      case 'meeting_completed':
+      case 'meeting_halted':
+        Api.getActiveMeeting().then(state => {
+          hud.updateMeeting(state);
+          warRoom.dataWalls.updatePanelData('meeting', {
+            Status: state.active ? 'ACTIVE' : 'IDLE',
+            Phase: state.phase || '\u2014',
+          });
+        }).catch(() => {});
+        if (event.type === 'meeting_completed' || event.type === 'meeting_halted') {
+          warRoom.agentNodes.clearAllSpeaking();
+        }
+        break;
+
+      case 'meeting_phase':
+        hud.updateMeeting({
+          active: true,
+          phase: event.payload.phase,
+          type: event.payload.meetingType,
+          messages: [],
+        });
+        warRoom.dataWalls.updatePanelData('meeting', {
+          Status: 'ACTIVE',
+          Phase: event.payload.phase,
+        });
+        break;
+
+      case 'new_decision':
+      case 'decision_resolved':
+        Api.getDecisionStats().then(stats => {
+          hud.updateDecisions(stats);
+          warRoom.dataWalls.updatePanelData('decisions', {
+            Pending: String(stats.pending || 0),
+            Approved: String(stats.approved || 0),
+            Rejected: String(stats.rejected || 0),
+            Deferred: String(stats.deferred || 0),
+          });
+        }).catch(() => {});
+        break;
+
+      case 'escalation':
+        Api.getActiveEscalations().then(esc => {
+          hud.updateEscalations(esc);
+          warRoom.setEscalationPulse(esc.length > 0);
+          warRoom.dataWalls.updatePanelData('escalations', {
+            Active: String(esc.length),
+            'Avg Age': esc.length > 0
+              ? `${(esc.reduce((s, e) => s + e.hours_elapsed, 0) / esc.length).toFixed(1)}h`
+              : '0h',
+            Critical: String(esc.filter(e => e.hours_elapsed > 24).length),
+          });
+        }).catch(() => {});
+        break;
+    }
+  });
+
+  ws.connect();
+
+  // Polling fallback — refresh every 30s
+  setInterval(async () => {
+    try {
+      const [stats, escalations, meeting, governance] = await Promise.all([
+        Api.getDecisionStats(),
+        Api.getActiveEscalations(),
+        Api.getActiveMeeting(),
+        Api.getGovernanceStatus().catch(() => null),
+      ]);
+      hud.updateDecisions(stats);
+      hud.updateEscalations(escalations);
+      hud.updateMeeting(meeting);
+      hud.updateConnection(ws.govConnected, ws.brConnected);
+      warRoom.setEscalationPulse(escalations.length > 0);
+
+      if (governance) hud.updateGovernance(governance);
+
+      // Update 3D DataWalls
+      warRoom.dataWalls.updatePanelData('decisions', {
+        Pending: String(stats.pending || 0),
+        Approved: String(stats.approved || 0),
+        Rejected: String(stats.rejected || 0),
+        Deferred: String(stats.deferred || 0),
+      });
+    } catch {}
+  }, 30000);
+
+  // System health polling — every 60s
+  setInterval(async () => {
+    try {
+      const health = await Api.getSystemHealth();
+      if (health) {
+        hud.updateSystemHealth(health);
+        warRoom.dataWalls.updatePanelData('status', {
+          CPU: health.cpu || '\u2014',
+          Memory: health.memory || '\u2014',
+          Services: String(health.services || 0),
+          Uptime: health.uptime || '\u2014',
+        });
+      }
+    } catch {}
+  }, 60000);
+
+  // Clock update
+  setInterval(() => hud.updateClock(), 1000);
+  hud.updateClock();
+
+  // Camera view buttons
+  document.getElementById('btn-overview')?.addEventListener('click', () => warRoom.viewOverview());
+  document.getElementById('btn-meeting')?.addEventListener('click', () => warRoom.viewMeetingFocus());
+  document.getElementById('btn-agents')?.addEventListener('click', () => warRoom.viewAgentFocus());
+
+  // Start animation loop
+  warRoom.animate();
+
+  console.log('[WarRoom] Initialized — enhanced with system health, governance, agent tooltips');
+}
+
+main().catch(err => console.error('[WarRoom] Fatal:', err));
diff --git a/src/panels/HudOverlay.ts b/src/panels/HudOverlay.ts
new file mode 100644
index 0000000..189e276
--- /dev/null
+++ b/src/panels/HudOverlay.ts
@@ -0,0 +1,897 @@
+// ═══════════════════════════════════════════════
+// DW War Room 3D — HUD Overlay (Figma Design System)
+// Professional glassmorphism command center overlay
+// Design tokens: --ds-* CSS custom properties
+// Stock Trader UI Kit inspired dark dashboard
+// ═══════════════════════════════════════════════
+
+import type { AgentNode, DecisionStats, GovernanceStatus, MeetingState, EscalationAlert, SystemHealth, FeedItem } from '../types';
+
+const PHASE_LABELS: Record<string, string> = {
+  caucus: 'Caucus', call_to_order: 'Call to Order', old_business: 'Old Business',
+  current_business: 'Current Business', new_business: 'New Business',
+  breakout: 'Breakout', minutes: 'Minutes', adjournment: 'Adjournment',
+};
+
+// Design token references for dynamic JS styling
+// Uses CSS custom property syntax so values resolve at render time
+const DS = {
+  success: 'var(--ds-success)',
+  warning: 'var(--ds-warning)',
+  error: 'var(--ds-error)',
+  info: 'var(--ds-info)',
+  violet: 'var(--ds-violet)',
+  cyan: 'var(--ds-cyan)',
+  gray100: 'var(--ds-gray-100)',
+  gray400: 'var(--ds-gray-400)',
+  gray500: 'var(--ds-gray-500)',
+};
+
+export class HudOverlay {
+  private container: HTMLElement;
+  private tooltipTimeout: ReturnType<typeof setTimeout> | null = null;
+
+  constructor(parent: HTMLElement) {
+    this.container = document.createElement('div');
+    this.container.id = 'war-room-hud';
+    this.buildDom();
+    parent.appendChild(this.container);
+    this.applyStyles();
+  }
+
+  /* ───────────────────────────────────────────
+   * DOM Construction — uses safe DOM methods
+   * ─────────────────────────────────────────── */
+  private buildDom(): void {
+    const root = this.container;
+
+    // ── Header ──
+    const header = this.el('div', 'hud-header');
+    const headerLeft = this.el('div', 'hud-header-left');
+    headerLeft.appendChild(this.el('span', 'hud-logo-pulse'));
+    const title = this.el('span', 'hud-title');
+    title.textContent = 'DW WAR ROOM';
+    headerLeft.appendChild(title);
+    header.appendChild(headerLeft);
+
+    const statusGroup = this.el('div', 'hud-status-group');
+    const statusDot = this.el('span', 'hud-status-dot');
+    statusDot.id = 'hud-status-dot';
+    statusGroup.appendChild(statusDot);
+    const statusText = this.el('span', 'hud-status');
+    statusText.id = 'hud-connection';
+    statusText.textContent = 'CONNECTING...';
+    statusGroup.appendChild(statusText);
+    const govMode = this.el('span', 'hud-governance-mode');
+    govMode.id = 'hud-gov-mode';
+    statusGroup.appendChild(govMode);
+    header.appendChild(statusGroup);
+    root.appendChild(header);
+
+    // ── Meeting Panel ──
+    root.appendChild(this.buildPanel(
+      'hud-meeting', 'hud-top-left accent-violet', 'MEETING STATUS',
+      'hud-meeting-content', 'No active meeting'
+    ));
+
+    // ── Decisions Panel ──
+    root.appendChild(this.buildPanel(
+      'hud-decisions', 'hud-top-right accent-cyan', 'DECISIONS',
+      'hud-decisions-content', 'Loading...'
+    ));
+
+    // ── Live Feed Panel ──
+    const feedPanel = this.buildPanel(
+      'hud-feed', 'hud-bottom-left accent-amber', 'LIVE FEED',
+      'hud-feed-content', ''
+    );
+    // Add feed count badge to title
+    const feedTitle = feedPanel.querySelector('.hud-panel-title');
+    if (feedTitle) {
+      const badge = this.el('span', 'hud-feed-badge');
+      badge.id = 'hud-feed-count';
+      feedTitle.appendChild(badge);
+    }
+    // Add scroll class to body
+    const feedBody = feedPanel.querySelector('.hud-panel-body');
+    if (feedBody) feedBody.classList.add('hud-feed-scroll');
+    root.appendChild(feedPanel);
+
+    // ── Escalations Panel ──
+    root.appendChild(this.buildPanel(
+      'hud-escalations', 'hud-bottom-right accent-red', 'ESCALATIONS',
+      'hud-escalations-content', 'None active'
+    ));
+
+    // ── System Health Panel ──
+    const healthPanel = this.buildPanel(
+      'hud-health', 'hud-mid-right accent-emerald', 'SYSTEM HEALTH',
+      'hud-health-content', ''
+    );
+    const healthBody = healthPanel.querySelector('.hud-panel-body')!;
+    ['CPU', 'MEMORY', 'SERVICES', 'UPTIME'].forEach(label => {
+      const row = this.el('div', 'hud-metric');
+      const lbl = this.el('span', 'hud-metric-label');
+      lbl.textContent = label;
+      row.appendChild(lbl);
+      const val = this.el('span', 'hud-metric-value');
+      val.id = `hud-${label.toLowerCase()}`;
+      val.textContent = '\u2014';
+      row.appendChild(val);
+      healthBody.appendChild(row);
+    });
+    root.appendChild(healthPanel);
+
+    // ── Governance Panel ──
+    const govPanel = this.buildPanel(
+      'hud-governance', 'hud-mid-left accent-blue', 'GOVERNANCE',
+      'hud-governance-content', ''
+    );
+    const govBody = govPanel.querySelector('.hud-panel-body')!;
+    const govMetrics = [
+      { label: 'MODE', id: 'hud-gov-mode-val' },
+      { label: 'PENDING', id: 'hud-gov-pending' },
+      { label: 'DELEGATIONS', id: 'hud-gov-delegations' },
+      { label: 'ESCALATIONS', id: 'hud-gov-escalations' },
+    ];
+    govMetrics.forEach(m => {
+      const row = this.el('div', 'hud-metric');
+      const lbl = this.el('span', 'hud-metric-label');
+      lbl.textContent = m.label;
+      row.appendChild(lbl);
+      const val = this.el('span', 'hud-metric-value');
+      val.id = m.id;
+      val.textContent = '\u2014';
+      row.appendChild(val);
+      govBody.appendChild(row);
+    });
+    root.appendChild(govPanel);
+
+    // ── Agent Tooltip ──
+    const tooltip = this.el('div', 'hud-agent-tooltip');
+    tooltip.id = 'hud-agent-tooltip';
+    tooltip.style.display = 'none';
+    ['tooltip-header', 'tooltip-role', 'tooltip-dept', 'tooltip-exec', 'tooltip-status'].forEach(cls => {
+      const d = this.el('div', cls);
+      d.id = cls;
+      tooltip.appendChild(d);
+    });
+    root.appendChild(tooltip);
+
+    // ── Bottom Bar ──
+    const bottomBar = this.el('div', 'hud-bottom-bar');
+    const bottomNav = this.el('div', 'hud-bottom-nav');
+
+    const btnData = [
+      { id: 'btn-overview', label: 'Overview', icon: '<rect x="1" y="1" width="6" height="6" rx="1"/><rect x="9" y="1" width="6" height="6" rx="1"/><rect x="1" y="9" width="6" height="6" rx="1"/><rect x="9" y="9" width="6" height="6" rx="1"/>' },
+      { id: 'btn-meeting', label: 'Meeting', icon: '<circle cx="8" cy="5" r="3"/><path d="M2 15c0-3.3 2.7-6 6-6s6 2.7 6 6"/>' },
+      { id: 'btn-agents', label: 'Agents', icon: '<circle cx="5" cy="5" r="2.5"/><circle cx="11" cy="5" r="2.5"/><path d="M1 14c0-2.2 1.8-4 4-4s4 1.8 4 4"/><path d="M7 14c0-2.2 1.8-4 4-4s4 1.8 4 4"/>' },
+    ];
+    btnData.forEach(b => {
+      const btn = document.createElement('button');
+      btn.className = 'hud-view-btn';
+      btn.id = b.id;
+      // Create SVG icon safely via namespace
+      const ns = 'http://www.w3.org/2000/svg';
+      const svg = document.createElementNS(ns, 'svg');
+      svg.setAttribute('width', '13');
+      svg.setAttribute('height', '13');
+      svg.setAttribute('viewBox', '0 0 16 16');
+      svg.setAttribute('fill', 'none');
+      svg.setAttribute('stroke', 'currentColor');
+      svg.setAttribute('stroke-width', '1.5');
+      // Use a temporary container to parse SVG children safely
+      const tmp = document.createElementNS(ns, 'svg');
+      tmp.innerHTML = b.icon;
+      while (tmp.firstChild) svg.appendChild(tmp.firstChild);
+      btn.appendChild(svg);
+      btn.appendChild(document.createTextNode(b.label));
+      bottomNav.appendChild(btn);
+    });
+    bottomBar.appendChild(bottomNav);
+
+    const bottomInfo = this.el('div', 'hud-bottom-info');
+    const agentCount = this.el('span', 'hud-agent-count');
+    agentCount.id = 'hud-agent-count';
+    bottomInfo.appendChild(agentCount);
+    bottomInfo.appendChild(this.el('span', 'hud-bottom-divider'));
+    const clock = this.el('span', 'hud-clock');
+    clock.id = 'hud-clock';
+    bottomInfo.appendChild(clock);
+    bottomBar.appendChild(bottomInfo);
+    root.appendChild(bottomBar);
+  }
+
+  /** Helper: create element with class(es) */
+  private el(tag: string, className: string): HTMLElement {
+    const e = document.createElement(tag);
+    e.className = className;
+    return e;
+  }
+
+  /** Helper: build a standard glass panel */
+  private buildPanel(id: string, classes: string, title: string, bodyId: string, bodyText: string): HTMLElement {
+    const panel = this.el('div', `hud-panel ${classes}`);
+    panel.id = id;
+    const titleEl = this.el('div', 'hud-panel-title');
+    titleEl.textContent = title;
+    panel.appendChild(titleEl);
+    const body = this.el('div', 'hud-panel-body');
+    body.id = bodyId;
+    if (bodyText) body.textContent = bodyText;
+    panel.appendChild(body);
+    return panel;
+  }
+
+  /* ───────────────────────────────────────────
+   * Data Update Methods
+   * ─────────────────────────────────────────── */
+
+  updateConnection(govConnected: boolean, brConnected: boolean): void {
+    const el = document.getElementById('hud-connection');
+    const dot = document.getElementById('hud-status-dot');
+    if (!el) return;
+    if (govConnected && brConnected) {
+      el.textContent = 'ALL SYSTEMS ONLINE';
+      el.style.color = DS.success;
+      if (dot) { dot.className = 'hud-status-dot dot-success'; }
+    } else if (govConnected || brConnected) {
+      el.textContent = 'PARTIAL CONNECTION';
+      el.style.color = DS.warning;
+      if (dot) { dot.className = 'hud-status-dot dot-warning'; }
+    } else {
+      el.textContent = 'DISCONNECTED';
+      el.style.color = DS.error;
+      if (dot) { dot.className = 'hud-status-dot dot-error'; }
+    }
+  }
+
+  updateMeeting(state: MeetingState): void {
+    const el = document.getElementById('hud-meeting-content');
+    if (!el) return;
+    if (!state.active) {
+      el.textContent = 'No active meeting';
+      el.style.color = DS.gray500;
+      return;
+    }
+    const phaseLabel = PHASE_LABELS[state.phase || ''] || state.phase || '\u2014';
+    el.textContent = '';
+
+    const typeDiv = document.createElement('div');
+    typeDiv.style.cssText = `color:${DS.violet};font-weight:700;font-size:13px`;
+    typeDiv.textContent = (state.type || '').toUpperCase();
+    el.appendChild(typeDiv);
+
+    const phaseDiv = document.createElement('div');
+    phaseDiv.style.marginTop = '4px';
+    phaseDiv.textContent = 'Phase: ';
+    const phaseSpan = document.createElement('span');
+    phaseSpan.style.color = DS.cyan;
+    phaseSpan.textContent = phaseLabel;
+    phaseDiv.appendChild(phaseSpan);
+    el.appendChild(phaseDiv);
+
+    const statusDiv = document.createElement('div');
+    statusDiv.textContent = 'Status: ';
+    const statusSpan = document.createElement('span');
+    statusSpan.style.color = state.status === 'active' ? DS.success : DS.warning;
+    statusSpan.textContent = (state.status || 'active').toUpperCase();
+    statusDiv.appendChild(statusSpan);
+    el.appendChild(statusDiv);
+
+    const msgDiv = document.createElement('div');
+    msgDiv.style.cssText = `font-size:9px;color:${DS.gray500};margin-top:4px`;
+    msgDiv.textContent = `${state.messages.length} message${state.messages.length !== 1 ? 's' : ''}`;
+    el.appendChild(msgDiv);
+  }
+
+  updateDecisions(stats: DecisionStats): void {
+    const el = document.getElementById('hud-decisions-content');
+    if (!el) return;
+    const p = stats.pending || 0;
+    const a = stats.approved || 0;
+    const r = stats.rejected || 0;
+    const d = stats.deferred || 0;
+    const t = stats.total || (p + a + r + d);
+
+    el.textContent = '';
+
+    const grid = document.createElement('div');
+    grid.className = 'decision-grid';
+
+    const statData = [
+      { val: p, color: DS.warning, label: 'Pending' },
+      { val: a, color: DS.success, label: 'Approved' },
+      { val: r, color: DS.error, label: 'Rejected' },
+      { val: d, color: DS.gray500, label: 'Deferred' },
+    ];
+
+    statData.forEach(s => {
+      const cell = document.createElement('div');
+      cell.className = 'decision-stat';
+      const count = document.createElement('span');
+      count.className = 'decision-count';
+      count.style.color = s.color;
+      count.textContent = String(s.val);
+      const label = document.createElement('span');
+      label.className = 'decision-label';
+      label.textContent = s.label;
+      cell.appendChild(count);
+      cell.appendChild(label);
+      grid.appendChild(cell);
+    });
+    el.appendChild(grid);
+
+    const totalDiv = document.createElement('div');
+    totalDiv.className = 'decision-total';
+    totalDiv.textContent = 'Total Decisions: ';
+    const totalSpan = document.createElement('span');
+    totalSpan.style.cssText = `color:${DS.cyan};font-weight:700`;
+    totalSpan.textContent = String(t);
+    totalDiv.appendChild(totalSpan);
+    el.appendChild(totalDiv);
+  }
+
+  updateEscalations(alerts: EscalationAlert[]): void {
+    const el = document.getElementById('hud-escalations-content');
+    if (!el) return;
+    el.textContent = '';
+
+    if (alerts.length === 0) {
+      const span = document.createElement('span');
+      span.style.color = DS.success;
+      span.textContent = 'None active';
+      el.appendChild(span);
+      return;
+    }
+
+    const header = document.createElement('div');
+    header.className = 'escalation-header';
+    header.textContent = `${alerts.length} ACTIVE`;
+    el.appendChild(header);
+
+    alerts.slice(0, 4).forEach(a => {
+      const row = document.createElement('div');
+      row.className = 'escalation-row';
+      const hours = document.createElement('span');
+      hours.className = 'escalation-hours';
+      hours.textContent = `${a.hours_elapsed.toFixed(1)}h`;
+      row.appendChild(hours);
+      row.appendChild(document.createTextNode(' \u2014 '));
+      const type = document.createElement('span');
+      type.style.color = DS.gray100;
+      type.textContent = a.escalation_type;
+      row.appendChild(type);
+      if (a.action_taken) {
+        const action = document.createElement('span');
+        action.className = 'escalation-action';
+        action.textContent = `[${a.action_taken}]`;
+        row.appendChild(action);
+      }
+      el.appendChild(row);
+    });
+  }
+
+  updateSystemHealth(health: SystemHealth): void {
+    const cpu = document.getElementById('hud-cpu');
+    const mem = document.getElementById('hud-memory');
+    const svc = document.getElementById('hud-services');
+    const up = document.getElementById('hud-uptime');
+    if (cpu) {
+      cpu.textContent = health.cpu;
+      cpu.style.color = parseFloat(health.cpu) > 80 ? DS.error : parseFloat(health.cpu) > 50 ? DS.warning : DS.success;
+    }
+    if (mem) {
+      mem.textContent = health.memory;
+      mem.style.color = parseFloat(health.memory) > 80 ? DS.error : parseFloat(health.memory) > 50 ? DS.warning : DS.success;
+    }
+    if (svc) {
+      svc.textContent = String(health.services);
+      svc.style.color = DS.info;
+    }
+    if (up) {
+      up.textContent = health.uptime;
+      up.style.color = DS.gray400;
+    }
+  }
+
+  updateGovernance(status: GovernanceStatus): void {
+    const mode = document.getElementById('hud-gov-mode-val');
+    const pending = document.getElementById('hud-gov-pending');
+    const delegations = document.getElementById('hud-gov-delegations');
+    const escalations = document.getElementById('hud-gov-escalations');
+    const headerMode = document.getElementById('hud-gov-mode');
+
+    const gm = status.governance_mode || 'unknown';
+    if (mode) {
+      mode.textContent = gm.toUpperCase();
+      mode.style.color = gm === 'autonomous' ? DS.success : DS.warning;
+    }
+    if (pending) {
+      const pd = status.pending_decisions ?? 0;
+      pending.textContent = String(pd);
+      pending.style.color = pd > 5 ? DS.error : pd > 0 ? DS.warning : DS.success;
+    }
+    if (delegations) {
+      delegations.textContent = String(status.active_delegations ?? 0);
+      delegations.style.color = DS.info;
+    }
+    if (escalations) {
+      const ae = status.active_escalations ?? 0;
+      escalations.textContent = String(ae);
+      escalations.style.color = ae > 0 ? DS.error : DS.success;
+    }
+    if (headerMode) {
+      headerMode.textContent = gm.toUpperCase();
+      headerMode.style.color = gm === 'autonomous' ? DS.success : DS.warning;
+    }
+  }
+
+  updateAgentCount(total: number, speaking: number): void {
+    const el = document.getElementById('hud-agent-count');
+    if (!el) return;
+    el.textContent = '';
+    const count = document.createElement('span');
+    count.style.color = DS.cyan;
+    count.textContent = String(total);
+    el.appendChild(count);
+    el.appendChild(document.createTextNode(' agents'));
+    if (speaking > 0) {
+      const sp = document.createElement('span');
+      sp.style.color = DS.success;
+      sp.textContent = ` (${speaking} speaking)`;
+      el.appendChild(sp);
+    }
+  }
+
+  addFeedMessage(agentName: string, message: string, type?: string): void {
+    const el = document.getElementById('hud-feed-content');
+    if (!el) return;
+
+    const div = document.createElement('div');
+    div.className = 'feed-item';
+    const typeColor = type === 'error' ? DS.error : type === 'warning' ? DS.warning : DS.violet;
+    const time = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', timeZone: 'America/Los_Angeles' });
+
+    const timeSpan = document.createElement('span');
+    timeSpan.className = 'feed-time';
+    timeSpan.textContent = time;
+    div.appendChild(timeSpan);
+
+    const nameSpan = document.createElement('span');
+    nameSpan.style.cssText = `color:${typeColor};font-weight:600`;
+    nameSpan.textContent = agentName;
+    div.appendChild(nameSpan);
+
+    const msgSpan = document.createElement('span');
+    msgSpan.style.color = DS.gray400;
+    msgSpan.textContent = `: ${message.slice(0, 100)}`;
+    div.appendChild(msgSpan);
+
+    el.appendChild(div);
+    el.scrollTop = el.scrollHeight;
+
+    // Update count badge
+    const badge = document.getElementById('hud-feed-count');
+    if (badge) badge.textContent = String(el.children.length);
+
+    // Keep max 30 messages
+    while (el.children.length > 30) {
+      el.removeChild(el.firstChild!);
+    }
+  }
+
+  // Bulk load feed items from API
+  loadFeed(items: FeedItem[]): void {
+    const el = document.getElementById('hud-feed-content');
+    if (!el) return;
+    if (el.children.length > 0) return;
+    items.slice(-15).forEach(item => {
+      this.addFeedMessage(item.agent_name, item.action, item.type);
+    });
+  }
+
+  // Show agent detail tooltip at screen position
+  showAgentTooltip(agent: AgentNode, screenX: number, screenY: number): void {
+    const tooltip = document.getElementById('hud-agent-tooltip');
+    if (!tooltip) return;
+
+    if (this.tooltipTimeout) clearTimeout(this.tooltipTimeout);
+
+    const header = document.getElementById('tooltip-header');
+    const role = document.getElementById('tooltip-role');
+    const dept = document.getElementById('tooltip-dept');
+    const exec = document.getElementById('tooltip-exec');
+    const status = document.getElementById('tooltip-status');
+
+    if (header) {
+      header.textContent = agent.name;
+      header.style.color = agent.color;
+    }
+    if (role) role.textContent = agent.role;
+    if (dept) dept.textContent = `Dept: ${agent.dept}`;
+    if (exec) exec.textContent = `Reports to: ${agent.exec.toUpperCase()}`;
+    if (status) {
+      status.textContent = agent.speaking ? 'SPEAKING' : 'IDLE';
+      status.style.color = agent.speaking ? DS.success : DS.gray500;
+    }
+
+    const maxX = window.innerWidth - 200;
+    const maxY = window.innerHeight - 140;
+    tooltip.style.left = `${Math.min(screenX + 15, maxX)}px`;
+    tooltip.style.top = `${Math.min(screenY - 20, maxY)}px`;
+    tooltip.style.display = 'block';
+
+    this.tooltipTimeout = setTimeout(() => {
+      tooltip.style.display = 'none';
+    }, 5000);
+  }
+
+  hideAgentTooltip(): void {
+    const tooltip = document.getElementById('hud-agent-tooltip');
+    if (tooltip) tooltip.style.display = 'none';
+    if (this.tooltipTimeout) clearTimeout(this.tooltipTimeout);
+  }
+
+  updateClock(): void {
+    const el = document.getElementById('hud-clock');
+    if (!el) return;
+    const now = new Date();
+    const pt = new Date(now.toLocaleString('en-US', { timeZone: 'America/Los_Angeles' }));
+    el.textContent = pt.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) + ' PT';
+  }
+
+  /* ───────────────────────────────────────────
+   * Design System Stylesheet
+   * ─────────────────────────────────────────── */
+  private applyStyles(): void {
+    const style = document.createElement('style');
+    style.textContent = `
+      /* ═══ Design System Tokens ═══ */
+      :root {
+        /* Typography */
+        --ds-font: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
+
+        /* Gray palette (Slate scale) */
+        --ds-gray-50: #f8fafc;
+        --ds-gray-100: #f1f5f9;
+        --ds-gray-200: #e2e8f0;
+        --ds-gray-300: #cbd5e1;
+        --ds-gray-400: #94a3b8;
+        --ds-gray-500: #64748b;
+        --ds-gray-600: #475569;
+        --ds-gray-700: #334155;
+        --ds-gray-800: #1e293b;
+        --ds-gray-900: #0f172a;
+
+        /* Accent colors */
+        --ds-violet: #8b5cf6;
+        --ds-indigo: #6366f1;
+        --ds-cyan: #06b6d4;
+        --ds-blue: #3b82f6;
+        --ds-emerald: #10b981;
+        --ds-amber: #f59e0b;
+
+        /* Semantic colors */
+        --ds-success: #10b981;
+        --ds-warning: #f59e0b;
+        --ds-error: #ef4444;
+        --ds-info: #3b82f6;
+
+        /* Glass surfaces */
+        --ds-glass: rgba(10, 14, 28, 0.72);
+        --ds-glass-strong: rgba(8, 11, 22, 0.88);
+        --ds-glass-subtle: rgba(15, 20, 38, 0.5);
+
+        /* Blur */
+        --ds-blur-sm: blur(8px);
+        --ds-blur: blur(16px);
+        --ds-blur-lg: blur(24px);
+
+        /* Borders */
+        --ds-border: rgba(148, 163, 184, 0.06);
+        --ds-border-strong: rgba(148, 163, 184, 0.12);
+        --ds-border-interactive: rgba(99, 102, 241, 0.25);
+
+        /* Radius */
+        --ds-radius-sm: 6px;
+        --ds-radius-md: 10px;
+        --ds-radius-lg: 14px;
+        --ds-radius-full: 9999px;
+
+        /* Shadows */
+        --ds-shadow-sm: 0 2px 8px rgba(0,0,0,0.3);
+        --ds-shadow-md: 0 4px 16px rgba(0,0,0,0.4);
+        --ds-shadow-lg: 0 8px 32px rgba(0,0,0,0.5);
+        --ds-shadow-glow-violet: 0 0 20px rgba(139,92,246,0.15);
+        --ds-shadow-glow-cyan: 0 0 20px rgba(6,182,212,0.12);
+
+        /* Transitions */
+        --ds-ease: cubic-bezier(0.4, 0, 0.2, 1);
+        --ds-duration-fast: 150ms;
+        --ds-duration-normal: 250ms;
+      }
+
+      /* ═══ HUD Container ═══ */
+      #war-room-hud {
+        position: absolute; top: 0; left: 0; width: 100%; height: 100%;
+        pointer-events: none;
+        font-family: var(--ds-font);
+        color: var(--ds-gray-100);
+        z-index: 10;
+      }
+
+      /* ═══ Header Bar ═══ */
+      .hud-header {
+        position: absolute; top: 0; left: 0; right: 0;
+        display: flex; justify-content: space-between; align-items: center;
+        padding: 14px 24px;
+        background: linear-gradient(180deg, rgba(5,5,16,0.95) 0%, rgba(5,5,16,0.6) 70%, transparent 100%);
+      }
+      .hud-header-left {
+        display: flex; align-items: center; gap: 10px;
+      }
+      .hud-logo-pulse {
+        width: 8px; height: 8px; border-radius: var(--ds-radius-full);
+        background: var(--ds-violet);
+        box-shadow: 0 0 8px rgba(139,92,246,0.6), 0 0 16px rgba(139,92,246,0.3);
+        animation: logoPulse 2.5s ease-in-out infinite;
+      }
+      @keyframes logoPulse {
+        0%, 100% { opacity: 1; box-shadow: 0 0 8px rgba(139,92,246,0.6), 0 0 16px rgba(139,92,246,0.3); }
+        50% { opacity: 0.7; box-shadow: 0 0 4px rgba(139,92,246,0.3), 0 0 8px rgba(139,92,246,0.15); }
+      }
+      .hud-title {
+        font-size: 13px; font-weight: 800; letter-spacing: 2.5px;
+        background: linear-gradient(135deg, var(--ds-violet), var(--ds-cyan));
+        -webkit-background-clip: text; -webkit-text-fill-color: transparent;
+        background-clip: text;
+      }
+      .hud-status-group {
+        display: flex; gap: 10px; align-items: center;
+      }
+      .hud-status-dot {
+        width: 6px; height: 6px; border-radius: var(--ds-radius-full);
+        background: var(--ds-warning);
+        animation: dotPulse 1.5s ease-in-out infinite;
+      }
+      .hud-status-dot.dot-success { background: var(--ds-success); animation: none; }
+      .hud-status-dot.dot-warning { background: var(--ds-warning); }
+      .hud-status-dot.dot-error { background: var(--ds-error); }
+      @keyframes dotPulse {
+        0%, 100% { opacity: 1; }
+        50% { opacity: 0.4; }
+      }
+      .hud-status {
+        font-size: 10px; font-weight: 700; letter-spacing: 0.5px;
+        color: var(--ds-warning);
+      }
+      .hud-governance-mode {
+        font-size: 9px; font-weight: 600;
+        padding: 2px 8px; border-radius: var(--ds-radius-full);
+        background: rgba(139,92,246,0.1);
+        border: 1px solid rgba(139,92,246,0.2);
+      }
+
+      /* ═══ Glass Panels ═══ */
+      .hud-panel {
+        position: absolute;
+        padding: 16px 20px;
+        background: var(--ds-glass-strong);
+        backdrop-filter: var(--ds-blur);
+        -webkit-backdrop-filter: var(--ds-blur);
+        border: 1px solid var(--ds-border-strong);
+        border-radius: var(--ds-radius-lg);
+        min-width: 240px;
+        font-size: 13px;
+        box-shadow: var(--ds-shadow-lg), inset 0 1px 0 rgba(255,255,255,0.03);
+        transition: border-color var(--ds-duration-normal) var(--ds-ease),
+                    box-shadow var(--ds-duration-normal) var(--ds-ease);
+        overflow: hidden;
+      }
+      /* Colored accent bar (left edge) via pseudo-element */
+      .hud-panel::before {
+        content: '';
+        position: absolute; left: 0; top: 0; bottom: 0;
+        width: 3px; border-radius: 3px 0 0 3px;
+        background: var(--ds-violet);
+        opacity: 0.8;
+      }
+      .hud-panel.accent-violet::before { background: var(--ds-violet); }
+      .hud-panel.accent-cyan::before { background: var(--ds-cyan); }
+      .hud-panel.accent-amber::before { background: var(--ds-amber); }
+      .hud-panel.accent-red::before { background: var(--ds-error); }
+      .hud-panel.accent-emerald::before { background: var(--ds-emerald); }
+      .hud-panel.accent-blue::before { background: var(--ds-blue); }
+
+      .hud-panel-title {
+        font-size: 11px; font-weight: 700;
+        color: var(--ds-gray-400);
+        letter-spacing: 1.5px;
+        margin-bottom: 12px;
+        display: flex; align-items: center; gap: 6px;
+      }
+      .hud-panel-body {
+        color: var(--ds-gray-300);
+      }
+
+      /* ═══ Panel Positions ═══ */
+      .hud-top-left { top: 56px; left: 16px; }
+      .hud-top-right { top: 56px; right: 16px; }
+      .hud-bottom-left { bottom: 64px; left: 16px; max-width: 320px; }
+      .hud-bottom-right { bottom: 64px; right: 16px; }
+      .hud-mid-right { top: 210px; right: 16px; }
+      .hud-mid-left { top: 210px; left: 16px; }
+
+      /* ═══ Feed ═══ */
+      .hud-feed-scroll { max-height: 220px; overflow-y: auto; pointer-events: all; }
+      .hud-feed-scroll::-webkit-scrollbar { width: 3px; }
+      .hud-feed-scroll::-webkit-scrollbar-track { background: transparent; }
+      .hud-feed-scroll::-webkit-scrollbar-thumb { background: rgba(148,163,184,0.15); border-radius: 2px; }
+      .hud-feed-badge {
+        font-size: 10px; font-weight: 600;
+        padding: 2px 8px; border-radius: var(--ds-radius-full);
+        background: rgba(139,92,246,0.15);
+        color: var(--ds-violet);
+        margin-left: 4px;
+      }
+      .feed-item {
+        padding: 5px 0;
+        border-bottom: 1px solid var(--ds-border);
+        font-size: 12px; line-height: 1.5;
+      }
+      .feed-item:last-child { border-bottom: none; }
+      .feed-time {
+        color: var(--ds-gray-500);
+        font-size: 10px; margin-right: 6px;
+        font-variant-numeric: tabular-nums;
+      }
+
+      /* ═══ Metrics ═══ */
+      .hud-metric {
+        display: flex; justify-content: space-between; align-items: center;
+        padding: 4px 0;
+        border-bottom: 1px solid var(--ds-border);
+      }
+      .hud-metric:last-child { border-bottom: none; }
+      .hud-metric-label {
+        font-size: 11px; color: var(--ds-gray-400);
+        letter-spacing: 0.5px; font-weight: 500;
+      }
+      .hud-metric-value {
+        font-size: 18px; font-weight: 700;
+        color: var(--ds-gray-100);
+        font-variant-numeric: tabular-nums;
+      }
+
+      /* ═══ Decisions Grid ═══ */
+      .decision-grid {
+        display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
+      }
+      .decision-stat {
+        display: flex; flex-direction: column; align-items: center;
+        padding: 6px 4px;
+        background: rgba(255,255,255,0.02);
+        border-radius: var(--ds-radius-sm);
+        border: 1px solid var(--ds-border);
+        transition: background var(--ds-duration-fast) var(--ds-ease);
+      }
+      .decision-count {
+        font-size: 26px; font-weight: 800;
+        font-variant-numeric: tabular-nums;
+        line-height: 1.2;
+      }
+      .decision-label {
+        font-size: 11px; color: var(--ds-gray-400);
+        letter-spacing: 0.5px; margin-top: 2px;
+      }
+      .decision-total {
+        margin-top: 8px; font-size: 12px;
+        color: var(--ds-gray-400);
+        border-top: 1px solid var(--ds-border-strong);
+        padding-top: 6px;
+      }
+
+      /* ═══ Escalations ═══ */
+      .escalation-header {
+        font-size: 12px; color: var(--ds-error);
+        font-weight: 700; margin-bottom: 8px;
+        display: flex; align-items: center; gap: 5px;
+      }
+      .escalation-header::before {
+        content: '';
+        width: 6px; height: 6px; border-radius: var(--ds-radius-full);
+        background: var(--ds-error);
+        animation: dotPulse 1s ease-in-out infinite;
+      }
+      .escalation-row {
+        padding: 5px 0;
+        border-bottom: 1px solid var(--ds-border);
+        font-size: 12px;
+      }
+      .escalation-row:last-child { border-bottom: none; }
+      .escalation-hours {
+        color: var(--ds-error); font-weight: 600;
+      }
+      .escalation-action {
+        color: var(--ds-success);
+        font-size: 9px; margin-left: 4px;
+      }
+
+      /* ═══ Agent Tooltip ═══ */
+      .hud-agent-tooltip {
+        position: absolute;
+        padding: 14px 18px;
+        background: var(--ds-glass-strong);
+        backdrop-filter: var(--ds-blur-lg);
+        -webkit-backdrop-filter: var(--ds-blur-lg);
+        border: 1px solid var(--ds-border-interactive);
+        border-radius: var(--ds-radius-lg);
+        min-width: 190px;
+        pointer-events: none; z-index: 20;
+        box-shadow: var(--ds-shadow-lg), var(--ds-shadow-glow-violet);
+      }
+      .tooltip-header { font-size: 14px; font-weight: 700; margin-bottom: 6px; }
+      .tooltip-role { font-size: 11px; color: var(--ds-gray-400); margin-bottom: 3px; }
+      .tooltip-dept { font-size: 10px; color: var(--ds-gray-500); }
+      .tooltip-exec { font-size: 10px; color: var(--ds-gray-500); }
+      .tooltip-status { font-size: 10px; font-weight: 600; margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--ds-border); }
+
+      /* ═══ Bottom Bar ═══ */
+      .hud-bottom-bar {
+        position: absolute; bottom: 0; left: 0; right: 0;
+        display: flex; justify-content: space-between; align-items: center;
+        padding: 10px 24px;
+        background: linear-gradient(0deg, rgba(5,5,16,0.95) 0%, rgba(5,5,16,0.6) 70%, transparent 100%);
+      }
+      .hud-bottom-nav {
+        display: flex; gap: 6px;
+      }
+      .hud-view-btn {
+        pointer-events: all;
+        display: flex; align-items: center; gap: 6px;
+        padding: 6px 14px; border-radius: var(--ds-radius-md);
+        border: 1px solid var(--ds-border-strong);
+        background: var(--ds-glass);
+        backdrop-filter: var(--ds-blur-sm);
+        -webkit-backdrop-filter: var(--ds-blur-sm);
+        color: var(--ds-gray-300); font-size: 12px; font-weight: 600;
+        cursor: pointer; font-family: inherit;
+        transition: all var(--ds-duration-fast) var(--ds-ease);
+        letter-spacing: 0.3px;
+      }
+      .hud-view-btn svg {
+        width: 13px; height: 13px; opacity: 0.7;
+        transition: opacity var(--ds-duration-fast) var(--ds-ease);
+      }
+      .hud-view-btn:hover {
+        border-color: var(--ds-border-interactive);
+        color: var(--ds-gray-100);
+        background: rgba(99,102,241,0.08);
+        box-shadow: var(--ds-shadow-sm), 0 0 12px rgba(99,102,241,0.1);
+      }
+      .hud-view-btn:hover svg { opacity: 1; }
+      .hud-view-btn:active {
+        transform: scale(0.97);
+      }
+
+      .hud-bottom-info {
+        display: flex; align-items: center; gap: 12px;
+      }
+      .hud-agent-count {
+        font-size: 12px; color: var(--ds-gray-400);
+      }
+      .hud-bottom-divider {
+        width: 1px; height: 12px;
+        background: var(--ds-border-strong);
+      }
+      .hud-clock {
+        font-size: 13px; color: var(--ds-gray-400);
+        font-variant-numeric: tabular-nums;
+        font-weight: 500;
+        letter-spacing: 0.3px;
+      }
+    `;
+    document.head.appendChild(style);
+  }
+}
diff --git a/src/scene/AgentNodes.ts b/src/scene/AgentNodes.ts
new file mode 100644
index 0000000..d9b5813
--- /dev/null
+++ b/src/scene/AgentNodes.ts
@@ -0,0 +1,256 @@
+// ═══════════════════════════════════════════════
+// DW War Room 3D — Agent Node Visualization
+// 47 agent spheres in org hierarchy + 5 exec nodes
+// Text label sprites + raycasting support
+// ═══════════════════════════════════════════════
+
+import * as THREE from 'three';
+import type { AgentNode } from '../types';
+
+// Exec colors matching production config
+const EXEC_COLORS: Record<string, number> = {
+  ceo: 0x5B21B6,
+  cfo: 0x065F46,
+  coo: 0xCD7F32,
+  cto: 0x9B111E,
+  'vp-ops': 0x6D28D9,
+};
+
+// Department → exec mapping
+const DEPT_EXEC: Record<string, string> = {
+  'Operations': 'coo',
+  'Commerce': 'cfo',
+  'Finance': 'cfo',
+  'Legal': 'ceo',
+  'Marketing': 'vp-ops',
+  'Catalogs': 'vp-ops',
+  'Catalogs-Heritage': 'vp-ops',
+  'Catalogs-Modern': 'vp-ops',
+  'Catalogs-National': 'vp-ops',
+  'Pipeline': 'vp-ops',
+  'Data': 'cto',
+  'Imagery': 'vp-ops',
+};
+
+// ─── Text sprite helper ────────────────────────────────────────────────────
+function createTextSprite(text: string, colorHex: string, fontSize: number = 28): THREE.Sprite {
+  const canvas = document.createElement('canvas');
+  canvas.width = 256;
+  canvas.height = 64;
+  const ctx = canvas.getContext('2d')!;
+
+  // Transparent background
+  ctx.clearRect(0, 0, 256, 64);
+
+  // Text with glow
+  ctx.font = `bold ${fontSize}px Inter, -apple-system, monospace`;
+  ctx.textAlign = 'center';
+  ctx.textBaseline = 'middle';
+
+  // Shadow/glow
+  ctx.shadowColor = '#000';
+  ctx.shadowBlur = 4;
+  ctx.shadowOffsetX = 0;
+  ctx.shadowOffsetY = 1;
+
+  ctx.fillStyle = colorHex;
+  ctx.fillText(text, 128, 32);
+
+  const texture = new THREE.CanvasTexture(canvas);
+  texture.minFilter = THREE.LinearFilter;
+  const mat = new THREE.SpriteMaterial({
+    map: texture,
+    transparent: true,
+    depthTest: false,
+  });
+  const sprite = new THREE.Sprite(mat);
+  sprite.scale.set(2, 0.5, 1);
+  return sprite;
+}
+
+export class AgentNodeManager {
+  private nodes: Map<string, { mesh: THREE.Mesh; glow: THREE.Mesh; label: THREE.Sprite; data: AgentNode }> = new Map();
+  private group: THREE.Group;
+  private connections: THREE.Line[] = [];
+
+  constructor(private scene: THREE.Scene) {
+    this.group = new THREE.Group();
+    this.group.name = 'agent-nodes';
+    scene.add(this.group);
+  }
+
+  buildHierarchy(agents: any[], executives: any[]): void {
+    // Clear existing
+    this.group.clear();
+    this.nodes.clear();
+    this.connections.forEach(c => this.scene.remove(c));
+    this.connections = [];
+
+    // Place executives in a ring at top
+    const execRadius = 3;
+    executives.forEach((exec: any, i: number) => {
+      const angle = (i / executives.length) * Math.PI * 2 - Math.PI / 2;
+      const x = Math.cos(angle) * execRadius;
+      const z = Math.sin(angle) * execRadius;
+      this.createNode(exec.id, exec.name, exec.role, 'Executive', exec.color, x, 5, z, 0.4);
+    });
+
+    // Place agents in rings below their exec
+    const execAgents: Record<string, any[]> = {};
+    agents.forEach((agent: any) => {
+      const exec = DEPT_EXEC[agent.dept] || 'vp-ops';
+      if (!execAgents[exec]) execAgents[exec] = [];
+      execAgents[exec].push(agent);
+    });
+
+    Object.entries(execAgents).forEach(([execId, agentList]) => {
+      const execNode = this.nodes.get(execId);
+      if (!execNode) return;
+
+      const baseX = execNode.data.position.x;
+      const baseZ = execNode.data.position.z;
+      const ringRadius = Math.min(2 + agentList.length * 0.15, 5);
+
+      agentList.forEach((agent: any, i: number) => {
+        const angle = (i / agentList.length) * Math.PI * 2;
+        const x = baseX + Math.cos(angle) * ringRadius;
+        const z = baseZ + Math.sin(angle) * ringRadius;
+        const y = 2;
+
+        this.createNode(agent.id, agent.name, agent.role, agent.dept, agent.color, x, y, z, 0.2);
+
+        // Connection line to exec
+        this.drawConnection(execNode.mesh.position, new THREE.Vector3(x, y, z), execNode.data.color);
+      });
+    });
+  }
+
+  private createNode(
+    id: string, name: string, role: string, dept: string,
+    colorHex: string, x: number, y: number, z: number, size: number
+  ): void {
+    const color = new THREE.Color(colorHex);
+
+    // Main sphere — MeshStandardMaterial for PBR lighting
+    const geo = new THREE.SphereGeometry(size, 32, 32);
+    const mat = new THREE.MeshStandardMaterial({
+      color,
+      emissive: color,
+      emissiveIntensity: 0.25,
+      metalness: 0.6,
+      roughness: 0.3,
+      transparent: true,
+      opacity: 0.92,
+    });
+    const mesh = new THREE.Mesh(geo, mat);
+    mesh.position.set(x, y, z);
+    mesh.name = `node-${id}`;
+    // Store agent ID in userData for raycasting lookup
+    mesh.userData.agentId = id;
+    this.group.add(mesh);
+
+    // Glow shell (invisible by default — lights up when speaking)
+    const glowGeo = new THREE.SphereGeometry(size * 1.6, 16, 16);
+    const glowMat = new THREE.MeshBasicMaterial({
+      color,
+      transparent: true,
+      opacity: 0.0,
+    });
+    const glow = new THREE.Mesh(glowGeo, glowMat);
+    glow.position.set(x, y, z);
+    glow.name = `glow-${id}`;
+    this.group.add(glow);
+
+    // Text label above node
+    const isExec = dept === 'Executive';
+    const displayName = isExec ? name.toUpperCase() : name.split(' ').pop() || name;
+    const label = createTextSprite(displayName, isExec ? '#e8eaf0' : '#9ca3af', isExec ? 30 : 22);
+    label.position.set(x, y + size + 0.5, z);
+    label.scale.set(isExec ? 2.4 : 1.6, isExec ? 0.6 : 0.4, 1);
+    this.group.add(label);
+
+    const data: AgentNode = {
+      id, name, role, dept, color: colorHex,
+      exec: DEPT_EXEC[dept] || 'vp-ops',
+      speaking: false,
+      position: { x, y, z },
+    };
+    this.nodes.set(id, { mesh, glow, label, data });
+  }
+
+  private drawConnection(from: THREE.Vector3, to: THREE.Vector3, colorHex: string): void {
+    const points = [from.clone(), to.clone()];
+    const geo = new THREE.BufferGeometry().setFromPoints(points);
+    const mat = new THREE.LineBasicMaterial({
+      color: new THREE.Color(colorHex),
+      transparent: true,
+      opacity: 0.15,
+    });
+    const line = new THREE.Line(geo, mat);
+    this.scene.add(line);
+    this.connections.push(line);
+  }
+
+  setSpeaking(agentId: string, speaking: boolean): void {
+    const node = this.nodes.get(agentId);
+    if (!node) return;
+
+    node.data.speaking = speaking;
+    const glowMat = node.glow.material as THREE.MeshBasicMaterial;
+    glowMat.opacity = speaking ? 0.3 : 0.0;
+
+    const meshMat = node.mesh.material as THREE.MeshStandardMaterial;
+    meshMat.emissiveIntensity = speaking ? 0.9 : 0.25;
+  }
+
+  clearAllSpeaking(): void {
+    this.nodes.forEach((node) => {
+      this.setSpeaking(node.data.id, false);
+    });
+  }
+
+  // Get all agent sphere meshes for raycasting
+  getMeshes(): THREE.Mesh[] {
+    const meshes: THREE.Mesh[] = [];
+    this.nodes.forEach(node => meshes.push(node.mesh));
+    return meshes;
+  }
+
+  // Lookup agent data from a raycasted mesh
+  getNodeByMesh(mesh: THREE.Object3D): AgentNode | null {
+    const agentId = mesh.userData?.agentId;
+    if (!agentId) return null;
+    const node = this.nodes.get(agentId);
+    return node ? node.data : null;
+  }
+
+  // Get agent data by ID
+  getAgent(id: string): AgentNode | null {
+    const node = this.nodes.get(id);
+    return node ? node.data : null;
+  }
+
+  // Get all agent data
+  getAllAgents(): AgentNode[] {
+    const agents: AgentNode[] = [];
+    this.nodes.forEach(n => agents.push(n.data));
+    return agents;
+  }
+
+  animate(time: number): void {
+    // Subtle floating animation for all nodes
+    this.nodes.forEach((node) => {
+      const offset = parseInt(node.data.id, 36) * 0.5;
+      node.mesh.position.y = node.data.position.y + Math.sin(time * 0.001 + offset) * 0.05;
+      node.glow.position.y = node.mesh.position.y;
+      // Keep label above the sphere
+      node.label.position.y = node.mesh.position.y + 0.5;
+
+      // Pulse glow for speaking agents
+      if (node.data.speaking) {
+        const glowMat = node.glow.material as THREE.MeshBasicMaterial;
+        glowMat.opacity = 0.15 + Math.sin(time * 0.005) * 0.15;
+      }
+    });
+  }
+}
diff --git a/src/scene/CommandTable.ts b/src/scene/CommandTable.ts
new file mode 100644
index 0000000..425e48a
--- /dev/null
+++ b/src/scene/CommandTable.ts
@@ -0,0 +1,270 @@
+// ═══════════════════════════════════════════════
+// DW War Room 3D — Command Table
+// Central holographic table in the war room
+// ═══════════════════════════════════════════════
+
+import * as THREE from 'three';
+
+// ─── Types ───────────────────────────────────────────────────────────────────
+
+export interface CommandTableHandle {
+  animate(elapsed: number): void;
+}
+
+// ─── Helpers ─────────────────────────────────────────────────────────────────
+
+/** Build a flat torus lying horizontal (rotated -90° on X). */
+function horizontalTorus(
+  radius: number,
+  tube: number,
+  radialSeg: number,
+  tubularSeg: number,
+  color: number,
+  opacity: number,
+  additive = false,
+): THREE.Mesh {
+  const geo = new THREE.TorusGeometry(radius, tube, radialSeg, tubularSeg);
+  const mat = new THREE.MeshBasicMaterial({
+    color,
+    transparent: true,
+    opacity,
+    blending: additive ? THREE.AdditiveBlending : THREE.NormalBlending,
+    depthWrite: false,
+  });
+  const mesh = new THREE.Mesh(geo, mat);
+  mesh.rotation.x = -Math.PI / 2;
+  return mesh;
+}
+
+// ─── Main factory ────────────────────────────────────────────────────────────
+
+export function createCommandTable(scene: THREE.Scene): CommandTableHandle {
+  const group = new THREE.Group();
+  group.name = 'command-table';
+
+  // ── 1. Table base — dark cylinder ────────────────────────────────────────
+  const baseGeo = new THREE.CylinderGeometry(6, 7, 0.3, 64);
+  const baseMat = new THREE.MeshPhongMaterial({
+    color: 0x0a0a1e,
+    emissive: 0x1a1a3e,
+    emissiveIntensity: 0.4,
+    transparent: true,
+    opacity: 0.92,
+  });
+  const base = new THREE.Mesh(baseGeo, baseMat);
+  base.position.y = 0.15;
+  group.add(base);
+
+  // ── 2. Animated outer ring (purple) ──────────────────────────────────────
+  const outerRing = horizontalTorus(6.2, 0.08, 8, 96, 0x8b5cf6, 0.75, true);
+  outerRing.position.y = 0.35;
+  group.add(outerRing);
+
+  // ── 3. Secondary outer glow ring (same radius, wider tube, very faint) ───
+  const outerGlow = horizontalTorus(6.2, 0.22, 8, 64, 0x8b5cf6, 0.12, true);
+  outerGlow.position.y = 0.35;
+  group.add(outerGlow);
+
+  // ── 4. Inner ring (blue) ─────────────────────────────────────────────────
+  const innerRing = horizontalTorus(4, 0.05, 8, 64, 0x3b82f6, 0.5, true);
+  innerRing.position.y = 0.36;
+  group.add(innerRing);
+
+  // ── 5. Mid ring (blue, thin) ─────────────────────────────────────────────
+  const midRing = horizontalTorus(5.1, 0.035, 8, 64, 0x3b82f6, 0.3, true);
+  midRing.position.y = 0.36;
+  group.add(midRing);
+
+  // ── 6. Edge glow ring — bright emissive at table edge ────────────────────
+  const edgeGlowGeo = new THREE.TorusGeometry(6.85, 0.06, 8, 96);
+  const edgeGlowMat = new THREE.MeshStandardMaterial({
+    color: 0x8b5cf6,
+    emissive: 0x8b5cf6,
+    emissiveIntensity: 2.5,
+    transparent: true,
+    opacity: 0.55,
+    blending: THREE.AdditiveBlending,
+    depthWrite: false,
+  });
+  const edgeGlow = new THREE.Mesh(edgeGlowGeo, edgeGlowMat);
+  edgeGlow.rotation.x = -Math.PI / 2;
+  edgeGlow.position.y = 0.33;
+  group.add(edgeGlow);
+
+  // ── 7. Animated grid ─────────────────────────────────────────────────────
+  const gridHelper = new THREE.GridHelper(12, 20, 0x3b3366, 0x1a1a3e);
+  gridHelper.position.y = 0.32;
+  // GridHelper material is transparent by default — boost it slightly
+  const gridMats = Array.isArray(gridHelper.material)
+    ? gridHelper.material
+    : [gridHelper.material];
+  gridMats.forEach((m) => {
+    (m as THREE.LineBasicMaterial).transparent = true;
+    (m as THREE.LineBasicMaterial).opacity = 0.55;
+    (m as THREE.LineBasicMaterial).blending = THREE.AdditiveBlending;
+  });
+  group.add(gridHelper);
+
+  // ── 8. Holographic projection cone ───────────────────────────────────────
+  // Open-ended cone pointing upward, wide base at table level, tip at top
+  const coneGeo = new THREE.ConeGeometry(5.5, 9, 64, 1, true);
+  const coneMat = new THREE.MeshBasicMaterial({
+    color: 0x8b5cf6,
+    transparent: true,
+    opacity: 0.06,
+    side: THREE.DoubleSide,
+    blending: THREE.AdditiveBlending,
+    depthWrite: false,
+  });
+  const cone = new THREE.Mesh(coneGeo, coneMat);
+  // Base of cone sits at table surface (y=0.35), tip points up to y≈9.35
+  cone.position.y = 0.35 + 9 / 2; // y = 4.85
+  group.add(cone);
+
+  // Second cone pass — slightly smaller, more opaque for depth
+  const coneInnerGeo = new THREE.ConeGeometry(3.2, 7, 48, 1, true);
+  const coneInnerMat = new THREE.MeshBasicMaterial({
+    color: 0x3b82f6,
+    transparent: true,
+    opacity: 0.045,
+    side: THREE.DoubleSide,
+    blending: THREE.AdditiveBlending,
+    depthWrite: false,
+  });
+  const coneInner = new THREE.Mesh(coneInnerGeo, coneInnerMat);
+  coneInner.position.y = 0.35 + 7 / 2; // y = 3.85
+  group.add(coneInner);
+
+  // ── 9. Pulsing energy core — sphere at y=2 ───────────────────────────────
+  const coreGeo = new THREE.SphereGeometry(0.22, 32, 32);
+  const coreMat = new THREE.MeshStandardMaterial({
+    color: 0xffffff,
+    emissive: 0x8b5cf6,
+    emissiveIntensity: 3.0,
+    transparent: true,
+    opacity: 0.9,
+    blending: THREE.AdditiveBlending,
+    depthWrite: false,
+  });
+  const core = new THREE.Mesh(coreGeo, coreMat);
+  core.position.y = 2;
+  group.add(core);
+
+  // Core halo — larger diffuse sphere around the core
+  const haloGeo = new THREE.SphereGeometry(0.55, 24, 24);
+  const haloMat = new THREE.MeshBasicMaterial({
+    color: 0x8b5cf6,
+    transparent: true,
+    opacity: 0.12,
+    blending: THREE.AdditiveBlending,
+    depthWrite: false,
+  });
+  const halo = new THREE.Mesh(haloGeo, haloMat);
+  halo.position.y = 2;
+  group.add(halo);
+
+  // ── 10. Data particle column ─────────────────────────────────────────────
+  const PARTICLE_COUNT = 200;
+  const particlePositions = new Float32Array(PARTICLE_COUNT * 3);
+  // Random speeds per particle (stored separately; not in geometry)
+  const particleSpeeds = new Float32Array(PARTICLE_COUNT);
+  const particleAngles = new Float32Array(PARTICLE_COUNT);
+  const particleRadii = new Float32Array(PARTICLE_COUNT);
+
+  for (let i = 0; i < PARTICLE_COUNT; i++) {
+    const angle = Math.random() * Math.PI * 2;
+    const radius = Math.random() * 4.5 + 0.5; // 0.5 – 5.0 from center
+    const startY = Math.random() * 8;          // scattered initial heights
+
+    particleAngles[i] = angle;
+    particleRadii[i] = radius;
+    particleSpeeds[i] = 0.4 + Math.random() * 1.2; // units/s
+
+    particlePositions[i * 3]     = Math.cos(angle) * radius;
+    particlePositions[i * 3 + 1] = startY;
+    particlePositions[i * 3 + 2] = Math.sin(angle) * radius;
+  }
+
+  const particleGeo = new THREE.BufferGeometry();
+  particleGeo.setAttribute(
+    'position',
+    new THREE.BufferAttribute(particlePositions, 3),
+  );
+
+  const particleMat = new THREE.PointsMaterial({
+    color: 0x8b5cf6,
+    size: 0.05,
+    transparent: true,
+    opacity: 0.7,
+    blending: THREE.AdditiveBlending,
+    depthWrite: false,
+    sizeAttenuation: true,
+  });
+
+  const particles = new THREE.Points(particleGeo, particleMat);
+  group.add(particles);
+
+  // ── Finalize group ────────────────────────────────────────────────────────
+  scene.add(group);
+
+  // ─── Animate closure ────────────────────────────────────────────────────
+  // elapsed is in milliseconds (matching the convention used in WarRoom.ts)
+  let lastElapsed = 0;
+
+  return {
+    animate(elapsed: number): void {
+      const t = elapsed / 1000; // seconds
+      const dt = (elapsed - lastElapsed) / 1000;
+      lastElapsed = elapsed;
+
+      // — Outer ring: slow clockwise rotation on Y axis —
+      outerRing.rotation.z += 0.004;
+      outerGlow.rotation.z -= 0.002; // counter-rotate for layered feel
+
+      // — Inner/mid rings: gentle counter-rotation —
+      innerRing.rotation.z -= 0.006;
+      midRing.rotation.z += 0.003;
+
+      // — Animated grid: very slow rotation —
+      gridHelper.rotation.y += 0.001;
+
+      // — Edge glow: pulse brightness via emissiveIntensity —
+      const edgePulse = 1.8 + Math.sin(t * 1.8) * 0.7;
+      (edgeGlowMat as THREE.MeshStandardMaterial).emissiveIntensity = edgePulse;
+
+      // — Energy core: scale + emissive pulse —
+      const corePulse = 0.85 + Math.sin(t * 2.4) * 0.25;
+      core.scale.setScalar(corePulse);
+      (coreMat as THREE.MeshStandardMaterial).emissiveIntensity =
+        2.5 + Math.sin(t * 2.4) * 1.5;
+      halo.scale.setScalar(0.9 + Math.sin(t * 2.4 + 0.5) * 0.35);
+      (haloMat as THREE.MeshBasicMaterial).opacity =
+        0.08 + Math.sin(t * 2.4) * 0.06;
+
+      // — Holographic cones: very slow counter-rotations on Y —
+      cone.rotation.y += 0.0015;
+      coneInner.rotation.y -= 0.002;
+
+      // — Data particles: rise upward, respawn at bottom —
+      const posAttr = particleGeo.attributes['position'] as THREE.BufferAttribute;
+      const posArray = posAttr.array as Float32Array;
+      const COLUMN_TOP = 8.5;
+
+      for (let i = 0; i < PARTICLE_COUNT; i++) {
+        posArray[i * 3 + 1] += particleSpeeds[i] * dt;
+
+        if (posArray[i * 3 + 1] > COLUMN_TOP) {
+          // Respawn at table surface with fresh random position
+          const newAngle = Math.random() * Math.PI * 2;
+          const newRadius = Math.random() * 4.5 + 0.5;
+          particleAngles[i] = newAngle;
+          particleRadii[i] = newRadius;
+          posArray[i * 3]     = Math.cos(newAngle) * newRadius;
+          posArray[i * 3 + 1] = 0.35 + Math.random() * 0.5; // just above table
+          posArray[i * 3 + 2] = Math.sin(newAngle) * newRadius;
+        }
+      }
+      posAttr.needsUpdate = true;
+    },
+  };
+}
diff --git a/src/scene/DataWalls.ts b/src/scene/DataWalls.ts
new file mode 100644
index 0000000..f5aa6bd
--- /dev/null
+++ b/src/scene/DataWalls.ts
@@ -0,0 +1,387 @@
+// ═══════════════════════════════════════════════
+// DW War Room 3D — Data Wall Panels
+// Floating panels showing live metrics
+// Canvas-textured with scanline sweep + corner brackets
+// ═══════════════════════════════════════════════
+//
+// Usage:
+//   const walls = new DataWalls(scene);
+//   walls.animate(elapsedMs);
+//   walls.pulsePanel('escalations', 0.3);
+//   walls.updatePanelData('decisions', { Pending: '7', Approved: '42', Rejected: '3' });
+
+import * as THREE from 'three';
+
+// ─── Canvas texture dimensions ───────────────────────────────────────────────
+const CANVAS_W = 768;
+const CANVAS_H = 512;
+
+// ─── Scanline sweep period (ms) ──────────────────────────────────────────────
+const SWEEP_PERIOD_MS = 4000;
+
+// ─── Default metric values per panel ────────────────────────────────────────
+const DEFAULT_DATA: Record<string, Record<string, string>> = {
+  decisions:   { Pending: '—', Approved: '—', Rejected: '—', Deferred: '—' },
+  escalations: { Active: '—', 'Avg Age': '—h', Critical: '—', Resolved: '—' },
+  meeting:     { Status: 'IDLE', Phase: '—', Agents: '—', Duration: '—' },
+  status:      { 'CPU': '—%', 'Memory': '—%', 'Services': '—', 'Uptime': '—' },
+};
+
+// ─── Panel definition ────────────────────────────────────────────────────────
+interface PanelDef {
+  name: string;
+  x: number;
+  y: number;
+  z: number;
+  width: number;
+  height: number;
+  accentColor: number;
+  accentHex: string;     // CSS hex for canvas rendering
+  labelCaps: string;     // Header text shown on the panel
+}
+
+const PANEL_DEFS: PanelDef[] = [
+  { name: 'decisions',   x: -12, y: 6, z: 0, width: 5, height: 4, accentColor: 0x8b5cf6, accentHex: '#8b5cf6', labelCaps: 'DECISIONS'   },
+  { name: 'escalations', x:  12, y: 6, z: 0, width: 5, height: 4, accentColor: 0xef4444, accentHex: '#ef4444', labelCaps: 'ESCALATIONS' },
+  { name: 'meeting',     x: -10, y: 2, z: 8, width: 6, height: 3, accentColor: 0x3b82f6, accentHex: '#3b82f6', labelCaps: 'MEETING'     },
+  { name: 'status',      x:  10, y: 2, z: 8, width: 6, height: 3, accentColor: 0x22c55e, accentHex: '#22c55e', labelCaps: 'SYS STATUS'  },
+];
+
+// ─── Per-panel runtime state ─────────────────────────────────────────────────
+interface PanelState {
+  def: PanelDef;
+  mesh: THREE.Mesh;
+  canvas: HTMLCanvasElement;
+  ctx: CanvasRenderingContext2D;
+  texture: THREE.CanvasTexture;
+  sweepY: number;           // current scanline y position in canvas pixels
+  data: Record<string, string>;
+  baseY: number;            // world-space Y at creation (for hover animation)
+}
+
+// ─── Corner bracket geometry helpers ────────────────────────────────────────
+// Returns 8 points (4 corners × 2 lines each) for L-shaped corner brackets.
+// All coordinates are in local panel space; the caller positions the LineSegments.
+function buildCornerBracketPoints(w: number, h: number, arm: number): Float32Array {
+  const hw = w / 2;
+  const hh = h / 2;
+  // Each corner: 2 line segments = 4 points.
+  // Order: (x1,y1,z), (x2,y2,z) per segment.
+  return new Float32Array([
+    // Top-left corner — horizontal arm
+    -hw, hh, 0,  -hw + arm, hh, 0,
+    // Top-left corner — vertical arm
+    -hw, hh, 0,  -hw, hh - arm, 0,
+
+    // Top-right corner — horizontal arm
+     hw, hh, 0,   hw - arm, hh, 0,
+    // Top-right corner — vertical arm
+     hw, hh, 0,   hw, hh - arm, 0,
+
+    // Bottom-left corner — horizontal arm
+    -hw, -hh, 0,  -hw + arm, -hh, 0,
+    // Bottom-left corner — vertical arm
+    -hw, -hh, 0,  -hw, -hh + arm, 0,
+
+    // Bottom-right corner — horizontal arm
+     hw, -hh, 0,   hw - arm, -hh, 0,
+    // Bottom-right corner — vertical arm
+     hw, -hh, 0,   hw, -hh + arm, 0,
+  ]);
+}
+
+// ─── Canvas draw helpers ────────────────────────────────────────────────────
+
+function drawPanelCanvas(
+  ctx: CanvasRenderingContext2D,
+  def: PanelDef,
+  data: Record<string, string>,
+  sweepY: number
+): void {
+  const { accentHex, labelCaps } = def;
+  const W = CANVAS_W;
+  const H = CANVAS_H;
+
+  // Background
+  ctx.fillStyle = '#0a0a1e';
+  ctx.fillRect(0, 0, W, H);
+
+  // Header bar (top strip, 52px tall)
+  const headerH = 52;
+  ctx.fillStyle = accentHex;
+  ctx.globalAlpha = 0.18;
+  ctx.fillRect(0, 0, W, headerH);
+  ctx.globalAlpha = 1.0;
+
+  // Header text
+  ctx.font = 'bold 22px Inter, monospace';
+  ctx.fillStyle = accentHex;
+  ctx.textBaseline = 'middle';
+  ctx.textAlign = 'left';
+  ctx.fillText(labelCaps, 18, headerH / 2);
+
+  // Live indicator dot
+  const dotX = W - 14;
+  const dotY = headerH / 2;
+  ctx.beginPath();
+  ctx.arc(dotX, dotY, 4, 0, Math.PI * 2);
+  ctx.fillStyle = accentHex;
+  ctx.globalAlpha = 0.9;
+  ctx.fill();
+  ctx.globalAlpha = 1.0;
+
+  // Divider line below header
+  ctx.strokeStyle = accentHex;
+  ctx.globalAlpha = 0.35;
+  ctx.lineWidth = 1;
+  ctx.beginPath();
+  ctx.moveTo(0, headerH);
+  ctx.lineTo(W, headerH);
+  ctx.stroke();
+  ctx.globalAlpha = 1.0;
+
+  // Metric rows
+  const entries = Object.entries(data);
+  const rowH = Math.floor((H - headerH - 24) / Math.max(entries.length, 1));
+  const colLabelX = 18;
+  const colValueX = W - 18;
+  const startY = headerH + 14;
+
+  entries.forEach(([key, value], i) => {
+    const rowY = startY + i * rowH + rowH / 2;
+
+    // Key label — larger, brighter for legibility
+    ctx.font = '16px Inter, monospace';
+    ctx.fillStyle = '#b0b4c8';
+    ctx.textAlign = 'left';
+    ctx.textBaseline = 'middle';
+    ctx.fillText(key.toUpperCase(), colLabelX, rowY);
+
+    // Value — accent-colored, bold, much larger
+    ctx.font = 'bold 28px Inter, monospace';
+    ctx.fillStyle = value === '—' ? '#3a3a5a' : accentHex;
+    ctx.textAlign = 'right';
+    ctx.fillText(value, colValueX, rowY);
+
+    // Subtle row divider
+    if (i < entries.length - 1) {
+      ctx.strokeStyle = '#1e1e3a';
+      ctx.globalAlpha = 0.8;
+      ctx.lineWidth = 1;
+      ctx.beginPath();
+      ctx.moveTo(colLabelX, rowY + rowH / 2);
+      ctx.lineTo(W - colLabelX, rowY + rowH / 2);
+      ctx.stroke();
+      ctx.globalAlpha = 1.0;
+    }
+
+    // Progress bar beneath value (proportional fill based on numeric parse)
+    const numVal = parseFloat(value);
+    if (!isNaN(numVal) && numVal > 0) {
+      const barMaxW = 140;
+      const barH = 6;
+      const barY = rowY + 16;
+      const fill = Math.min(numVal / 100, 1.0);
+      ctx.fillStyle = '#1a1a38';
+      ctx.beginPath();
+      ctx.roundRect(colValueX - barMaxW, barY, barMaxW, barH, 3);
+      ctx.fill();
+      ctx.fillStyle = accentHex;
+      ctx.globalAlpha = 0.8;
+      ctx.beginPath();
+      ctx.roundRect(colValueX - barMaxW, barY, barMaxW * fill, barH, 3);
+      ctx.fill();
+      ctx.globalAlpha = 1.0;
+    }
+  });
+
+  // Scanline sweep — bright horizontal band
+  if (sweepY >= 0 && sweepY <= H) {
+    const grad = ctx.createLinearGradient(0, sweepY - 6, 0, sweepY + 6);
+    grad.addColorStop(0, 'rgba(255,255,255,0)');
+    grad.addColorStop(0.4, `${accentHex}55`);
+    grad.addColorStop(0.5, `${accentHex}bb`);
+    grad.addColorStop(0.6, `${accentHex}55`);
+    grad.addColorStop(1, 'rgba(255,255,255,0)');
+    ctx.fillStyle = grad;
+    ctx.fillRect(0, sweepY - 6, W, 12);
+  }
+
+  // Subtle vignette border
+  const vignette = ctx.createRadialGradient(W / 2, H / 2, H * 0.25, W / 2, H / 2, H * 0.85);
+  vignette.addColorStop(0, 'rgba(0,0,0,0)');
+  vignette.addColorStop(1, 'rgba(0,0,0,0.45)');
+  ctx.fillStyle = vignette;
+  ctx.fillRect(0, 0, W, H);
+}
+
+// ─── DataWalls class ─────────────────────────────────────────────────────────
+
+export class DataWalls {
+  // Expose panels map for external pulsePanel usage (keyed by panel name)
+  private panels: Map<string, THREE.Mesh> = new Map();
+  private states: Map<string, PanelState> = new Map();
+  private group: THREE.Group;
+
+  constructor(private scene: THREE.Scene) {
+    this.group = new THREE.Group();
+    this.group.name = 'data-walls';
+    scene.add(this.group);
+    this.createPanels();
+  }
+
+  // ── Panel construction ──────────────────────────────────────────────────────
+  private createPanels(): void {
+    for (const def of PANEL_DEFS) {
+      this.createPanel(def);
+    }
+  }
+
+  private createPanel(def: PanelDef): void {
+    const { name, x, y, z, width, height, accentColor } = def;
+
+    // ── Canvas + texture ──
+    const canvas = document.createElement('canvas');
+    canvas.width = CANVAS_W;
+    canvas.height = CANVAS_H;
+    const ctx = canvas.getContext('2d')!;
+
+    const initialData = { ...DEFAULT_DATA[name] } || {};
+    drawPanelCanvas(ctx, def, initialData, -20);   // -20 = scanline off-screen initially
+
+    const texture = new THREE.CanvasTexture(canvas);
+    texture.minFilter = THREE.LinearFilter;
+    texture.magFilter = THREE.LinearFilter;
+
+    // ── Panel mesh (MeshBasicMaterial for sharp canvas text — no lighting wash) ──
+    const geo = new THREE.PlaneGeometry(width, height);
+    const mat = new THREE.MeshBasicMaterial({
+      map: texture,
+      transparent: true,
+      opacity: 0.92,
+      side: THREE.DoubleSide,
+    });
+    const mesh = new THREE.Mesh(geo, mat);
+    mesh.position.set(x, y, z);
+    mesh.name = `panel-${name}`;
+
+    // Face toward scene center slightly
+    if (x !== 0) {
+      mesh.rotation.y = x > 0 ? -Math.PI * 0.15 : Math.PI * 0.15;
+    }
+
+    this.group.add(mesh);
+    this.panels.set(name, mesh);
+
+    // ── Header bar strip — thin BoxGeometry at top of panel ──
+    const barThickness = 0.04;
+    const barGeo = new THREE.BoxGeometry(width, 0.22, barThickness);
+    const barMat = new THREE.MeshStandardMaterial({
+      color: accentColor,
+      emissive: new THREE.Color(accentColor),
+      emissiveIntensity: 0.5,
+      roughness: 0.4,
+      metalness: 0.2,
+    });
+    const bar = new THREE.Mesh(barGeo, barMat);
+    bar.name = `bar-${name}`;
+    // Position at top edge of panel, slightly in front of it
+    bar.position.set(x, y + height / 2 - 0.11, z + 0.02);
+    bar.rotation.copy(mesh.rotation);
+    this.group.add(bar);
+
+    // ── Corner brackets (Line segments — no full frame) ──
+    const armLength = Math.min(width, height) * 0.18;
+    const bracketPositions = buildCornerBracketPoints(width, height, armLength);
+    const bracketGeo = new THREE.BufferGeometry();
+    bracketGeo.setAttribute('position', new THREE.BufferAttribute(bracketPositions, 3));
+    const bracketMat = new THREE.LineBasicMaterial({
+      color: accentColor,
+      transparent: true,
+      opacity: 0.75,
+    });
+    const brackets = new THREE.LineSegments(bracketGeo, bracketMat);
+    brackets.position.copy(mesh.position);
+    brackets.rotation.copy(mesh.rotation);
+    // Offset slightly in front so brackets are visible over panel face
+    brackets.position.z += 0.01;
+    if (x !== 0 && z === 0) {
+      // Recalculate the Z-forward offset in world space after rotation
+      // Simpler: just nudge along local Z via translateZ after rotation is applied
+      brackets.translateZ(0.02);
+    }
+    this.group.add(brackets);
+
+    // ── Store runtime state ──
+    const state: PanelState = {
+      def,
+      mesh,
+      canvas,
+      ctx,
+      texture,
+      sweepY: 0,
+      data: initialData,
+      baseY: y,
+    };
+    this.states.set(name, state);
+  }
+
+  // ── Public: update panel data and redraw ────────────────────────────────────
+  updatePanelData(name: string, data: Record<string, string>): void {
+    const state = this.states.get(name);
+    if (!state) return;
+
+    // Merge incoming data over existing data
+    state.data = { ...state.data, ...data };
+    this.redrawPanel(state);
+  }
+
+  // ── Public: pulse emissive (brightness flash from external events) ──────────
+  pulsePanel(name: string, intensity: number): void {
+    // With MeshBasicMaterial there's no emissive, so we modulate opacity instead
+    const panel = this.panels.get(name);
+    if (!panel) return;
+    const mat = panel.material as THREE.MeshBasicMaterial;
+    // Map intensity to opacity range [0.70, 1.0]
+    mat.opacity = 0.70 + Math.min(intensity, 1.0) * 0.30;
+    mat.needsUpdate = true;
+
+    // Also briefly brighten the header bar — find it by name convention
+    const bar = this.group.children.find(
+      c => c instanceof THREE.Mesh && c.name === `bar-${name}`
+    ) as THREE.Mesh | undefined;
+    if (bar) {
+      const bMat = bar.material as THREE.MeshStandardMaterial;
+      bMat.emissiveIntensity = 0.5 + intensity * 0.8;
+    }
+  }
+
+  // ── Public: main animate loop ───────────────────────────────────────────────
+  animate(time: number): void {
+    this.states.forEach((state) => {
+      // Subtle hover — different phase per panel so they move independently
+      const phase = this.hashName(state.def.name);
+      state.mesh.position.y = state.baseY + Math.sin(time * 0.0008 + phase) * 0.03;
+
+      // Advance scanline sweep
+      const progress = (time % SWEEP_PERIOD_MS) / SWEEP_PERIOD_MS;
+      state.sweepY = Math.floor(progress * CANVAS_H);
+
+      // Redraw canvas with updated sweep position
+      this.redrawPanel(state);
+    });
+  }
+
+  // ── Internal redraw ──────────────────────────────────────────────────────────
+  private redrawPanel(state: PanelState): void {
+    drawPanelCanvas(state.ctx, state.def, state.data, state.sweepY);
+    state.texture.needsUpdate = true;
+  }
+
+  // ── Utility ──────────────────────────────────────────────────────────────────
+  private hashName(name: string): number {
+    let h = 0;
+    for (let i = 0; i < name.length; i++) h += name.charCodeAt(i);
+    return h;
+  }
+}
diff --git a/src/scene/Environment.ts b/src/scene/Environment.ts
new file mode 100644
index 0000000..cd7f338
--- /dev/null
+++ b/src/scene/Environment.ts
@@ -0,0 +1,183 @@
+// ═══════════════════════════════════════════════
+// DW War Room 3D — Room Environment
+// Architectural shell: floor grid, pillars, ceiling, walls
+// ═══════════════════════════════════════════════
+
+import * as THREE from 'three';
+
+export function createEnvironment(scene: THREE.Scene): void {
+  addFloorWithGrid(scene);
+  addPillars(scene);
+  addCeiling(scene);
+  addWallSegments(scene);
+}
+
+// ─── Floor + Grid ───────────────────────────────
+function addFloorWithGrid(scene: THREE.Scene): void {
+  // Base floor plane — industrial concrete/polished stone (not pure metallic)
+  const floorGeo = new THREE.PlaneGeometry(60, 60);
+  const floorMat = new THREE.MeshStandardMaterial({
+    color: 0x0b0f14,
+    metalness: 0.2,
+    roughness: 0.65,
+  });
+  const floor = new THREE.Mesh(floorGeo, floorMat);
+  floor.rotation.x = -Math.PI / 2;
+  floor.position.y = -0.01;
+  floor.receiveShadow = true;
+  scene.add(floor);
+
+  // Grid overlay — subtle cool-grey lines, not glowing neon
+  const grid = new THREE.GridHelper(60, 40, 0x1a2530, 0x0d1520);
+  grid.position.y = 0.01;
+  scene.add(grid);
+}
+
+// ─── Octagonal Pillar Ring ───────────────────────
+function addPillars(scene: THREE.Scene): void {
+  const COUNT = 8;
+  const RADIUS = 22;
+  const PILLAR_HEIGHT = 10;
+
+  // Brushed aluminum pillar — high metalness, low roughness
+  const pillarMat = new THREE.MeshStandardMaterial({
+    color: 0x141c24,
+    emissive: new THREE.Color(0x0a1018),
+    emissiveIntensity: 0.1,
+    metalness: 0.8,
+    roughness: 0.2,
+  });
+
+  // Warm gold torus rings at base and top — corporate accent, not neon
+  const ringMat = new THREE.MeshStandardMaterial({
+    color: 0xb8860b,
+    emissive: new THREE.Color(0xc8900c),
+    emissiveIntensity: 0.6,
+    metalness: 0.7,
+    roughness: 0.3,
+    transparent: true,
+    opacity: 0.55,
+  });
+
+  for (let i = 0; i < COUNT; i++) {
+    const angle = (i / COUNT) * Math.PI * 2;
+    const x = Math.cos(angle) * RADIUS;
+    const z = Math.sin(angle) * RADIUS;
+
+    // Main pillar body — octagonal cross-section (8 radial segments)
+    const pillarGeo = new THREE.CylinderGeometry(0.3, 0.3, PILLAR_HEIGHT, 8);
+    const pillar = new THREE.Mesh(pillarGeo, pillarMat);
+    pillar.position.set(x, PILLAR_HEIGHT / 2, z);
+    scene.add(pillar);
+
+    // Base ring
+    const baseRingGeo = new THREE.TorusGeometry(0.55, 0.06, 8, 24);
+    const baseRing = new THREE.Mesh(baseRingGeo, ringMat);
+    baseRing.rotation.x = Math.PI / 2;
+    baseRing.position.set(x, 0.12, z);
+    scene.add(baseRing);
+
+    // Top ring
+    const topRingGeo = new THREE.TorusGeometry(0.55, 0.06, 8, 24);
+    const topRing = new THREE.Mesh(topRingGeo, ringMat);
+    topRing.rotation.x = Math.PI / 2;
+    topRing.position.set(x, PILLAR_HEIGHT - 0.12, z);
+    scene.add(topRing);
+  }
+}
+
+// ─── Ceiling + Recessed Light Slots ─────────────
+function addCeiling(scene: THREE.Scene): void {
+  const CEILING_Y = 10;
+
+  // Ceiling plane — matte dark panel (not metallic — ceilings are typically matte)
+  const ceilGeo = new THREE.PlaneGeometry(50, 50);
+  const ceilMat = new THREE.MeshStandardMaterial({
+    color: 0x090d12,
+    metalness: 0.0,
+    roughness: 0.85,
+    transparent: true,
+    opacity: 0.45,
+    side: THREE.BackSide, // render from below
+  });
+  const ceiling = new THREE.Mesh(ceilGeo, ceilMat);
+  ceiling.rotation.x = Math.PI / 2;
+  ceiling.position.y = CEILING_Y;
+  scene.add(ceiling);
+
+  // 4 recessed light slots — warm white architectural lighting, not neon blue
+  const slotMat = new THREE.MeshStandardMaterial({
+    color: 0xe8e4d8,
+    emissive: new THREE.Color(0xd4c090),
+    emissiveIntensity: 0.7,
+    metalness: 0.1,
+    roughness: 0.5,
+    transparent: true,
+    opacity: 0.75,
+  });
+
+  const slotPositions: [number, number][] = [
+    [-8, -8],
+    [8, -8],
+    [-8, 8],
+    [8, 8],
+  ];
+
+  slotPositions.forEach(([sx, sz]) => {
+    const slotGeo = new THREE.BoxGeometry(6, 0.05, 0.4);
+    const slot = new THREE.Mesh(slotGeo, slotMat);
+    slot.position.set(sx, CEILING_Y - 0.03, sz);
+    scene.add(slot);
+  });
+}
+
+// ─── Wall Segments ───────────────────────────────
+function addWallSegments(scene: THREE.Scene): void {
+  const WALL_HEIGHT = 10;
+  const WALL_LENGTH = 50;
+  const OFFSET = 25;
+
+  // Matte architectural walls — zero metalness, high roughness (concrete/gypsum)
+  const wallMat = new THREE.MeshStandardMaterial({
+    color: 0x0a0e14,
+    emissive: new THREE.Color(0x080c10),
+    emissiveIntensity: 0.05,
+    metalness: 0.0,
+    roughness: 0.75,
+    transparent: true,
+    opacity: 0.6,
+    side: THREE.DoubleSide,
+  });
+
+  // Wall configs: [x, z, rotationY]
+  const walls: [number, number, number][] = [
+    [0, -OFFSET, 0],          // back  (+Z face)
+    [0,  OFFSET, Math.PI],    // front (-Z face)
+    [-OFFSET, 0, Math.PI / 2],  // left
+    [ OFFSET, 0, -Math.PI / 2], // right
+  ];
+
+  walls.forEach(([wx, wz, ry]) => {
+    const wallGeo = new THREE.PlaneGeometry(WALL_LENGTH, WALL_HEIGHT);
+    const wall = new THREE.Mesh(wallGeo, wallMat);
+    wall.position.set(wx, WALL_HEIGHT / 2, wz);
+    wall.rotation.y = ry;
+    scene.add(wall);
+
+    // Warm gold edge trim — architectural base accent, not neon purple
+    const trimMat = new THREE.MeshStandardMaterial({
+      color: 0xa07828,
+      emissive: new THREE.Color(0xb08030),
+      emissiveIntensity: 0.4,
+      metalness: 0.6,
+      roughness: 0.4,
+      transparent: true,
+      opacity: 0.45,
+    });
+    const trimGeo = new THREE.BoxGeometry(WALL_LENGTH, 0.06, 0.08);
+    const trim = new THREE.Mesh(trimGeo, trimMat);
+    trim.position.set(wx, 0.06, wz);
+    trim.rotation.y = ry;
+    scene.add(trim);
+  });
+}
diff --git a/src/scene/Lighting.ts b/src/scene/Lighting.ts
new file mode 100644
index 0000000..f061c68
--- /dev/null
+++ b/src/scene/Lighting.ts
@@ -0,0 +1,60 @@
+// ═══════════════════════════════════════════════
+// DW War Room 3D — Lighting Setup
+// Sci-fi ambient with purple accents
+// ═══════════════════════════════════════════════
+
+import * as THREE from 'three';
+
+export function createLighting(scene: THREE.Scene): void {
+  // Ambient — cool neutral base (lifted from dark purple to architectural neutral)
+  const ambient = new THREE.AmbientLight(0x223344, 0.6);
+  scene.add(ambient);
+
+  // Primary directional — cool white key light with shadows
+  const mainLight = new THREE.DirectionalLight(0xddeeff, 1.2);
+  mainLight.position.set(8, 25, 12);
+  mainLight.castShadow = true;
+  mainLight.shadow.mapSize.width = 2048;
+  mainLight.shadow.mapSize.height = 2048;
+  mainLight.shadow.camera.near = 0.5;
+  mainLight.shadow.camera.far = 80;
+  mainLight.shadow.camera.left = -30;
+  mainLight.shadow.camera.right = 30;
+  mainLight.shadow.camera.top = 30;
+  mainLight.shadow.camera.bottom = -30;
+  mainLight.shadow.bias = -0.0005;
+  scene.add(mainLight);
+
+  // Secondary fill — warm gold from front-right (corporate warmth)
+  const fillLight = new THREE.DirectionalLight(0xffd580, 0.35);
+  fillLight.position.set(15, 10, 15);
+  scene.add(fillLight);
+
+  // Cool white accent from left (clean industrial)
+  const leftAccent = new THREE.PointLight(0xc8d8f0, 0.5, 55);
+  leftAccent.position.set(-15, 8, 0);
+  scene.add(leftAccent);
+
+  // Warm gold accent from right
+  const rightAccent = new THREE.PointLight(0xffa040, 0.3, 55);
+  rightAccent.position.set(15, 8, 0);
+  scene.add(rightAccent);
+
+  // Red warning light (for escalations) — starts dim
+  const redLight = new THREE.PointLight(0xef4444, 0.0, 30);
+  redLight.position.set(0, 12, -10);
+  redLight.name = 'escalation-light';
+  scene.add(redLight);
+
+  // Subtle uplight from floor — bounced ambient warmth
+  const uplift = new THREE.PointLight(0x334455, 0.2, 20);
+  uplift.position.set(0, 0.5, 0);
+  scene.add(uplift);
+}
+
+export function pulseEscalationLight(scene: THREE.Scene, intensity: number): void {
+  const light = scene.getObjectByName('escalation-light') as THREE.PointLight;
+  if (light) {
+    light.intensity = intensity;
+  }
+}
diff --git a/src/scene/WarRoom.ts b/src/scene/WarRoom.ts
new file mode 100644
index 0000000..0386413
--- /dev/null
+++ b/src/scene/WarRoom.ts
@@ -0,0 +1,198 @@
+// ═══════════════════════════════════════════════
+// DW War Room 3D — Main Scene Composition
+// Assembles all scene components + raycasting
+// ═══════════════════════════════════════════════
+
+import * as THREE from 'three';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+import { createLighting, pulseEscalationLight } from './Lighting';
+import { createCommandTable, CommandTableHandle } from './CommandTable';
+import { AgentNodeManager } from './AgentNodes';
+import { DataWalls } from './DataWalls';
+import { PostProcessingPipeline } from '../effects/PostProcessing';
+import { ParticleSystem } from '../effects/Particles';
+import { createEnvironment } from './Environment';
+import type { AgentNode } from '../types';
+
+export class WarRoom {
+  scene: THREE.Scene;
+  camera: THREE.PerspectiveCamera;
+  renderer: THREE.WebGLRenderer;
+  controls: OrbitControls;
+  agentNodes: AgentNodeManager;
+  dataWalls: DataWalls;
+
+  private clock: THREE.Clock;
+  private postProcessing: PostProcessingPipeline;
+  private particles: ParticleSystem;
+  private commandTable: CommandTableHandle;
+  private raycaster: THREE.Raycaster;
+  private mouse: THREE.Vector2;
+
+  // Callback for agent click
+  onAgentClick: ((agent: AgentNode, screenX: number, screenY: number) => void) | null = null;
+
+  constructor(container: HTMLElement) {
+    // Scene
+    this.scene = new THREE.Scene();
+    this.scene.background = new THREE.Color(0x050510);
+    this.scene.fog = new THREE.Fog(0x050510, 25, 80);
+
+    // Camera
+    this.camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 100);
+    this.camera.position.set(0, 12, 18);
+    this.camera.lookAt(0, 2, 0);
+
+    // Renderer — physically-based rendering pipeline
+    this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
+    this.renderer.setSize(container.clientWidth, container.clientHeight);
+    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+    this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
+    this.renderer.toneMappingExposure = 1.2;
+    this.renderer.outputColorSpace = THREE.SRGBColorSpace;
+    this.renderer.useLegacyLights = false;
+    this.renderer.shadowMap.enabled = true;
+    this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
+    container.appendChild(this.renderer.domElement);
+
+    // Controls
+    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
+    this.controls.enableDamping = true;
+    this.controls.dampingFactor = 0.05;
+    this.controls.minDistance = 5;
+    this.controls.maxDistance = 40;
+    this.controls.maxPolarAngle = Math.PI / 2.1;
+
+    // Clock
+    this.clock = new THREE.Clock();
+
+    // Raycasting
+    this.raycaster = new THREE.Raycaster();
+    this.mouse = new THREE.Vector2();
+
+    // Build scene
+    createLighting(this.scene);
+    this.commandTable = createCommandTable(this.scene);
+    this.agentNodes = new AgentNodeManager(this.scene);
+    this.dataWalls = new DataWalls(this.scene);
+    createEnvironment(this.scene);
+    this.addEnvMap();
+    this.addStarfield();
+
+    // Particle system
+    this.particles = new ParticleSystem(this.scene);
+
+    // Post-processing
+    this.postProcessing = new PostProcessingPipeline(this.renderer, this.scene, this.camera);
+
+    // Resize handling
+    window.addEventListener('resize', () => this.onResize(container));
+
+    // Click handler for agent raycasting
+    this.renderer.domElement.addEventListener('click', (e) => this.handleClick(e));
+  }
+
+  private addEnvMap(): void {
+    // Generate a neutral PMREM environment from a simple gradient scene
+    // Gives all MeshStandardMaterial surfaces ambient IBL reflections
+    const pmrem = new THREE.PMREMGenerator(this.renderer);
+    pmrem.compileEquirectangularShader();
+
+    // Build a tiny gradient sky scene to bake into the env map
+    const envScene = new THREE.Scene();
+    // Cool dark-blue top gradient, warm dark base — corporate command center tone
+    const skyGeo = new THREE.SphereGeometry(50, 16, 8);
+    const skyMat = new THREE.MeshBasicMaterial({
+      color: 0x0a1520,
+      side: THREE.BackSide,
+    });
+    envScene.add(new THREE.Mesh(skyGeo, skyMat));
+
+    // Add a soft white highlight for IBL top reflection
+    const capGeo = new THREE.SphereGeometry(40, 8, 4, 0, Math.PI * 2, 0, Math.PI * 0.25);
+    const capMat = new THREE.MeshBasicMaterial({ color: 0x4488aa, side: THREE.BackSide });
+    const cap = new THREE.Mesh(capGeo, capMat);
+    cap.rotation.x = Math.PI;
+    envScene.add(cap);
+
+    const envRenderTarget = pmrem.fromScene(envScene as any, 0.04);
+    this.renderer.environment = envRenderTarget.texture;
+    pmrem.dispose();
+    skyMat.dispose();
+    capMat.dispose();
+  }
+
+  private addStarfield(): void {
+    const starGeo = new THREE.BufferGeometry();
+    const positions = new Float32Array(1500);
+    for (let i = 0; i < 1500; i += 3) {
+      positions[i] = (Math.random() - 0.5) * 80;
+      positions[i + 1] = Math.random() * 30 + 10;
+      positions[i + 2] = (Math.random() - 0.5) * 80;
+    }
+    starGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
+    // Neutral cool-white stars — no neon purple
+    const starMat = new THREE.PointsMaterial({ color: 0xc8ddf0, size: 0.05, transparent: true, opacity: 0.5 });
+    const stars = new THREE.Points(starGeo, starMat);
+    this.scene.add(stars);
+  }
+
+  private handleClick(event: MouseEvent): void {
+    const rect = this.renderer.domElement.getBoundingClientRect();
+    this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
+    this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
+
+    this.raycaster.setFromCamera(this.mouse, this.camera);
+    const meshes = this.agentNodes.getMeshes();
+    const intersects = this.raycaster.intersectObjects(meshes);
+
+    if (intersects.length > 0) {
+      const agent = this.agentNodes.getNodeByMesh(intersects[0].object);
+      if (agent && this.onAgentClick) {
+        this.onAgentClick(agent, event.clientX, event.clientY);
+      }
+    }
+  }
+
+  setEscalationPulse(active: boolean): void {
+    const intensity = active ? 0.8 : 0.0;
+    pulseEscalationLight(this.scene, intensity);
+    this.dataWalls.pulsePanel('escalations', active ? 0.2 : 0.05);
+  }
+
+  animate(): void {
+    requestAnimationFrame(() => this.animate());
+    const elapsed = this.clock.getElapsedTime() * 1000;
+    this.controls.update();
+    this.commandTable.animate(elapsed);
+    this.agentNodes.animate(elapsed);
+    this.dataWalls.animate(elapsed);
+    this.particles.animate(elapsed);
+    this.postProcessing.render(this.scene, this.camera);
+  }
+
+  private onResize(container: HTMLElement): void {
+    const w = container.clientWidth;
+    const h = container.clientHeight;
+    this.camera.aspect = w / h;
+    this.camera.updateProjectionMatrix();
+    this.renderer.setSize(w, h);
+    this.postProcessing.resize(w, h);
+  }
+
+  // Preset camera views
+  viewOverview(): void {
+    this.camera.position.set(0, 12, 18);
+    this.camera.lookAt(0, 2, 0);
+  }
+
+  viewMeetingFocus(): void {
+    this.camera.position.set(0, 8, 10);
+    this.camera.lookAt(0, 3, 0);
+  }
+
+  viewAgentFocus(): void {
+    this.camera.position.set(5, 6, 8);
+    this.camera.lookAt(0, 3, 0);
+  }
+}
diff --git a/src/showroom/FigmaTransformer.ts b/src/showroom/FigmaTransformer.ts
new file mode 100644
index 0000000..174e684
--- /dev/null
+++ b/src/showroom/FigmaTransformer.ts
@@ -0,0 +1,128 @@
+// FigmaTransformer.ts — Converts Figma design context into Three.js scene graph config
+// Maps Figma frames → positions, rectangles → geometry, fills → materials
+import type { SceneConfig, WingConfig, WallConfig, LightConfig, FurnitureConfig } from './types';
+
+/**
+ * Transform Figma design JSON into Three.js scene configuration.
+ * Accepts output from Figma MCP `get_design_context` or raw Figma REST API.
+ */
+export function transformFigmaToScene(figmaData: any): SceneConfig {
+  const root = figmaData.document || figmaData;
+  const config: SceneConfig = {
+    corridor: { width: 40, depth: 20, height: 12, wallMaterial: 'brick', floorMaterial: 'polished-concrete' },
+    wings: [],
+    walls: [],
+    lights: [],
+    furniture: [],
+    entry: { position: [0, 0, 10], signText: 'DESIGNER WALLCOVERINGS' },
+  };
+
+  // Traverse Figma node tree
+  function traverse(node: any, depth = 0) {
+    if (!node) return;
+
+    // Map FRAME nodes to wings or rooms based on name
+    if (node.type === 'FRAME' && node.name?.toLowerCase().includes('wing')) {
+      const bbox = node.absoluteBoundingBox || {};
+      config.wings.push({
+        index: config.wings.length,
+        vendor: node.name.replace(/wing[-_\s]*/i, '').trim(),
+        position: [bbox.x / 10 || config.wings.length * 4, 0, bbox.y / 10 || 0],
+        rotation: 0,
+        panelCount: (node.children || []).filter((c: any) => c.type === 'RECTANGLE').length || 10,
+        accentColor: extractFillColor(node) || '#c0c0c0',
+      });
+    }
+
+    // Map RECTANGLE nodes with IMAGE fills to wall sample panels
+    if (node.type === 'RECTANGLE' && node.fills?.some((f: any) => f.type === 'IMAGE')) {
+      const bbox = node.absoluteBoundingBox || {};
+      config.walls.push({
+        position: [bbox.x / 10 || 0, bbox.height / 20 || 2, bbox.y / 10 || 0],
+        dimensions: [bbox.width / 10 || 4, bbox.height / 10 || 8],
+        material: 'brick',
+        hasBookshelf: false,
+        hasSamplePanels: true,
+        sampleCount: 1,
+      });
+    }
+
+    // Map circles/ellipses to spotlights
+    if (node.type === 'ELLIPSE' && node.name?.toLowerCase().includes('light')) {
+      const bbox = node.absoluteBoundingBox || {};
+      config.lights.push({
+        type: 'spot',
+        position: [bbox.x / 10 || 0, 10, bbox.y / 10 || 0],
+        target: [bbox.x / 10 || 0, 0, bbox.y / 10 || 0],
+        color: extractFillColor(node) || '#ffffff',
+        intensity: 1.5,
+        angle: Math.PI / 6,
+        penumbra: 0.5,
+      });
+    }
+
+    // Map FRAME named 'furniture' or 'table' to furniture
+    if (node.type === 'FRAME' && /table|desk|furniture/i.test(node.name || '')) {
+      const bbox = node.absoluteBoundingBox || {};
+      config.furniture.push({
+        type: 'display-table',
+        position: [bbox.x / 10 || 0, 0.45, bbox.y / 10 || 0],
+        dimensions: [bbox.width / 10 || 3, 0.9, (bbox.height || 150) / 10],
+      });
+    }
+
+    // Map FRAME named 'entry' or 'entrance' to entry
+    if (node.type === 'FRAME' && /entry|entrance|door/i.test(node.name || '')) {
+      const bbox = node.absoluteBoundingBox || {};
+      config.entry.position = [bbox.x / 10 || 0, 0, bbox.y / 10 || 10];
+      // Look for text child as sign text
+      const textChild = (node.children || []).find((c: any) => c.type === 'TEXT');
+      if (textChild?.characters) {
+        config.entry.signText = textChild.characters;
+      }
+    }
+
+    // Map RECTANGLE with SOLID fills named 'bookshelf' or 'shelf'
+    if (node.type === 'RECTANGLE' && /bookshelf|shelf|book/i.test(node.name || '')) {
+      const bbox = node.absoluteBoundingBox || {};
+      config.walls.push({
+        position: [bbox.x / 10 || 0, bbox.height / 20 || 2, bbox.y / 10 || 0],
+        dimensions: [bbox.width / 10 || 4, bbox.height / 10 || 6],
+        material: 'wood',
+        hasBookshelf: true,
+        hasSamplePanels: false,
+        sampleCount: 0,
+      });
+    }
+
+    if (node.children) node.children.forEach((c: any) => traverse(c, depth + 1));
+  }
+
+  traverse(root);
+
+  // Add default track lighting if none extracted
+  if (config.lights.length === 0) {
+    for (let i = 0; i < 8; i++) {
+      config.lights.push({
+        type: 'spot',
+        position: [i * 5 - 17.5, 11, 0],
+        target: [i * 5 - 17.5, 0, 0],
+        color: '#f5e6d0',
+        intensity: 1.2,
+        angle: Math.PI / 5,
+        penumbra: 0.4,
+      });
+    }
+  }
+
+  return config;
+}
+
+function extractFillColor(node: any): string | null {
+  const fill = (node.fills || []).find((f: any) => f.type === 'SOLID' && f.visible !== false);
+  if (!fill?.color) return null;
+  const r = Math.round(fill.color.r * 255);
+  const g = Math.round(fill.color.g * 255);
+  const b = Math.round(fill.color.b * 255);
+  return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
+}
diff --git a/src/showroom/ProceduralMaps.ts b/src/showroom/ProceduralMaps.ts
new file mode 100644
index 0000000..ec475bc
--- /dev/null
+++ b/src/showroom/ProceduralMaps.ts
@@ -0,0 +1,188 @@
+// ProceduralMaps.ts — Canvas-based procedural normal and roughness maps
+// Zero external file dependencies — all generated at runtime
+import * as THREE from 'three';
+
+/**
+ * Generate a brick wall normal map using canvas.
+ * Creates realistic mortar lines and brick surface variation.
+ */
+export function generateBrickNormalMap(width = 1024, height = 1024): THREE.CanvasTexture {
+  const canvas = document.createElement('canvas');
+  canvas.width = width;
+  canvas.height = height;
+  const ctx = canvas.getContext('2d');
+  if (!ctx) throw new Error('Failed to create 2d canvas context');
+
+  // Base neutral normal (pointing straight out: RGB 128,128,255)
+  ctx.fillStyle = 'rgb(128, 128, 255)';
+  ctx.fillRect(0, 0, width, height);
+
+  const brickW = width / 8;
+  const brickH = height / 16;
+  const mortarW = 3;
+
+  for (let row = 0; row < 16; row++) {
+    const offset = (row % 2) * (brickW / 2); // stagger rows
+    for (let col = -1; col < 9; col++) {
+      const x = col * brickW + offset;
+      const y = row * brickH;
+
+      // Mortar lines (recessed = normal pointing inward)
+      // Horizontal mortar
+      ctx.fillStyle = 'rgb(128, 100, 255)'; // slight downward normal
+      ctx.fillRect(0, y, width, mortarW);
+
+      // Vertical mortar
+      ctx.fillStyle = 'rgb(100, 128, 255)'; // slight leftward normal
+      ctx.fillRect(x, y, mortarW, brickH);
+
+      // Brick surface noise (subtle variation)
+      for (let i = 0; i < 20; i++) {
+        const nx = x + Math.random() * brickW;
+        const ny = y + mortarW + Math.random() * (brickH - mortarW * 2);
+        const r = 128 + (Math.random() - 0.5) * 12;
+        const g = 128 + (Math.random() - 0.5) * 12;
+        ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
+        ctx.fillRect(nx, ny, 3 + Math.random() * 6, 2 + Math.random() * 4);
+      }
+    }
+  }
+
+  const tex = new THREE.CanvasTexture(canvas);
+  tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
+  tex.repeat.set(2, 2);
+  return tex;
+}
+
+/**
+ * Generate a polished concrete normal map.
+ * Subtle micro-texture with occasional aggregate patches.
+ */
+export function generateConcreteNormalMap(width = 1024, height = 1024): THREE.CanvasTexture {
+  const canvas = document.createElement('canvas');
+  canvas.width = width;
+  canvas.height = height;
+  const ctx = canvas.getContext('2d');
+  if (!ctx) throw new Error('Failed to create 2d canvas context');
+
+  // Base neutral normal
+  ctx.fillStyle = 'rgb(128, 128, 255)';
+  ctx.fillRect(0, 0, width, height);
+
+  // Micro-texture noise — polished concrete has subtle surface variation
+  for (let i = 0; i < 5000; i++) {
+    const x = Math.random() * width;
+    const y = Math.random() * height;
+    const r = 128 + (Math.random() - 0.5) * 6;
+    const g = 128 + (Math.random() - 0.5) * 6;
+    ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
+    ctx.fillRect(x, y, 1 + Math.random() * 3, 1 + Math.random() * 3);
+  }
+
+  // Aggregate patches (larger bumps)
+  for (let i = 0; i < 80; i++) {
+    const x = Math.random() * width;
+    const y = Math.random() * height;
+    const radius = 4 + Math.random() * 12;
+    const r = 128 + (Math.random() - 0.5) * 18;
+    const g = 128 + (Math.random() - 0.5) * 18;
+    ctx.beginPath();
+    ctx.arc(x, y, radius, 0, Math.PI * 2);
+    ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
+    ctx.fill();
+  }
+
+  const tex = new THREE.CanvasTexture(canvas);
+  tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
+  tex.repeat.set(3, 3);
+  return tex;
+}
+
+/**
+ * Generate a brushed aluminum normal map.
+ * Directional brushing lines for metallic surfaces.
+ */
+export function generateBrushedAluminumNormalMap(width = 512, height = 512): THREE.CanvasTexture {
+  const canvas = document.createElement('canvas');
+  canvas.width = width;
+  canvas.height = height;
+  const ctx = canvas.getContext('2d');
+  if (!ctx) throw new Error('Failed to create 2d canvas context');
+
+  ctx.fillStyle = 'rgb(128, 128, 255)';
+  ctx.fillRect(0, 0, width, height);
+
+  // Horizontal brush strokes
+  for (let y = 0; y < height; y++) {
+    const r = 128 + (Math.random() - 0.5) * 4;
+    const g = 128 + Math.sin(y * 0.3) * 3 + (Math.random() - 0.5) * 2;
+    ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
+    ctx.fillRect(0, y, width, 1);
+  }
+
+  const tex = new THREE.CanvasTexture(canvas);
+  tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
+  return tex;
+}
+
+/**
+ * Generate a wood grain normal map for shelving.
+ */
+export function generateWoodNormalMap(width = 512, height = 512): THREE.CanvasTexture {
+  const canvas = document.createElement('canvas');
+  canvas.width = width;
+  canvas.height = height;
+  const ctx = canvas.getContext('2d');
+  if (!ctx) throw new Error('Failed to create 2d canvas context');
+
+  ctx.fillStyle = 'rgb(128, 128, 255)';
+  ctx.fillRect(0, 0, width, height);
+
+  // Wood grain lines (vertical, slightly wavy)
+  for (let x = 0; x < width; x += 4 + Math.random() * 8) {
+    ctx.beginPath();
+    ctx.moveTo(x, 0);
+    for (let y = 0; y < height; y += 10) {
+      ctx.lineTo(x + Math.sin(y * 0.02 + x * 0.1) * 3, y);
+    }
+    const r = 128 + (Math.random() - 0.5) * 10;
+    ctx.strokeStyle = `rgb(${r}, 128, 255)`;
+    ctx.lineWidth = 1 + Math.random() * 2;
+    ctx.stroke();
+  }
+
+  const tex = new THREE.CanvasTexture(canvas);
+  tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
+  return tex;
+}
+
+/**
+ * Generate a fabric/paper subtle normal map for wallcovering product panels.
+ * Very subtle — just enough to catch light at grazing angles.
+ */
+export function generateFabricNormalMap(width = 256, height = 256): THREE.CanvasTexture {
+  const canvas = document.createElement('canvas');
+  canvas.width = width;
+  canvas.height = height;
+  const ctx = canvas.getContext('2d');
+  if (!ctx) throw new Error('Failed to create 2d canvas context');
+
+  ctx.fillStyle = 'rgb(128, 128, 255)';
+  ctx.fillRect(0, 0, width, height);
+
+  // Woven texture: alternating horizontal and vertical micro-lines
+  for (let y = 0; y < height; y += 2) {
+    for (let x = 0; x < width; x += 2) {
+      const horizontal = (y / 2) % 2 === 0;
+      const r = 128 + (horizontal ? 2 : -2) + (Math.random() - 0.5) * 3;
+      const g = 128 + (horizontal ? -2 : 2) + (Math.random() - 0.5) * 3;
+      ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
+      ctx.fillRect(x, y, 2, 2);
+    }
+  }
+
+  const tex = new THREE.CanvasTexture(canvas);
+  tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
+  tex.repeat.set(4, 4);
+  return tex;
+}
diff --git a/src/showroom/SearchOverlay.ts b/src/showroom/SearchOverlay.ts
new file mode 100644
index 0000000..d5cda80
--- /dev/null
+++ b/src/showroom/SearchOverlay.ts
@@ -0,0 +1,732 @@
+// SearchOverlay.ts — Live Shopify product search overlay for the 3D showroom
+// Debounced search with glassmorphism dropdown matching the DW design system
+// Uses a server-side proxy at /api/shopify/search to keep credentials safe
+
+import * as THREE from 'three';
+import type { Showroom } from './Showroom';
+import { cropTexture } from './TextureCrop';
+
+interface ShopifyProduct {
+  id: number;
+  title: string;
+  vendor: string;
+  product_type: string;
+  handle: string;
+  images: { src: string }[];
+  variants: { price: string; sku: string }[];
+  tags: string;
+}
+
+interface SearchResult {
+  id: number;
+  title: string;
+  vendor: string;
+  productType: string;
+  handle: string;
+  imageUrl: string;
+  price: string;
+  sku: string;
+}
+
+export class SearchOverlay {
+  private container: HTMLElement;
+  private inputEl!: HTMLInputElement;
+  private resultsEl!: HTMLElement;
+  private wrapperEl!: HTMLElement;
+  private debounceTimer: ReturnType<typeof setTimeout> | null = null;
+  private isOpen = false;
+  private results: SearchResult[] = [];
+  private selectedIndex = -1;
+  private showroom: Showroom | null = null;
+  private loadingEl!: HTMLElement;
+  private emptyEl!: HTMLElement;
+
+  constructor(parent: HTMLElement) {
+    this.container = parent;
+    this.buildDom();
+    this.applyStyles();
+    this.bindEvents();
+  }
+
+  /** Optionally wire to a Showroom instance for wall display */
+  setShowroom(showroom: Showroom): void {
+    this.showroom = showroom;
+  }
+
+  private buildDom(): void {
+    // Wrapper — top-center overlay
+    this.wrapperEl = document.createElement('div');
+    this.wrapperEl.id = 'search-overlay';
+    this.wrapperEl.className = 'search-overlay';
+
+    // Search input group
+    const inputGroup = document.createElement('div');
+    inputGroup.className = 'search-input-group';
+
+    // Search icon
+    const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+    iconSvg.setAttribute('width', '16');
+    iconSvg.setAttribute('height', '16');
+    iconSvg.setAttribute('viewBox', '0 0 24 24');
+    iconSvg.setAttribute('fill', 'none');
+    iconSvg.setAttribute('stroke', 'currentColor');
+    iconSvg.setAttribute('stroke-width', '2');
+    iconSvg.setAttribute('class', 'search-icon');
+    const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+    circle.setAttribute('cx', '11');
+    circle.setAttribute('cy', '11');
+    circle.setAttribute('r', '8');
+    iconSvg.appendChild(circle);
+    const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+    line.setAttribute('x1', '21');
+    line.setAttribute('y1', '21');
+    line.setAttribute('x2', '16.65');
+    line.setAttribute('y2', '16.65');
+    iconSvg.appendChild(line);
+    inputGroup.appendChild(iconSvg);
+
+    // Input
+    this.inputEl = document.createElement('input');
+    this.inputEl.type = 'text';
+    this.inputEl.className = 'search-input';
+    this.inputEl.placeholder = 'Search products...';
+    this.inputEl.autocomplete = 'off';
+    this.inputEl.spellcheck = false;
+    inputGroup.appendChild(this.inputEl);
+
+    // Keyboard shortcut hint
+    const shortcut = document.createElement('span');
+    shortcut.className = 'search-shortcut';
+    shortcut.textContent = '/';
+    inputGroup.appendChild(shortcut);
+
+    this.wrapperEl.appendChild(inputGroup);
+
+    // Results dropdown
+    this.resultsEl = document.createElement('div');
+    this.resultsEl.className = 'search-results';
+    this.resultsEl.style.display = 'none';
+
+    // Loading indicator
+    this.loadingEl = document.createElement('div');
+    this.loadingEl.className = 'search-loading';
+    this.loadingEl.textContent = 'Searching...';
+    this.loadingEl.style.display = 'none';
+    this.resultsEl.appendChild(this.loadingEl);
+
+    // Empty state
+    this.emptyEl = document.createElement('div');
+    this.emptyEl.className = 'search-empty';
+    this.emptyEl.textContent = 'No products found';
+    this.emptyEl.style.display = 'none';
+    this.resultsEl.appendChild(this.emptyEl);
+
+    this.wrapperEl.appendChild(this.resultsEl);
+    this.container.appendChild(this.wrapperEl);
+  }
+
+  private bindEvents(): void {
+    // Input events
+    this.inputEl.addEventListener('input', () => this.onInputChange());
+    this.inputEl.addEventListener('focus', () => this.onFocus());
+    this.inputEl.addEventListener('keydown', (e) => this.onKeyDown(e));
+
+    // Close on outside click
+    document.addEventListener('click', (e) => {
+      if (!this.wrapperEl.contains(e.target as Node)) {
+        this.closeResults();
+      }
+    });
+
+    // Keyboard shortcut: "/" to focus search
+    document.addEventListener('keydown', (e) => {
+      if (e.key === '/' && document.activeElement !== this.inputEl) {
+        e.preventDefault();
+        this.inputEl.focus();
+      }
+      if (e.key === 'Escape' && document.activeElement === this.inputEl) {
+        this.inputEl.blur();
+        this.closeResults();
+      }
+    });
+  }
+
+  private onInputChange(): void {
+    const query = this.inputEl.value.trim();
+
+    if (this.debounceTimer) clearTimeout(this.debounceTimer);
+
+    if (query.length < 2) {
+      this.closeResults();
+      return;
+    }
+
+    this.showLoading();
+
+    this.debounceTimer = setTimeout(() => {
+      this.searchShopify(query);
+    }, 300);
+  }
+
+  private onFocus(): void {
+    if (this.results.length > 0) {
+      this.openResults();
+    }
+  }
+
+  private onKeyDown(e: KeyboardEvent): void {
+    if (!this.isOpen) return;
+
+    switch (e.key) {
+      case 'ArrowDown':
+        e.preventDefault();
+        this.selectedIndex = Math.min(this.selectedIndex + 1, this.results.length - 1);
+        this.highlightResult();
+        break;
+      case 'ArrowUp':
+        e.preventDefault();
+        this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
+        this.highlightResult();
+        break;
+      case 'Enter':
+        e.preventDefault();
+        if (this.selectedIndex >= 0 && this.selectedIndex < this.results.length) {
+          this.selectResult(this.results[this.selectedIndex]);
+        }
+        break;
+      case 'Escape':
+        this.closeResults();
+        break;
+    }
+  }
+
+  private async searchShopify(query: string): Promise<void> {
+    try {
+      const res = await fetch(`/api/shopify/search?q=${encodeURIComponent(query)}&limit=10`);
+      if (!res.ok) throw new Error(`${res.status}`);
+      const data = await res.json();
+
+      const products: ShopifyProduct[] = data.products || [];
+      this.results = products.map(p => ({
+        id: p.id,
+        title: p.title,
+        vendor: p.vendor,
+        productType: p.product_type,
+        handle: p.handle,
+        imageUrl: p.images?.[0]?.src || '',
+        price: p.variants?.[0]?.price || '0.00',
+        sku: p.variants?.[0]?.sku || '',
+      }));
+
+      this.renderResults();
+    } catch (err) {
+      console.error('[SearchOverlay] Search failed:', err);
+      this.results = [];
+      this.showEmpty('Search unavailable');
+    }
+  }
+
+  private renderResults(): void {
+    // Clear previous result items (keep loading/empty elements)
+    const items = this.resultsEl.querySelectorAll('.search-result-item');
+    items.forEach(item => item.remove());
+
+    this.loadingEl.style.display = 'none';
+
+    if (this.results.length === 0) {
+      this.showEmpty('No products found');
+      return;
+    }
+
+    this.emptyEl.style.display = 'none';
+    this.selectedIndex = -1;
+
+    this.results.forEach((result, index) => {
+      const item = document.createElement('div');
+      item.className = 'search-result-item';
+      item.dataset.index = String(index);
+
+      // Product image
+      const imgWrap = document.createElement('div');
+      imgWrap.className = 'search-result-image';
+      if (result.imageUrl) {
+        const img = document.createElement('img');
+        // Use Shopify CDN resize for small thumbnails
+        img.src = result.imageUrl.replace(/\.([^.]+)$/, '_80x80.$1');
+        img.alt = result.title;
+        img.loading = 'lazy';
+        img.onerror = () => {
+          img.style.display = 'none';
+          imgWrap.classList.add('no-image');
+        };
+        imgWrap.appendChild(img);
+      } else {
+        imgWrap.classList.add('no-image');
+      }
+      item.appendChild(imgWrap);
+
+      // Product details
+      const details = document.createElement('div');
+      details.className = 'search-result-details';
+
+      const titleEl = document.createElement('div');
+      titleEl.className = 'search-result-title';
+      titleEl.textContent = result.title;
+      details.appendChild(titleEl);
+
+      const metaEl = document.createElement('div');
+      metaEl.className = 'search-result-meta';
+
+      const vendorSpan = document.createElement('span');
+      vendorSpan.className = 'search-result-vendor';
+      vendorSpan.textContent = result.vendor;
+      metaEl.appendChild(vendorSpan);
+
+      if (result.productType) {
+        const dot = document.createElement('span');
+        dot.className = 'search-result-dot';
+        dot.textContent = ' \u00b7 ';
+        metaEl.appendChild(dot);
+        const typeSpan = document.createElement('span');
+        typeSpan.textContent = result.productType;
+        metaEl.appendChild(typeSpan);
+      }
+
+      details.appendChild(metaEl);
+      item.appendChild(details);
+
+      // Price
+      const priceEl = document.createElement('div');
+      priceEl.className = 'search-result-price';
+      const priceVal = parseFloat(result.price);
+      priceEl.textContent = priceVal > 0 ? `$${priceVal.toFixed(2)}` : '';
+      item.appendChild(priceEl);
+
+      // Events
+      item.addEventListener('click', () => this.selectResult(result));
+      item.addEventListener('mouseenter', () => {
+        this.selectedIndex = index;
+        this.highlightResult();
+      });
+
+      this.resultsEl.appendChild(item);
+    });
+
+    this.openResults();
+  }
+
+  private highlightResult(): void {
+    const items = this.resultsEl.querySelectorAll('.search-result-item');
+    items.forEach((item, i) => {
+      (item as HTMLElement).classList.toggle('highlighted', i === this.selectedIndex);
+    });
+
+    // Scroll highlighted item into view
+    if (this.selectedIndex >= 0) {
+      const highlighted = items[this.selectedIndex] as HTMLElement;
+      if (highlighted) {
+        highlighted.scrollIntoView({ block: 'nearest' });
+      }
+    }
+  }
+
+  private selectResult(result: SearchResult): void {
+    console.log('[SearchOverlay] Selected product:', result.title, result.handle);
+    this.closeResults();
+    this.inputEl.value = '';
+
+    // Try to apply texture to a wall in the showroom
+    if (result.imageUrl && this.showroom) {
+      this.applyToShowroomWall(result);
+    } else {
+      // Fallback: open product on Shopify store
+      const url = `https://designer-laboratory-sandbox.myshopify.com/products/${result.handle}`;
+      window.open(url, '_blank');
+    }
+  }
+
+  private applyToShowroomWall(result: SearchResult): void {
+    if (!this.showroom) return;
+
+    this.showToast(`Loading "${result.title}" onto wall...`);
+
+    // Use higher resolution source, then crop to 512x768 (no stretching)
+    const textureUrl = result.imageUrl.replace(/\.([^.]+)$/, '_600x600.$1');
+
+    cropTexture(textureUrl)
+      .then((texture) => {
+        const scene = this.showroom!.scene;
+        let applied = false;
+
+        scene.traverse((child) => {
+          if (applied) return;
+          if (child instanceof THREE.Mesh && child.name === 'search-preview-wall') {
+            const mat = child.material as THREE.MeshStandardMaterial;
+            if (mat.map) mat.map.dispose();
+            mat.map = texture;
+            mat.needsUpdate = true;
+            applied = true;
+          }
+        });
+
+        if (!applied) {
+          const camera = this.showroom!.camera;
+          const dir = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
+          const wallPos = camera.position.clone().add(dir.multiplyScalar(5));
+          wallPos.y = 2.5;
+
+          // PlaneGeometry matches the 2:3 crop aspect (3 wide x 4.5 tall)
+          const geo = new THREE.PlaneGeometry(3, 4.5);
+          const mat = new THREE.MeshStandardMaterial({
+            map: texture,
+            roughness: 0.4,
+            metalness: 0.0,
+            envMapIntensity: 0.5,
+          });
+          const mesh = new THREE.Mesh(geo, mat);
+          mesh.name = 'search-preview-wall';
+          mesh.position.copy(wallPos);
+          mesh.lookAt(camera.position);
+          mesh.castShadow = true;
+          mesh.receiveShadow = true;
+          scene.add(mesh);
+
+          // Frame matches the 2:3 plane
+          const frameGeo = new THREE.BoxGeometry(3.15, 4.65, 0.05);
+          const frameMat = new THREE.MeshStandardMaterial({
+            color: 0x333333,
+            roughness: 0.3,
+            metalness: 0.4,
+          });
+          const frame = new THREE.Mesh(frameGeo, frameMat);
+          frame.name = 'search-preview-frame';
+          frame.position.copy(wallPos);
+          frame.position.z -= 0.03;
+          frame.lookAt(camera.position);
+          scene.add(frame);
+
+          // Label below
+          const labelCanvas = document.createElement('canvas');
+          labelCanvas.width = 512;
+          labelCanvas.height = 64;
+          const ctx = labelCanvas.getContext('2d');
+          if (ctx) {
+            ctx.fillStyle = '#1a1a1a';
+            ctx.fillRect(0, 0, 512, 64);
+            ctx.fillStyle = '#f0ebe4';
+            ctx.font = '24px Inter, -apple-system, sans-serif';
+            ctx.textAlign = 'center';
+            ctx.fillText(result.title.substring(0, 40), 256, 28);
+            ctx.font = '14px Inter, -apple-system, sans-serif';
+            ctx.fillStyle = '#888888';
+            ctx.fillText(result.vendor, 256, 52);
+          }
+          const labelTex = new THREE.CanvasTexture(labelCanvas);
+          const labelGeo = new THREE.PlaneGeometry(2.5, 0.3);
+          const labelMat = new THREE.MeshBasicMaterial({ map: labelTex });
+          const label = new THREE.Mesh(labelGeo, labelMat);
+          label.name = 'search-preview-label';
+          label.position.copy(wallPos);
+          label.position.y -= 2.55;
+          label.lookAt(camera.position);
+          scene.add(label);
+        }
+
+        this.showToast(`"${result.title}" displayed on wall`);
+      })
+      .catch((err) => {
+        console.error('[SearchOverlay] Failed to crop texture:', err);
+        const url = `https://designer-laboratory-sandbox.myshopify.com/products/${result.handle}`;
+        window.open(url, '_blank');
+      });
+  }
+
+  private showToast(message: string): void {
+    // Remove existing toast
+    const existing = document.getElementById('search-toast');
+    if (existing) existing.remove();
+
+    const toast = document.createElement('div');
+    toast.id = 'search-toast';
+    toast.className = 'search-toast';
+    toast.textContent = message;
+    this.container.appendChild(toast);
+
+    // Auto-remove after 3 seconds
+    setTimeout(() => {
+      toast.classList.add('fade-out');
+      setTimeout(() => toast.remove(), 300);
+    }, 3000);
+  }
+
+  private showLoading(): void {
+    this.resultsEl.style.display = 'block';
+    this.isOpen = true;
+    this.loadingEl.style.display = 'block';
+    this.emptyEl.style.display = 'none';
+    // Remove existing result items
+    const items = this.resultsEl.querySelectorAll('.search-result-item');
+    items.forEach(item => item.remove());
+  }
+
+  private showEmpty(message: string): void {
+    this.resultsEl.style.display = 'block';
+    this.isOpen = true;
+    this.loadingEl.style.display = 'none';
+    this.emptyEl.style.display = 'block';
+    this.emptyEl.textContent = message;
+  }
+
+  private openResults(): void {
+    this.resultsEl.style.display = 'block';
+    this.isOpen = true;
+  }
+
+  private closeResults(): void {
+    this.resultsEl.style.display = 'none';
+    this.isOpen = false;
+    this.selectedIndex = -1;
+  }
+
+  private applyStyles(): void {
+    const style = document.createElement('style');
+    style.textContent = `
+      /* ===== Search Overlay ===== */
+      .search-overlay {
+        position: fixed;
+        top: 16px;
+        left: 50%;
+        transform: translateX(-50%);
+        z-index: 100;
+        width: 420px;
+        max-width: calc(100vw - 32px);
+        font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
+      }
+
+      /* ===== Input Group ===== */
+      .search-input-group {
+        position: relative;
+        display: flex;
+        align-items: center;
+        background: rgba(8, 11, 22, 0.88);
+        backdrop-filter: blur(16px);
+        -webkit-backdrop-filter: blur(16px);
+        border: 1px solid rgba(148, 163, 184, 0.12);
+        border-radius: 10px;
+        padding: 0 14px;
+        transition: border-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
+                    box-shadow 150ms cubic-bezier(0.4, 0, 0.2, 1);
+      }
+      .search-input-group:focus-within {
+        border-color: rgba(99, 102, 241, 0.4);
+        box-shadow: 0 4px 16px rgba(0,0,0,0.4), 0 0 20px rgba(99, 102, 241, 0.1);
+      }
+
+      .search-icon {
+        color: #64748b;
+        flex-shrink: 0;
+        margin-right: 10px;
+        transition: color 150ms;
+      }
+      .search-input-group:focus-within .search-icon {
+        color: #8b5cf6;
+      }
+
+      .search-input {
+        flex: 1;
+        background: transparent;
+        border: none;
+        outline: none;
+        color: #f1f5f9;
+        font-size: 14px;
+        font-family: inherit;
+        font-weight: 500;
+        padding: 12px 0;
+        letter-spacing: 0.2px;
+      }
+      .search-input::placeholder {
+        color: #64748b;
+        font-weight: 400;
+      }
+
+      .search-shortcut {
+        flex-shrink: 0;
+        display: inline-flex;
+        align-items: center;
+        justify-content: center;
+        width: 22px;
+        height: 22px;
+        border-radius: 4px;
+        background: rgba(255, 255, 255, 0.04);
+        border: 1px solid rgba(148, 163, 184, 0.12);
+        color: #64748b;
+        font-size: 11px;
+        font-weight: 600;
+        font-family: monospace;
+        margin-left: 8px;
+      }
+      .search-input-group:focus-within .search-shortcut {
+        display: none;
+      }
+
+      /* ===== Results Dropdown ===== */
+      .search-results {
+        margin-top: 6px;
+        background: rgba(8, 11, 22, 0.95);
+        backdrop-filter: blur(24px);
+        -webkit-backdrop-filter: blur(24px);
+        border: 1px solid rgba(148, 163, 184, 0.1);
+        border-radius: 12px;
+        max-height: 420px;
+        overflow-y: auto;
+        box-shadow: 0 8px 32px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.02) inset;
+      }
+      .search-results::-webkit-scrollbar { width: 4px; }
+      .search-results::-webkit-scrollbar-track { background: transparent; }
+      .search-results::-webkit-scrollbar-thumb { background: rgba(148,163,184,0.15); border-radius: 2px; }
+
+      /* ===== Result Item ===== */
+      .search-result-item {
+        display: flex;
+        align-items: center;
+        gap: 12px;
+        padding: 10px 14px;
+        cursor: pointer;
+        border-bottom: 1px solid rgba(148, 163, 184, 0.06);
+        transition: background 100ms;
+      }
+      .search-result-item:last-child {
+        border-bottom: none;
+      }
+      .search-result-item:hover,
+      .search-result-item.highlighted {
+        background: rgba(99, 102, 241, 0.06);
+      }
+
+      /* ===== Product Image ===== */
+      .search-result-image {
+        width: 48px;
+        height: 48px;
+        border-radius: 6px;
+        overflow: hidden;
+        flex-shrink: 0;
+        background: rgba(255, 255, 255, 0.03);
+        border: 1px solid rgba(148, 163, 184, 0.08);
+        display: flex;
+        align-items: center;
+        justify-content: center;
+      }
+      .search-result-image img {
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+      }
+      .search-result-image.no-image::after {
+        content: '';
+        width: 20px;
+        height: 20px;
+        background: rgba(148, 163, 184, 0.15);
+        border-radius: 3px;
+      }
+
+      /* ===== Product Details ===== */
+      .search-result-details {
+        flex: 1;
+        min-width: 0;
+      }
+      .search-result-title {
+        font-size: 13px;
+        font-weight: 600;
+        color: #f1f5f9;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        line-height: 1.3;
+      }
+      .search-result-meta {
+        font-size: 11px;
+        color: #64748b;
+        margin-top: 2px;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
+      .search-result-vendor {
+        color: #8b5cf6;
+        font-weight: 500;
+      }
+      .search-result-dot {
+        color: #475569;
+      }
+
+      /* ===== Price ===== */
+      .search-result-price {
+        flex-shrink: 0;
+        font-size: 13px;
+        font-weight: 700;
+        color: #06b6d4;
+        font-variant-numeric: tabular-nums;
+      }
+
+      /* ===== Loading / Empty ===== */
+      .search-loading,
+      .search-empty {
+        padding: 20px 14px;
+        text-align: center;
+        color: #64748b;
+        font-size: 13px;
+      }
+      .search-loading::before {
+        content: '';
+        display: inline-block;
+        width: 14px;
+        height: 14px;
+        border: 2px solid rgba(148, 163, 184, 0.15);
+        border-top-color: #8b5cf6;
+        border-radius: 50%;
+        animation: searchSpin 0.6s linear infinite;
+        margin-right: 8px;
+        vertical-align: middle;
+      }
+      @keyframes searchSpin {
+        to { transform: rotate(360deg); }
+      }
+
+      /* ===== Toast Notification ===== */
+      .search-toast {
+        position: fixed;
+        bottom: 80px;
+        left: 50%;
+        transform: translateX(-50%);
+        z-index: 200;
+        background: rgba(8, 11, 22, 0.92);
+        backdrop-filter: blur(16px);
+        -webkit-backdrop-filter: blur(16px);
+        border: 1px solid rgba(99, 102, 241, 0.2);
+        border-radius: 8px;
+        padding: 10px 20px;
+        color: #f1f5f9;
+        font-size: 13px;
+        font-family: 'Inter', -apple-system, sans-serif;
+        font-weight: 500;
+        box-shadow: 0 8px 32px rgba(0,0,0,0.4);
+        animation: toastIn 250ms cubic-bezier(0.4, 0, 0.2, 1);
+      }
+      .search-toast.fade-out {
+        opacity: 0;
+        transform: translateX(-50%) translateY(10px);
+        transition: opacity 300ms, transform 300ms;
+      }
+      @keyframes toastIn {
+        from { opacity: 0; transform: translateX(-50%) translateY(10px); }
+        to { opacity: 1; transform: translateX(-50%) translateY(0); }
+      }
+    `;
+    document.head.appendChild(style);
+  }
+
+  dispose(): void {
+    if (this.debounceTimer) clearTimeout(this.debounceTimer);
+    this.wrapperEl.remove();
+  }
+}
diff --git a/src/showroom/Showroom.ts b/src/showroom/Showroom.ts
new file mode 100644
index 0000000..7764082
--- /dev/null
+++ b/src/showroom/Showroom.ts
@@ -0,0 +1,206 @@
+// Showroom.ts — Main photorealistic showroom compositor
+// Ties together: FigmaTransformer + Materials + Environment + Layout + PostProcess
+import * as THREE from 'three';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+import { createMaterialLibrary, disposeMaterialLibrary } from './ShowroomMaterials';
+import { createShowroomEnvMap } from './ShowroomEnvironment';
+import { ShowroomLayout } from './ShowroomLayout';
+import { ShowroomPostProcess } from './ShowroomPostProcess';
+import { transformFigmaToScene } from './FigmaTransformer';
+import type { SceneConfig } from './types';
+import type { MaterialLibrary } from './ShowroomMaterials';
+
+export class Showroom {
+  scene: THREE.Scene;
+  camera: THREE.PerspectiveCamera;
+  renderer: THREE.WebGLRenderer;
+  controls: OrbitControls;
+
+  private clock: THREE.Clock;
+  private _elapsed = 0;
+  private _running = false;
+  private _resizeHandler: (() => void) | null = null;
+  private materials: MaterialLibrary;
+  private layout: ShowroomLayout;
+  private postProcess: ShowroomPostProcess;
+  private dustParticles: THREE.Points;
+
+  constructor(container: HTMLElement) {
+    // Scene
+    this.scene = new THREE.Scene();
+    this.scene.background = new THREE.Color(0x0d0c0a);
+    this.scene.fog = new THREE.Fog(0x0d0c0a, 20, 60);
+
+    // Camera — human eye level
+    this.camera = new THREE.PerspectiveCamera(
+      55, container.clientWidth / container.clientHeight, 0.1, 100
+    );
+    this.camera.position.set(0, 1.7, 8);
+    this.camera.lookAt(0, 1.5, 0);
+
+    // Renderer — photorealistic pipeline
+    this.renderer = new THREE.WebGLRenderer({ antialias: true });
+    this.renderer.setSize(container.clientWidth, container.clientHeight);
+    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+    this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
+    this.renderer.toneMappingExposure = 1.4;
+    this.renderer.outputColorSpace = THREE.SRGBColorSpace;
+    this.renderer.shadowMap.enabled = true;
+    this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
+    container.appendChild(this.renderer.domElement);
+
+    // Controls — walking speed
+    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
+    this.controls.enableDamping = true;
+    this.controls.dampingFactor = 0.08;
+    this.controls.maxPolarAngle = Math.PI / 2;
+    this.controls.minDistance = 1;
+    this.controls.maxDistance = 50;
+
+    // Clock
+    this.clock = new THREE.Clock();
+
+    // Materials
+    this.materials = createMaterialLibrary();
+
+    // Environment map (warm gallery IBL)
+    this.scene.environment = createShowroomEnvMap(this.renderer);
+
+    // Ambient + key lighting
+    this.addBaseLighting();
+
+    // Layout builder
+    this.layout = new ShowroomLayout(this.scene, this.materials);
+
+    // Atmospheric dust particles
+    this.dustParticles = this.createDustMotes();
+    this.scene.add(this.dustParticles);
+
+    // Post-processing
+    this.postProcess = new ShowroomPostProcess(this.renderer, this.scene, this.camera);
+
+    // Resize — store reference for cleanup
+    this._resizeHandler = () => this.onResize(container);
+    window.addEventListener('resize', this._resizeHandler);
+  }
+
+  /**
+   * Load showroom from Figma design data.
+   */
+  loadFromFigma(figmaData: any): void {
+    const config = transformFigmaToScene(figmaData);
+    this.layout.buildFromConfig(config);
+  }
+
+  /**
+   * Load showroom from a pre-built SceneConfig.
+   */
+  loadFromConfig(config: SceneConfig): void {
+    this.layout.buildFromConfig(config);
+  }
+
+  private addBaseLighting(): void {
+    // Ambient — warm neutral
+    const ambient = new THREE.AmbientLight(0x2a2520, 0.4);
+    this.scene.add(ambient);
+
+    // Hemisphere — sky/ground color contrast
+    const hemi = new THREE.HemisphereLight(0xf5e6d0, 0x1a1510, 0.3);
+    this.scene.add(hemi);
+
+    // Main key light — warm directional
+    const key = new THREE.DirectionalLight(0xf5e6d0, 0.8);
+    key.position.set(5, 15, 10);
+    key.castShadow = true;
+    key.shadow.mapSize.set(2048, 2048);
+    key.shadow.camera.near = 0.5;
+    key.shadow.camera.far = 50;
+    key.shadow.camera.left = -25;
+    key.shadow.camera.right = 25;
+    key.shadow.camera.top = 15;
+    key.shadow.camera.bottom = -15;
+    key.shadow.bias = -0.0003;
+    this.scene.add(key);
+  }
+
+  private createDustMotes(): THREE.Points {
+    const count = 200;
+    const positions = new Float32Array(count * 3);
+    for (let i = 0; i < count * 3; i += 3) {
+      positions[i] = (Math.random() - 0.5) * 40;
+      positions[i + 1] = Math.random() * 12;
+      positions[i + 2] = (Math.random() - 0.5) * 20;
+    }
+    const geo = new THREE.BufferGeometry();
+    geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
+    const mat = new THREE.PointsMaterial({
+      color: 0xf5e6d0,
+      size: 0.02,
+      transparent: true,
+      opacity: 0.3,
+      blending: THREE.AdditiveBlending,
+      depthWrite: false,
+    });
+    return new THREE.Points(geo, mat);
+  }
+
+  animate(): void {
+    if (!this._running) return;
+    requestAnimationFrame(() => this.animate());
+    // Use only getDelta() — getElapsedTime() internally calls getDelta() again,
+    // which double-advances the clock and yields a near-zero second delta
+    const dt = Math.min(this.clock.getDelta(), 0.1);
+    this._elapsed += dt;
+    const elapsed = this._elapsed;
+
+    this.controls.update();
+
+    // Animate dust motes
+    const dustPos = this.dustParticles.geometry.attributes.position;
+    for (let i = 0; i < dustPos.count; i++) {
+      const y = dustPos.getY(i) + Math.sin(elapsed * 0.3 + i) * 0.001;
+      dustPos.setY(i, y > 12 ? 0 : y);
+    }
+    dustPos.needsUpdate = true;
+
+    // Update wing visibility based on camera distance
+    this.layout.updateVisibility(this.camera.position);
+
+    // Update DOF focus to orbit target distance
+    const focusDist = this.camera.position.distanceTo(this.controls.target);
+    this.postProcess.setFocusDistance(focusDist);
+
+    // Render through post-processing pipeline
+    this.postProcess.render();
+  }
+
+  private onResize(container: HTMLElement): void {
+    const w = container.clientWidth;
+    const h = container.clientHeight;
+    this.camera.aspect = w / h;
+    this.camera.updateProjectionMatrix();
+    this.renderer.setSize(w, h);
+    this.postProcess.resize(w, h);
+  }
+
+  start(): void {
+    this._running = true;
+    this.clock.start();
+    this.animate();
+  }
+
+  dispose(): void {
+    this._running = false;
+    if (this._resizeHandler) {
+      window.removeEventListener('resize', this._resizeHandler);
+      this._resizeHandler = null;
+    }
+    this.postProcess.dispose();
+    this.layout.dispose();
+    disposeMaterialLibrary(this.materials);
+    this.dustParticles.geometry.dispose();
+    (this.dustParticles.material as THREE.PointsMaterial).dispose();
+    this.controls.dispose();
+    this.renderer.dispose();
+  }
+}
diff --git a/src/showroom/ShowroomEnvironment.ts b/src/showroom/ShowroomEnvironment.ts
new file mode 100644
index 0000000..6b4925f
--- /dev/null
+++ b/src/showroom/ShowroomEnvironment.ts
@@ -0,0 +1,93 @@
+// ShowroomEnvironment.ts — HDR environment map for photorealistic IBL
+import * as THREE from 'three';
+
+/**
+ * Generate a high-quality procedural environment map for gallery/showroom IBL.
+ * Creates a warm gallery interior environment rather than the cold command-center gradient.
+ *
+ * This procedural approach avoids needing an external HDRI file download.
+ * It simulates a well-lit interior with warm ceiling lights and neutral walls.
+ */
+export function createShowroomEnvMap(renderer: THREE.WebGLRenderer): THREE.Texture {
+  const pmrem = new THREE.PMREMGenerator(renderer);
+  pmrem.compileEquirectangularShader();
+
+  const envScene = new THREE.Scene();
+
+  // Sky dome — warm dark interior
+  const skyGeo = new THREE.SphereGeometry(50, 32, 16);
+  const skyMat = new THREE.MeshBasicMaterial({
+    color: 0x1a1815,
+    side: THREE.BackSide,
+  });
+  envScene.add(new THREE.Mesh(skyGeo, skyMat));
+
+  // Ceiling highlight — warm white overhead panels (gallery lighting ~3200K)
+  const ceilGeo = new THREE.SphereGeometry(40, 16, 8, 0, Math.PI * 2, 0, Math.PI * 0.2);
+  const ceilMat = new THREE.MeshBasicMaterial({
+    color: 0xf5e6d0,
+    side: THREE.BackSide,
+  });
+  const ceil = new THREE.Mesh(ceilGeo, ceilMat);
+  ceil.rotation.x = Math.PI;
+  envScene.add(ceil);
+
+  // Floor reflection — dark polished concrete
+  const floorGeo = new THREE.SphereGeometry(40, 16, 8, 0, Math.PI * 2, Math.PI * 0.75, Math.PI * 0.25);
+  const floorMat = new THREE.MeshBasicMaterial({
+    color: 0x0d0c0a,
+    side: THREE.BackSide,
+  });
+  envScene.add(new THREE.Mesh(floorGeo, floorMat));
+
+  // Wall bands — warm brick tones from 4 sides
+  const wallMaterials: THREE.MeshBasicMaterial[] = [];
+  for (let i = 0; i < 4; i++) {
+    const wallGeo = new THREE.PlaneGeometry(30, 15);
+    const wallMat = new THREE.MeshBasicMaterial({
+      color: 0x3a2820,
+      side: THREE.DoubleSide,
+    });
+    wallMaterials.push(wallMat);
+    const wall = new THREE.Mesh(wallGeo, wallMat);
+    const angle = (i / 4) * Math.PI * 2;
+    wall.position.set(Math.cos(angle) * 35, 5, Math.sin(angle) * 35);
+    wall.lookAt(0, 5, 0);
+    envScene.add(wall);
+  }
+
+  // Track light spots — bright warm points at ceiling level
+  const spotMaterials: THREE.MeshBasicMaterial[] = [];
+  for (let i = 0; i < 6; i++) {
+    const spotGeo = new THREE.SphereGeometry(2, 8, 8);
+    const spotMat = new THREE.MeshBasicMaterial({ color: 0xffe8c0 });
+    spotMaterials.push(spotMat);
+    const spot = new THREE.Mesh(spotGeo, spotMat);
+    const angle = (i / 6) * Math.PI * 2;
+    spot.position.set(Math.cos(angle) * 15, 18, Math.sin(angle) * 15);
+    envScene.add(spot);
+  }
+
+  const envRenderTarget = pmrem.fromScene(envScene as any, 0.02);
+  const texture = envRenderTarget.texture;
+
+  // Cleanup — dispose PMREM render target, generator, and all env scene materials/geometries
+  pmrem.dispose();
+  envRenderTarget.dispose();
+  skyGeo.dispose();
+  skyMat.dispose();
+  ceilGeo.dispose();
+  ceilMat.dispose();
+  floorGeo.dispose();
+  floorMat.dispose();
+  wallMaterials.forEach(m => m.dispose());
+  spotMaterials.forEach(m => m.dispose());
+  // Dispose wall and spot geometries via traversal
+  envScene.traverse(child => {
+    if (child instanceof THREE.Mesh && child.geometry) {
+      child.geometry.dispose();
+    }
+  });
+
+  return texture;
+}
diff --git a/src/showroom/ShowroomLayout.ts b/src/showroom/ShowroomLayout.ts
new file mode 100644
index 0000000..3160ff6
--- /dev/null
+++ b/src/showroom/ShowroomLayout.ts
@@ -0,0 +1,343 @@
+// ShowroomLayout.ts — Builds the photorealistic showroom corridor and wing bays
+import * as THREE from 'three';
+import type { SceneConfig, WingConfig, WallConfig, LightConfig } from './types';
+import type { MaterialLibrary } from './ShowroomMaterials';
+
+export class ShowroomLayout {
+  private scene: THREE.Scene;
+  private materials: MaterialLibrary;
+  private corridorGroup: THREE.Group;
+  private wingGroups: THREE.Group[] = [];
+  private spotLights: THREE.SpotLight[] = [];
+
+  constructor(scene: THREE.Scene, materials: MaterialLibrary) {
+    this.scene = scene;
+    this.materials = materials;
+    this.corridorGroup = new THREE.Group();
+    this.corridorGroup.name = 'showroom-corridor';
+    scene.add(this.corridorGroup);
+  }
+
+  /**
+   * Build the showroom from a SceneConfig (output of FigmaTransformer).
+   */
+  buildFromConfig(config: SceneConfig): void {
+    this.buildCorridor(config.corridor);
+    this.buildWings(config.wings);
+    this.buildWallDisplays(config.walls);
+    this.buildLighting(config.lights);
+    this.buildFurniture(config.furniture);
+    this.buildEntry(config.entry);
+  }
+
+  private buildCorridor(corridor: SceneConfig['corridor']): void {
+    const { width, depth, height } = corridor;
+    const halfW = width / 2;
+    const halfD = depth / 2;
+
+    // Floor — polished concrete with normal map
+    const floorGeo = new THREE.PlaneGeometry(width, depth, 1, 1);
+    const floor = new THREE.Mesh(floorGeo, this.materials.concrete);
+    floor.rotation.x = -Math.PI / 2;
+    floor.receiveShadow = true;
+    this.corridorGroup.add(floor);
+
+    // Left wall — exposed brick
+    const leftWallGeo = new THREE.PlaneGeometry(depth, height);
+    const leftWall = new THREE.Mesh(leftWallGeo, this.materials.brick);
+    leftWall.position.set(-halfW, height / 2, 0);
+    leftWall.rotation.y = Math.PI / 2;
+    leftWall.receiveShadow = true;
+    this.corridorGroup.add(leftWall);
+
+    // Right wall — exposed brick
+    const rightWall = new THREE.Mesh(leftWallGeo, this.materials.brick);
+    rightWall.position.set(halfW, height / 2, 0);
+    rightWall.rotation.y = -Math.PI / 2;
+    rightWall.receiveShadow = true;
+    this.corridorGroup.add(rightWall);
+
+    // Back wall — dark charcoal
+    const backWallGeo = new THREE.PlaneGeometry(width, height);
+    const backWallMat = this.materials.steel.clone();
+    backWallMat.color.setHex(0x1a1a1a);
+    const backWall = new THREE.Mesh(backWallGeo, backWallMat);
+    backWall.position.set(0, height / 2, -halfD);
+    backWall.receiveShadow = true;
+    this.corridorGroup.add(backWall);
+
+    // Ceiling — steel with exposed duct geometry
+    const ceilGeo = new THREE.PlaneGeometry(width, depth);
+    const ceiling = new THREE.Mesh(ceilGeo, this.materials.steel);
+    ceiling.rotation.x = Math.PI / 2;
+    ceiling.position.y = height;
+    this.corridorGroup.add(ceiling);
+
+    // Exposed ducts — cylindrical pipes along ceiling
+    const ductMat = this.materials.steel.clone();
+    ductMat.metalness = 0.5;
+    for (let i = 0; i < 3; i++) {
+      const ductGeo = new THREE.CylinderGeometry(0.15, 0.15, depth, 8);
+      const duct = new THREE.Mesh(ductGeo, ductMat);
+      duct.rotation.x = Math.PI / 2;
+      duct.position.set(-halfW / 2 + i * (halfW / 2), height - 0.3, 0);
+      this.corridorGroup.add(duct);
+    }
+
+    // Baseboards — warm wood trim
+    const baseGeo = new THREE.BoxGeometry(depth, 0.15, 0.04);
+    for (const xPos of [-halfW + 0.02, halfW - 0.02]) {
+      const base = new THREE.Mesh(baseGeo, this.materials.wood);
+      base.position.set(xPos, 0.075, 0);
+      base.rotation.y = Math.PI / 2;
+      this.corridorGroup.add(base);
+    }
+  }
+
+  private buildWings(wings: WingConfig[]): void {
+    wings.forEach((wingConfig, i) => {
+      const group = new THREE.Group();
+      group.name = `wing-${i}-${wingConfig.vendor}`;
+      group.userData = { vendor: wingConfig.vendor, index: i, loaded: false };
+
+      // Wing entrance frame — brushed aluminum
+      const frameGeo = new THREE.BoxGeometry(0.08, 3.5, 0.08);
+      const leftFrame = new THREE.Mesh(frameGeo, this.materials.aluminum);
+      leftFrame.position.set(-1.5, 1.75, 0);
+      group.add(leftFrame);
+
+      const rightFrame = new THREE.Mesh(frameGeo, this.materials.aluminum);
+      rightFrame.position.set(1.5, 1.75, 0);
+      group.add(rightFrame);
+
+      // Header bar
+      const headerGeo = new THREE.BoxGeometry(3.08, 0.08, 0.08);
+      const header = new THREE.Mesh(headerGeo, this.materials.aluminum);
+      header.position.set(0, 3.5, 0);
+      group.add(header);
+
+      // Vendor label (canvas texture)
+      const labelCanvas = document.createElement('canvas');
+      labelCanvas.width = 512;
+      labelCanvas.height = 64;
+      const ctx = labelCanvas.getContext('2d');
+      if (ctx) {
+        ctx.fillStyle = '#1a1a1a';
+        ctx.fillRect(0, 0, 512, 64);
+        ctx.fillStyle = '#f0ebe4';
+        ctx.font = 'bold 28px Inter, -apple-system, sans-serif';
+        ctx.textAlign = 'center';
+        ctx.fillText(wingConfig.vendor.toUpperCase(), 256, 42);
+      }
+      const labelTex = new THREE.CanvasTexture(labelCanvas);
+      const labelGeo = new THREE.PlaneGeometry(2.5, 0.3);
+      const labelMat = new THREE.MeshBasicMaterial({ map: labelTex });
+      const label = new THREE.Mesh(labelGeo, labelMat);
+      label.position.set(0, 3.7, 0.01);
+      group.add(label);
+
+      // Position the wing in the corridor
+      group.position.set(
+        wingConfig.position[0],
+        wingConfig.position[1],
+        wingConfig.position[2]
+      );
+      group.rotation.y = wingConfig.rotation;
+
+      this.wingGroups.push(group);
+      this.corridorGroup.add(group);
+    });
+  }
+
+  private buildWallDisplays(walls: WallConfig[]): void {
+    walls.forEach(wall => {
+      if (wall.hasSamplePanels) {
+        // Large mounted sample panel
+        const panelGeo = new THREE.PlaneGeometry(wall.dimensions[0], wall.dimensions[1]);
+        const panel = new THREE.Mesh(panelGeo, this.materials.fabric);
+        panel.position.set(wall.position[0], wall.position[1], wall.position[2]);
+        this.corridorGroup.add(panel);
+
+        // Aluminum frame around panel
+        const frameThickness = 0.03;
+        const frameDepth = 0.04;
+        const frameMat = this.materials.aluminum;
+
+        // Top frame
+        const topGeo = new THREE.BoxGeometry(wall.dimensions[0] + frameThickness * 2, frameThickness, frameDepth);
+        const top = new THREE.Mesh(topGeo, frameMat);
+        top.position.set(wall.position[0], wall.position[1] + wall.dimensions[1] / 2, wall.position[2] + 0.02);
+        this.corridorGroup.add(top);
+
+        // Bottom frame
+        const bottom = top.clone();
+        bottom.position.y = wall.position[1] - wall.dimensions[1] / 2;
+        this.corridorGroup.add(bottom);
+      }
+
+      if (wall.hasBookshelf) {
+        // Glass-front bookshelf
+        const shelfGeo = new THREE.BoxGeometry(wall.dimensions[0], wall.dimensions[1], 0.3);
+        const shelf = new THREE.Mesh(shelfGeo, this.materials.wood);
+        shelf.position.set(wall.position[0], wall.position[1], wall.position[2]);
+        this.corridorGroup.add(shelf);
+
+        // Glass front
+        const glassGeo = new THREE.PlaneGeometry(wall.dimensions[0] - 0.05, wall.dimensions[1] - 0.05);
+        const glass = new THREE.Mesh(glassGeo, this.materials.glass);
+        glass.position.set(wall.position[0], wall.position[1], wall.position[2] + 0.16);
+        this.corridorGroup.add(glass);
+      }
+    });
+  }
+
+  private buildLighting(lights: LightConfig[]): void {
+    lights.forEach(lightConfig => {
+      if (lightConfig.type === 'spot') {
+        const spot = new THREE.SpotLight(
+          new THREE.Color(lightConfig.color),
+          lightConfig.intensity,
+          30,
+          lightConfig.angle || Math.PI / 6,
+          lightConfig.penumbra || 0.4,
+          1
+        );
+        spot.position.set(lightConfig.position[0], lightConfig.position[1], lightConfig.position[2]);
+        if (lightConfig.target) {
+          spot.target.position.set(lightConfig.target[0], lightConfig.target[1], lightConfig.target[2]);
+          this.scene.add(spot.target);
+        }
+        spot.castShadow = true;
+        spot.shadow.mapSize.set(1024, 1024);
+        this.spotLights.push(spot);
+        this.corridorGroup.add(spot);
+
+        // Track light housing (small chrome cylinder)
+        const housingGeo = new THREE.CylinderGeometry(0.08, 0.12, 0.15, 8);
+        const housing = new THREE.Mesh(housingGeo, this.materials.trackLight);
+        housing.position.set(lightConfig.position[0], lightConfig.position[1], lightConfig.position[2]);
+        this.corridorGroup.add(housing);
+
+        // Volumetric light cone (subtle)
+        const coneH = lightConfig.position[1] - (lightConfig.target?.[1] || 0);
+        const coneR = Math.tan(lightConfig.angle || Math.PI / 6) * coneH;
+        const coneGeo = new THREE.ConeGeometry(coneR, coneH, 16, 1, true);
+        const coneMat = new THREE.MeshBasicMaterial({
+          color: lightConfig.color,
+          transparent: true,
+          opacity: 0.015,
+          blending: THREE.AdditiveBlending,
+          side: THREE.DoubleSide,
+          depthWrite: false,
+        });
+        const cone = new THREE.Mesh(coneGeo, coneMat);
+        cone.position.set(
+          lightConfig.position[0],
+          lightConfig.position[1] - coneH / 2,
+          lightConfig.position[2]
+        );
+        this.corridorGroup.add(cone);
+      }
+    });
+
+    // Track rails (continuous aluminum rail along ceiling)
+    const railGeo = new THREE.BoxGeometry(40, 0.03, 0.05);
+    const rail = new THREE.Mesh(railGeo, this.materials.aluminum);
+    rail.position.set(0, 11.5, 0);
+    this.corridorGroup.add(rail);
+  }
+
+  private buildFurniture(furniture: SceneConfig['furniture']): void {
+    furniture.forEach(item => {
+      if (item.type === 'display-table') {
+        const tableGeo = new THREE.BoxGeometry(item.dimensions[0], item.dimensions[1], item.dimensions[2]);
+        const table = new THREE.Mesh(tableGeo, this.materials.wood);
+        table.position.set(item.position[0], item.position[1], item.position[2]);
+        table.castShadow = true;
+        this.corridorGroup.add(table);
+      }
+    });
+  }
+
+  private buildEntry(entry: SceneConfig['entry']): void {
+    // Entry signage
+    const signCanvas = document.createElement('canvas');
+    signCanvas.width = 1024;
+    signCanvas.height = 128;
+    const ctx = signCanvas.getContext('2d');
+    if (ctx) {
+      ctx.fillStyle = '#1a1a1a';
+      ctx.fillRect(0, 0, 1024, 128);
+      ctx.fillStyle = '#f0ebe4';
+      ctx.font = 'bold 36px Inter, -apple-system, sans-serif';
+      ctx.textAlign = 'center';
+      ctx.fillText(entry.signText, 512, 70);
+      ctx.font = '18px Inter, -apple-system, sans-serif';
+      ctx.fillStyle = '#888888';
+      ctx.fillText('ARCHITECTURAL TRADE SHOWROOM', 512, 105);
+    }
+
+    const signTex = new THREE.CanvasTexture(signCanvas);
+    const signGeo = new THREE.PlaneGeometry(6, 0.75);
+    const signMat = new THREE.MeshStandardMaterial({
+      map: signTex,
+      emissive: new THREE.Color(0xffffff),
+      emissiveMap: signTex,
+      emissiveIntensity: 0.3,
+    });
+    const sign = new THREE.Mesh(signGeo, signMat);
+    sign.position.set(entry.position[0], 10, entry.position[2]);
+    this.corridorGroup.add(sign);
+  }
+
+  /**
+   * Get wing groups for culling/visibility management.
+   */
+  getWings(): THREE.Group[] {
+    return this.wingGroups;
+  }
+
+  /**
+   * Set visibility of wings by distance from camera for performance.
+   */
+  updateVisibility(cameraPosition: THREE.Vector3, maxDistance = 30): void {
+    this.wingGroups.forEach(wing => {
+      const dist = wing.position.distanceTo(cameraPosition);
+      wing.visible = dist < maxDistance;
+    });
+  }
+
+  dispose(): void {
+    // Dispose spotlight targets from scene root
+    this.spotLights.forEach(spot => {
+      if (spot.target.parent) {
+        spot.target.parent.remove(spot.target);
+      }
+      spot.dispose();
+    });
+
+    this.corridorGroup.traverse(child => {
+      if (child instanceof THREE.Mesh) {
+        child.geometry.dispose();
+        // Dispose material (handles cloned materials and canvas textures)
+        const mat = child.material;
+        if (Array.isArray(mat)) {
+          mat.forEach(m => {
+            if (m.map) m.map.dispose();
+            if (m.emissiveMap) m.emissiveMap.dispose();
+            if (m.normalMap) m.normalMap.dispose();
+            m.dispose();
+          });
+        } else {
+          if (mat.map) mat.map.dispose();
+          if ((mat as any).emissiveMap) (mat as any).emissiveMap.dispose();
+          if ((mat as any).normalMap) (mat as any).normalMap.dispose();
+          mat.dispose();
+        }
+      }
+    });
+    this.scene.remove(this.corridorGroup);
+    this.wingGroups.length = 0;
+    this.spotLights.length = 0;
+  }
+}
diff --git a/src/showroom/ShowroomMaterials.ts b/src/showroom/ShowroomMaterials.ts
new file mode 100644
index 0000000..80af5ca
--- /dev/null
+++ b/src/showroom/ShowroomMaterials.ts
@@ -0,0 +1,104 @@
+// ShowroomMaterials.ts — PBR material definitions for photorealistic showroom
+import * as THREE from 'three';
+import {
+  generateBrickNormalMap,
+  generateConcreteNormalMap,
+  generateBrushedAluminumNormalMap,
+  generateWoodNormalMap,
+  generateFabricNormalMap,
+} from './ProceduralMaps';
+
+export interface MaterialLibrary {
+  brick: THREE.MeshStandardMaterial;
+  concrete: THREE.MeshStandardMaterial;
+  aluminum: THREE.MeshStandardMaterial;
+  wood: THREE.MeshStandardMaterial;
+  fabric: THREE.MeshStandardMaterial;
+  steel: THREE.MeshStandardMaterial;
+  glass: THREE.MeshStandardMaterial;
+  trackLight: THREE.MeshStandardMaterial;
+}
+
+/**
+ * Create the full PBR material library for the showroom.
+ * Each material has proper normal maps for photorealistic surface detail.
+ */
+export function createMaterialLibrary(): MaterialLibrary {
+  return {
+    brick: new THREE.MeshStandardMaterial({
+      color: 0x8b4513,
+      metalness: 0.0,
+      roughness: 0.85,
+      normalMap: generateBrickNormalMap(),
+      normalScale: new THREE.Vector2(1.2, 1.2),
+    }),
+
+    concrete: new THREE.MeshStandardMaterial({
+      color: 0x8a8580,
+      metalness: 0.15,
+      roughness: 0.55,
+      normalMap: generateConcreteNormalMap(),
+      normalScale: new THREE.Vector2(0.5, 0.5),
+    }),
+
+    aluminum: new THREE.MeshStandardMaterial({
+      color: 0xc0c0c0,
+      metalness: 0.7,
+      roughness: 0.25,
+      normalMap: generateBrushedAluminumNormalMap(),
+      normalScale: new THREE.Vector2(0.3, 0.3),
+      envMapIntensity: 1.5,
+    }),
+
+    wood: new THREE.MeshStandardMaterial({
+      color: 0x8b6914,
+      metalness: 0.0,
+      roughness: 0.65,
+      normalMap: generateWoodNormalMap(),
+      normalScale: new THREE.Vector2(0.8, 0.8),
+    }),
+
+    fabric: new THREE.MeshStandardMaterial({
+      color: 0xf0ebe4,
+      metalness: 0.0,
+      roughness: 0.4,
+      normalMap: generateFabricNormalMap(),
+      normalScale: new THREE.Vector2(0.2, 0.2),
+      envMapIntensity: 0.3,
+    }),
+
+    steel: new THREE.MeshStandardMaterial({
+      color: 0x2a2a2a,
+      metalness: 0.6,
+      roughness: 0.3,
+      envMapIntensity: 1.0,
+    }),
+
+    glass: new THREE.MeshStandardMaterial({
+      color: 0xffffff,
+      metalness: 0.0,
+      roughness: 0.05,
+      transparent: true,
+      opacity: 0.15,
+      envMapIntensity: 2.0,
+    }),
+
+    trackLight: new THREE.MeshStandardMaterial({
+      color: 0xffffff,
+      metalness: 0.9,
+      roughness: 0.1,
+      emissive: new THREE.Color(0xf5e6d0),
+      emissiveIntensity: 0.8,
+    }),
+  };
+}
+
+/**
+ * Dispose all materials and their textures.
+ */
+export function disposeMaterialLibrary(lib: MaterialLibrary): void {
+  Object.values(lib).forEach(mat => {
+    if (mat.normalMap) mat.normalMap.dispose();
+    mat.dispose();
+  });
+}
diff --git a/src/showroom/ShowroomPostProcess.ts b/src/showroom/ShowroomPostProcess.ts
new file mode 100644
index 0000000..baa28dc
--- /dev/null
+++ b/src/showroom/ShowroomPostProcess.ts
@@ -0,0 +1,181 @@
+// ShowroomPostProcess.ts — Photorealistic post-processing for the showroom
+// Adds SSAO, DOF, and color grading on top of existing bloom/grain pipeline
+import * as THREE from 'three';
+import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
+import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
+import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
+import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
+import { SSAOPass } from 'three/examples/jsm/postprocessing/SSAOPass.js';
+import { BokehPass } from 'three/examples/jsm/postprocessing/BokehPass.js';
+
+// ── Color Grading Shader ─────────────────────────────────────────────────────
+// Warm architectural tones: slight orange lift in shadows, cool highlight compression
+const ColorGradingShader = {
+  name: 'ColorGradingShader',
+  uniforms: {
+    tDiffuse: { value: null as THREE.Texture | null },
+    uSaturation: { value: 1.1 },
+    uContrast: { value: 1.08 },
+    uBrightness: { value: 0.02 },
+    uWarmth: { value: 0.04 },
+  },
+  vertexShader: /* glsl */ `
+    varying vec2 vUv;
+    void main() {
+      vUv = uv;
+      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+    }
+  `,
+  fragmentShader: /* glsl */ `
+    uniform sampler2D tDiffuse;
+    uniform float uSaturation;
+    uniform float uContrast;
+    uniform float uBrightness;
+    uniform float uWarmth;
+    varying vec2 vUv;
+
+    void main() {
+      vec4 color = texture2D(tDiffuse, vUv);
+
+      // Saturation adjustment
+      float luminance = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));
+      color.rgb = mix(vec3(luminance), color.rgb, uSaturation);
+
+      // Contrast (pivot at mid-gray)
+      color.rgb = (color.rgb - 0.5) * uContrast + 0.5;
+
+      // Brightness lift
+      color.rgb += uBrightness;
+
+      // Warm tint — add slight orange to shadows, cool highlights slightly
+      float shadowMask = 1.0 - smoothstep(0.0, 0.5, luminance);
+      color.r += uWarmth * shadowMask;
+      color.g += uWarmth * 0.5 * shadowMask;
+
+      // Vignette — darkens corners for photographic depth
+      vec2 vUvCenter = vUv - 0.5;
+      float vignette = 1.0 - dot(vUvCenter, vUvCenter) * 0.5;
+      color.rgb *= vignette;
+
+      gl_FragColor = color;
+    }
+  `,
+};
+
+// ── Film Grain (reused from war room) ────────────────────────────────────────
+const FilmGrainShader = {
+  name: 'FilmGrainShader',
+  uniforms: {
+    tDiffuse: { value: null as THREE.Texture | null },
+    uTime: { value: 0.0 },
+    uOpacity: { value: 0.006 },
+  },
+  vertexShader: /* glsl */ `
+    varying vec2 vUv;
+    void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
+  `,
+  fragmentShader: /* glsl */ `
+    uniform sampler2D tDiffuse;
+    uniform float uTime;
+    uniform float uOpacity;
+    varying vec2 vUv;
+    float hash(vec2 p) { p = fract(p * vec2(234.34, 435.345)); p += dot(p, p + 34.23); return fract(p.x * p.y); }
+    void main() {
+      vec4 color = texture2D(tDiffuse, vUv);
+      float grain = hash(vUv + fract(uTime * 0.1));
+      color.rgb += (grain - 0.5) * 2.0 * uOpacity;
+      gl_FragColor = color;
+    }
+  `,
+};
+
+export class ShowroomPostProcess {
+  private composer: EffectComposer;
+  private ssaoPass: SSAOPass;
+  private bloomPass: UnrealBloomPass;
+  private bokehPass: BokehPass;
+  private colorGradingPass: ShaderPass;
+  private filmGrainPass: ShaderPass;
+  private clock: THREE.Clock;
+
+  constructor(
+    renderer: THREE.WebGLRenderer,
+    scene: THREE.Scene,
+    camera: THREE.PerspectiveCamera
+  ) {
+    this.clock = new THREE.Clock();
+    const size = renderer.getSize(new THREE.Vector2());
+
+    this.composer = new EffectComposer(renderer);
+
+    // Pass 1: Render
+    this.composer.addPass(new RenderPass(scene, camera));
+
+    // Pass 2: SSAO — ambient occlusion for depth
+    this.ssaoPass = new SSAOPass(scene, camera, size.x, size.y);
+    this.ssaoPass.kernelRadius = 0.5;
+    this.ssaoPass.minDistance = 0.001;
+    this.ssaoPass.maxDistance = 0.1;
+    (this.ssaoPass as any).output = SSAOPass.OUTPUT.Default;
+    this.composer.addPass(this.ssaoPass);
+
+    // Pass 3: Bloom — subtle architectural glow
+    this.bloomPass = new UnrealBloomPass(
+      new THREE.Vector2(size.x, size.y),
+      0.2,   // strength (slightly less than war room — showroom is brighter)
+      0.4,   // radius
+      0.7    // threshold
+    );
+    this.composer.addPass(this.bloomPass);
+
+    // Pass 4: Depth of Field — cinematic focus
+    this.bokehPass = new BokehPass(scene, camera, {
+      focus: 15.0,
+      aperture: 0.002,
+      maxblur: 0.005,
+    });
+    this.composer.addPass(this.bokehPass);
+
+    // Pass 5: Color Grading — warm architectural tones
+    this.colorGradingPass = new ShaderPass(ColorGradingShader);
+    this.composer.addPass(this.colorGradingPass);
+
+    // Pass 6: Film Grain — organic texture
+    this.filmGrainPass = new ShaderPass(FilmGrainShader);
+    this.composer.addPass(this.filmGrainPass);
+  }
+
+  render(): void {
+    this.filmGrainPass.uniforms['uTime'].value = this.clock.getElapsedTime();
+    this.composer.render();
+  }
+
+  setFocusDistance(distance: number): void {
+    // BokehPass stores uniforms on materialBokeh.uniforms
+    const uniforms = (this.bokehPass as any).materialBokeh?.uniforms
+      ?? (this.bokehPass as any).uniforms;
+    if (uniforms?.focus) {
+      uniforms.focus.value = distance;
+    }
+  }
+
+  resize(width: number, height: number): void {
+    this.composer.setSize(width, height);
+    this.bloomPass.resolution.set(width, height);
+    this.ssaoPass.setSize(width, height);
+  }
+
+  getComposer(): EffectComposer {
+    return this.composer;
+  }
+
+  dispose(): void {
+    this.composer.renderTarget1.dispose();
+    this.composer.renderTarget2.dispose();
+    this.composer.passes.forEach(pass => {
+      if ('dispose' in pass && typeof pass.dispose === 'function') {
+        pass.dispose();
+      }
+    });
+  }
+}
diff --git a/src/showroom/TextureCrop.ts b/src/showroom/TextureCrop.ts
new file mode 100644
index 0000000..bccc742
--- /dev/null
+++ b/src/showroom/TextureCrop.ts
@@ -0,0 +1,159 @@
+// TextureCrop.ts — Center-crop images to 512x768 with white-border trimming
+// Loads image → trims white borders → center-crops to 2:3 aspect → returns CanvasTexture
+import * as THREE from 'three';
+
+const CANVAS_W = 512;
+const CANVAS_H = 768;
+const WHITE_THRESHOLD = 240;  // RGB channels above this = "white"
+const SCAN_DEPTH = 80;        // Max pixels inward to scan for border
+
+/**
+ * Detect how many pixels of white border exist on each edge.
+ * Scans rows/columns from outside in, stops when a non-white row/col is found.
+ */
+function detectWhiteBorders(
+  data: Uint8ClampedArray,
+  w: number,
+  h: number
+): { top: number; bottom: number; left: number; right: number } {
+  const isWhitePixel = (x: number, y: number): boolean => {
+    const i = (y * w + x) * 4;
+    return data[i] > WHITE_THRESHOLD
+      && data[i + 1] > WHITE_THRESHOLD
+      && data[i + 2] > WHITE_THRESHOLD;
+  };
+
+  // Check if an entire row is white (sample every 4th pixel for speed)
+  const isWhiteRow = (y: number): boolean => {
+    for (let x = 0; x < w; x += 4) {
+      if (!isWhitePixel(x, y)) return false;
+    }
+    return true;
+  };
+
+  // Check if an entire column is white
+  const isWhiteCol = (x: number): boolean => {
+    for (let y = 0; y < h; y += 4) {
+      if (!isWhitePixel(x, y)) return false;
+    }
+    return true;
+  };
+
+  let top = 0;
+  while (top < Math.min(SCAN_DEPTH, h / 3) && isWhiteRow(top)) top++;
+
+  let bottom = 0;
+  while (bottom < Math.min(SCAN_DEPTH, h / 3) && isWhiteRow(h - 1 - bottom)) bottom++;
+
+  let left = 0;
+  while (left < Math.min(SCAN_DEPTH, w / 3) && isWhiteCol(left)) left++;
+
+  let right = 0;
+  while (right < Math.min(SCAN_DEPTH, w / 3) && isWhiteCol(w - 1 - right)) right++;
+
+  return { top, bottom, left, right };
+}
+
+/**
+ * Load an image URL, trim white borders, center-crop to 512x768 canvas.
+ * Returns a Promise<THREE.CanvasTexture>.
+ */
+export function cropTexture(imageUrl: string): Promise<THREE.CanvasTexture> {
+  return new Promise((resolve, reject) => {
+    const img = new Image();
+    img.crossOrigin = 'anonymous';
+
+    img.onload = () => {
+      // Step 1: Draw full image to a scratch canvas for pixel analysis
+      const scratch = document.createElement('canvas');
+      scratch.width = img.naturalWidth;
+      scratch.height = img.naturalHeight;
+      const sCtx = scratch.getContext('2d')!;
+      sCtx.drawImage(img, 0, 0);
+
+      const imgData = sCtx.getImageData(0, 0, scratch.width, scratch.height);
+      const borders = detectWhiteBorders(imgData.data, scratch.width, scratch.height);
+
+      // Step 2: Compute content rect (after trimming white borders)
+      const contentX = borders.left;
+      const contentY = borders.top;
+      const contentW = scratch.width - borders.left - borders.right;
+      const contentH = scratch.height - borders.top - borders.bottom;
+
+      if (contentW <= 0 || contentH <= 0) {
+        // Entire image is white — use full image as fallback
+        const fallback = createFallbackTexture(imageUrl);
+        resolve(fallback);
+        return;
+      }
+
+      // Step 3: Center-crop the content rect to 2:3 aspect ratio
+      const targetAspect = CANVAS_W / CANVAS_H; // 0.667
+      const contentAspect = contentW / contentH;
+
+      let srcX: number, srcY: number, srcW: number, srcH: number;
+
+      if (contentAspect > targetAspect) {
+        // Content is wider than target — crop sides
+        srcH = contentH;
+        srcW = Math.round(contentH * targetAspect);
+        srcX = contentX + Math.round((contentW - srcW) / 2);
+        srcY = contentY;
+      } else {
+        // Content is taller than target — crop top/bottom
+        srcW = contentW;
+        srcH = Math.round(contentW / targetAspect);
+        srcX = contentX;
+        srcY = contentY + Math.round((contentH - srcH) / 2);
+      }
+
+      // Step 4: Draw cropped region onto final 512x768 canvas
+      const canvas = document.createElement('canvas');
+      canvas.width = CANVAS_W;
+      canvas.height = CANVAS_H;
+      const ctx = canvas.getContext('2d')!;
+
+      // Fill with warm gallery bg in case of rounding gaps
+      ctx.fillStyle = '#f0ebe4';
+      ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
+
+      ctx.drawImage(
+        img,
+        srcX, srcY, srcW, srcH,  // source rect (cropped)
+        0, 0, CANVAS_W, CANVAS_H // dest rect (fill canvas)
+      );
+
+      const texture = new THREE.CanvasTexture(canvas);
+      texture.colorSpace = THREE.SRGBColorSpace;
+      texture.generateMipmaps = true;
+      texture.minFilter = THREE.LinearMipmapLinearFilter;
+      texture.magFilter = THREE.LinearFilter;
+
+      resolve(texture);
+    };
+
+    img.onerror = () => reject(new Error(`Failed to load image: ${imageUrl}`));
+    img.src = imageUrl;
+  });
+}
+
+/**
+ * Fallback: warm canvas with a subtle message (for all-white or broken images).
+ */
+function createFallbackTexture(label: string): THREE.CanvasTexture {
+  const canvas = document.createElement('canvas');
+  canvas.width = CANVAS_W;
+  canvas.height = CANVAS_H;
+  const ctx = canvas.getContext('2d')!;
+
+  ctx.fillStyle = '#f0ebe4';
+  ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
+  ctx.fillStyle = '#888888';
+  ctx.font = '18px Inter, -apple-system, sans-serif';
+  ctx.textAlign = 'center';
+  ctx.fillText('Image unavailable', CANVAS_W / 2, CANVAS_H / 2);
+
+  const texture = new THREE.CanvasTexture(canvas);
+  texture.colorSpace = THREE.SRGBColorSpace;
+  return texture;
+}
diff --git a/src/showroom/index.ts b/src/showroom/index.ts
new file mode 100644
index 0000000..1a8e0e0
--- /dev/null
+++ b/src/showroom/index.ts
@@ -0,0 +1,50 @@
+// src/showroom/index.ts — Showroom standalone entry point
+import { Showroom } from './Showroom';
+import { SearchOverlay } from './SearchOverlay';
+import type { SceneConfig } from './types';
+
+// Default showroom config (used when no Figma data available)
+const DEFAULT_CONFIG: SceneConfig = {
+  corridor: { width: 40, depth: 20, height: 12, wallMaterial: 'brick', floorMaterial: 'polished-concrete' },
+  wings: Array.from({ length: 6 }, (_, i) => ({
+    index: i,
+    vendor: ['Thibaut', 'Schumacher', 'Arte', 'Scalamandre', 'Elitis', 'Phillip Jeffries'][i],
+    position: [-12 + i * 5, 0, -8] as [number, number, number],
+    rotation: 0,
+    panelCount: 12,
+    accentColor: ['#1a4b8c', '#c41e3a', '#d4a574', '#2d4a3e', '#e67e22', '#8b4513'][i],
+  })),
+  walls: [
+    { position: [-18, 4, 0], dimensions: [4, 8], material: 'brick', hasBookshelf: true, hasSamplePanels: false, sampleCount: 0 },
+    { position: [18, 4, 0], dimensions: [4, 8], material: 'brick', hasBookshelf: false, hasSamplePanels: true, sampleCount: 3 },
+  ],
+  lights: Array.from({ length: 8 }, (_, i) => ({
+    type: 'spot' as const,
+    position: [-14 + i * 4, 11, 0] as [number, number, number],
+    target: [-14 + i * 4, 0, 0] as [number, number, number],
+    color: '#f5e6d0',
+    intensity: 1.2,
+    angle: Math.PI / 5,
+    penumbra: 0.4,
+  })),
+  furniture: [
+    { type: 'display-table' as const, position: [0, 0.45, 0] as [number, number, number], dimensions: [3, 0.9, 1.5] as [number, number, number] },
+  ],
+  entry: { position: [0, 0, 10], signText: 'DESIGNER WALLCOVERINGS' },
+};
+
+// Boot
+const container = document.getElementById('showroom-container');
+if (container) {
+  const showroom = new Showroom(container);
+  showroom.loadFromConfig(DEFAULT_CONFIG);
+  showroom.start();
+
+  // Live search overlay — searches DW Shopify products
+  const search = new SearchOverlay(container);
+  search.setShowroom(showroom);
+
+  // Expose for MCP/debug
+  (window as any).__showroom = showroom;
+  (window as any).__search = search;
+}
diff --git a/src/showroom/types.ts b/src/showroom/types.ts
new file mode 100644
index 0000000..ac7bab2
--- /dev/null
+++ b/src/showroom/types.ts
@@ -0,0 +1,50 @@
+// types.ts — Shared type definitions for the photorealistic showroom pipeline
+
+export interface SceneConfig {
+  corridor: {
+    width: number;    // world units (1 unit = 1 foot)
+    depth: number;
+    height: number;
+    wallMaterial: 'brick' | 'concrete' | 'charcoal';
+    floorMaterial: 'polished-concrete' | 'wood' | 'marble';
+  };
+  wings: WingConfig[];
+  walls: WallConfig[];
+  lights: LightConfig[];
+  furniture: FurnitureConfig[];
+  entry: { position: [number, number, number]; signText: string };
+}
+
+export interface WingConfig {
+  index: number;
+  vendor: string;
+  position: [number, number, number];
+  rotation: number;
+  panelCount: number;
+  accentColor: string;   // hex from Figma fill
+}
+
+export interface WallConfig {
+  position: [number, number, number];
+  dimensions: [number, number];  // width, height
+  material: string;
+  hasBookshelf: boolean;
+  hasSamplePanels: boolean;
+  sampleCount: number;
+}
+
+export interface LightConfig {
+  type: 'spot' | 'track' | 'ambient' | 'point';
+  position: [number, number, number];
+  target?: [number, number, number];
+  color: string;
+  intensity: number;
+  angle?: number;    // SpotLight cone angle
+  penumbra?: number;
+}
+
+export interface FurnitureConfig {
+  type: 'display-table' | 'stool' | 'bookshelf' | 'sample-rack';
+  position: [number, number, number];
+  dimensions: [number, number, number];
+}
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..4a443e8
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,84 @@
+// ═══════════════════════════════════════════════
+// DW War Room 3D — Types
+// ═══════════════════════════════════════════════
+
+export interface AgentNode {
+  id: string;
+  name: string;
+  role: string;
+  dept: string;
+  color: string;
+  exec: string;
+  port?: number;
+  speaking: boolean;
+  position: { x: number; y: number; z: number };
+}
+
+export interface Executive {
+  id: string;
+  name: string;
+  role: string;
+  color: string;
+  port: number;
+}
+
+export interface MeetingState {
+  active: boolean;
+  id?: string;
+  type?: string;
+  phase?: string;
+  status?: string;
+  messages: MeetingMessage[];
+}
+
+export interface MeetingMessage {
+  id: number;
+  agent_id: string;
+  agent_name: string;
+  message: string;
+  message_type: string;
+  phase?: string;
+}
+
+export interface DecisionStats {
+  total: number;
+  pending: number;
+  approved: number;
+  rejected: number;
+  deferred: number;
+}
+
+export interface EscalationAlert {
+  id: number;
+  decision_id: string | null;
+  escalation_type: string;
+  hours_elapsed: number;
+  action_taken: string | null;
+}
+
+export interface GovernanceStatus {
+  governance_mode: string;
+  pending_decisions: number;
+  active_delegations: number;
+  active_escalations: number;
+}
+
+export interface WSEvent {
+  type: string;
+  payload: any;
+  timestamp: string;
+}
+
+export interface SystemHealth {
+  cpu: string;
+  memory: string;
+  services: number;
+  uptime: string;
+}
+
+export interface FeedItem {
+  agent_name: string;
+  action: string;
+  timestamp: string;
+  type?: string;
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..c7d9591
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,18 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "skipLibCheck": true,
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "isolatedModules": true,
+    "moduleDetection": "force",
+    "noEmit": true,
+    "strict": true,
+    "noUnusedLocals": false,
+    "noUnusedParameters": false
+  },
+  "include": ["src"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..9765235
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,107 @@
+import { defineConfig } from 'vite';
+import { resolve } from 'path';
+import https from 'https';
+
+// Shopify GraphQL search proxy for Vite dev server
+function shopifySearchPlugin() {
+  const SHOPIFY_STORE = 'designer-laboratory-sandbox.myshopify.com';
+  const SHOPIFY_TOKEN = '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';
+  const API_VERSION = '2024-01';
+
+  return {
+    name: 'shopify-search-proxy',
+    configureServer(server: any) {
+      server.middlewares.use('/api/shopify/search', (req: any, res: any) => {
+        const url = new URL(req.url || '/', `http://${req.headers.host}`);
+        const query = url.searchParams.get('q') || '';
+        const limit = Math.min(parseInt(url.searchParams.get('limit') || '10', 10), 20);
+
+        if (query.length < 2) {
+          res.writeHead(400, { 'Content-Type': 'application/json' });
+          return res.end(JSON.stringify({ error: 'Query too short' }));
+        }
+
+        const gqlQuery = JSON.stringify({
+          query: `{
+            products(first: ${limit}, query: "${query.replace(/"/g, '\\"')}") {
+              edges { node {
+                id title vendor productType handle tags
+                images(first: 1) { edges { node { url } } }
+                variants(first: 1) { edges { node { price sku } } }
+              }}
+            }
+          }`
+        });
+
+        const options = {
+          hostname: SHOPIFY_STORE,
+          path: `/admin/api/${API_VERSION}/graphql.json`,
+          method: 'POST',
+          headers: {
+            'X-Shopify-Access-Token': SHOPIFY_TOKEN,
+            'Content-Type': 'application/json',
+            'Content-Length': Buffer.byteLength(gqlQuery),
+          },
+        };
+
+        const proxyReq = https.request(options, (shopifyRes) => {
+          let data = '';
+          shopifyRes.on('data', (chunk: string) => { data += chunk; });
+          shopifyRes.on('end', () => {
+            try {
+              const gql = JSON.parse(data);
+              const edges = gql?.data?.products?.edges || [];
+              const products = edges.map(({ node }: any) => ({
+                id: parseInt(node.id.replace('gid://shopify/Product/', ''), 10),
+                title: node.title,
+                vendor: node.vendor,
+                product_type: node.productType,
+                handle: node.handle,
+                tags: node.tags ? node.tags.join(', ') : '',
+                images: node.images?.edges?.length
+                  ? node.images.edges.map((e: any) => ({ src: e.node.url }))
+                  : [],
+                variants: node.variants?.edges?.length
+                  ? node.variants.edges.map((e: any) => ({ price: e.node.price, sku: e.node.sku || '' }))
+                  : [{ price: '0.00', sku: '' }],
+              }));
+              res.writeHead(200, { 'Content-Type': 'application/json' });
+              res.end(JSON.stringify({ products }));
+            } catch {
+              res.writeHead(502, { 'Content-Type': 'application/json' });
+              res.end(JSON.stringify({ error: 'Parse error', products: [] }));
+            }
+          });
+        });
+
+        proxyReq.on('error', () => {
+          res.writeHead(502, { 'Content-Type': 'application/json' });
+          res.end(JSON.stringify({ error: 'Proxy error', products: [] }));
+        });
+
+        proxyReq.write(gqlQuery);
+        proxyReq.end();
+      });
+    },
+  };
+}
+
+export default defineConfig({
+  plugins: [shopifySearchPlugin()],
+  server: {
+    port: 4060,
+    host: '0.0.0.0',
+  },
+  preview: {
+    port: 4060,
+    host: '0.0.0.0',
+  },
+  build: {
+    rollupOptions: {
+      input: {
+        main: resolve(__dirname, 'index.html'),
+        showroom: resolve(__dirname, 'showroom.html'),
+      },
+    },
+  },
+});

(oldest)  ·  back to Dw War Room  ·  gitignore: add backup file patterns (*.bak-*, *.pre-*, *.ori 8109610 →