[object Object]

← back to Quadrille Showroom

Scene luxury pass 1: limewash plaster left wall (was brick), warm wide-plank oak floor, removed TV/medallion rug/sphere plants/framed art, single olive in stone planter, brass frame + gallery placard per board, picture-light graze on board wall, cognac-leather chairs

575ee371ca2f5fb1eee18cfa6600ef7215330ebf · 2026-06-27 00:28:35 -0700 · Steve

Files touched

Diff

commit 575ee371ca2f5fb1eee18cfa6600ef7215330ebf
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Jun 27 00:28:35 2026 -0700

    Scene luxury pass 1: limewash plaster left wall (was brick), warm wide-plank oak floor, removed TV/medallion rug/sphere plants/framed art, single olive in stone planter, brass frame + gallery placard per board, picture-light graze on board wall, cognac-leather chairs
---
 ART-DIRECTION.md      |  55 ++++++++++++
 WINGBOARD-SPEC.md     |  66 +++++++++++++++
 public/js/showroom.js | 230 ++++++++++++++++++++++++++++++--------------------
 3 files changed, 261 insertions(+), 90 deletions(-)

diff --git a/ART-DIRECTION.md b/ART-DIRECTION.md
new file mode 100644
index 0000000..3643749
--- /dev/null
+++ b/ART-DIRECTION.md
@@ -0,0 +1,55 @@
+# The Quadrille House — Art Direction (luxury gallery)
+
+Governing design system for the showroom's professional pass. DTD verdict (2026-06-27):
+**A — scene-first**, but under ONE shared system (folding in Codex's coherence dissent).
+Every overnight pass — 3D scene AND 2D HUD — conforms to this so the result reads
+coherently premium, not a polished mismatch.
+
+## North star
+A hushed, museum-grade private showroom — de Gournay / flagship-atelier energy.
+Restraint over decoration. The wallcoverings are the only loud thing; everything
+else is quiet, warm, and expensive. NOT a game level, NOT a SaaS dashboard.
+
+## Palette (CSS vars + Three material colors)
+- Ground/architecture neutrals: Plaster `#e7e1d6`, Warm Stone `#cfc7b8`, Greige `#b8afa0`
+- Floor: warm oak `#9a7a52` (satin) OR honed stone `#d8d3c8`
+- Accent metal: brushed brass `#b9933f` / `#c9a96e` (rail, hairlines, labels)
+- Ink / text: `#1a1206` on light, `#f0e6cf` on dark
+- Shadows: never pure black — warm charcoal `#1c1c22`
+
+## Materials (Three PBR — replace procedural cartoon canvases)
+- Walls: matte limewash plaster (roughness ~0.95, faint large-scale mottle, NOT brick)
+- Floor: wide-plank warm oak (subtle grain, satin, roughness ~0.4) or honed stone
+- Board frames: thin brushed-brass or dark walnut surround on each 30" sample wing
+- Rail: real brushed brass (metalness ~0.9, roughness ~0.25), slim
+- Remove: canvas brick wall, sphere "toy" plants, boxy TV, procedural medallion rug
+- Replace plants (if kept) with a single restrained olive/fig in a stone planter; or drop
+- Furniture: one low travertine/oak console + a leather bench — minimal, off to the side
+
+## Lighting
+- Soft museum wash: warm key (done), cool fill (done), brass-catching rim (done)
+- Add gentle picture-light glow grazing the board wall (warm, no hard hotspots)
+- Tone: ACES, exposure ~1.06; shadows soft (PCFSoft 2048, done)
+
+## Typography (HUD)
+- Display/serif for pattern names + wordmark: a refined serif (Cormorant/Canela feel) —
+  load a webfont; current Georgia is the placeholder
+- Labels/eyebrows: tracked uppercase sans, small, `letter-spacing: 2–2.5px`
+- Numbers/specs: tabular lining figures
+- Reduce gold saturation in chrome; use it as a hairline accent, not fills
+
+## Board presentation
+- Each 30" board: subtle frame + a small engraved-look label plate under it
+  (pattern name + colorway), brass-on-dark, like a gallery placard
+- Brass rail spans the wall; boards hang from it with a hint of a hinge bracket
+
+## Hard constraints (locked — never regress)
+- Boards 30" wide × 6 ft tall; 8 per wall; Peruse pages the rest
+- Fan-out = the focused board's L/R panels open to present ONE design
+- Peruse: 4 ft back, centered, open board, dwell 5 s, advance
+- China Seas specs 27"×27" straight match hand-screen-print (Grasscloth 36" free match),
+  shown in the spec-sheet detail card
+- Local only on :7690 (DTD verdict C earlier) — Kamatera deploy/DNS is GATED
+
+## Verify-before-done (every pass)
+`node scripts/walkthrough-record.js` → check FPS ≥ 50 + a screenshot, then commit.
diff --git a/WINGBOARD-SPEC.md b/WINGBOARD-SPEC.md
new file mode 100644
index 0000000..a6960a9
--- /dev/null
+++ b/WINGBOARD-SPEC.md
@@ -0,0 +1,66 @@
+# Wingboard Flip / Peruse — corrected interaction model (Phillip Jeffries reference)
+
+Source of truth for the board display + flip interaction. Supersedes the earlier
+"butterfly book-match" model. Grounded in Phillip Jeffries showrooms (they call them
+**"wingboards"**; showroom designers are shown **"flipping through wingboards"**, 430+
+boards) and two reference images Steve provided (a sample wing binder; a BOYADISPLAY
+wall-mounted wing rack fanned open).
+
+## The real-world fixture (what we're recreating)
+A wall-mounted **wingboard rack**: a horizontal brushed-brass rail; many large sample
+boards (each ~30" wide × 6 ft tall) hang from it, **hinged on a vertical edge**, packed
+tightly. Closed, you see only a **~4" sliver** of each board's wallcovering at its
+exposed edge — like the fore-edges of books on a shelf. You **flip/swing a wingboard
+open** on its vertical hinge (like turning a big page) to reveal its FULL wallcovering;
+flipped boards fan to the side. (Image #3 = the wall rack fanned open. Image #2 = the
+binder form: closed wings stacked left & right, the selected one open in the center.)
+
+## Showroom layout (50 at a time)
+- **50 wingboards** visible at once on the rail (Steve: "show 50 at a time"). Peruse pages
+  through all 883.
+- Boards overlap at a **~4" pitch** (0.1016 m) so each shows a 4" sliver of its design —
+  occlusion gives the sliver for free; NO image slicing needed. (Slicers exist if ever
+  wanted: TextureCrop.ts canvas-crop, img_tools.py Pillow, force-crop sharp.)
+- Slight per-board angle (fan-deck rake) so the exposed sliver faces the viewer.
+- The exposed sliver must show the actual wallcovering (the board's right/leading 4").
+
+## Flip / open interaction
+- **Closed:** 4" sliver only.
+- **Open (flip):** the selected wingboard swings open on its vertical hinge to face the
+  viewer and present its FULL design. Open it **~65°** (Steve: "open up to 65 degrees on
+  each") — i.e. the panel rotates out ~65° from the rail plane to present the design;
+  if a two-leaf board, each leaf opens 65° to show one continuous design across both.
+- **Isolate:** when one board is open, the others dim/recede so the open design is the
+  hero (Steve: "isolate the design ... to see entire design").
+- **One continuous design:** the open board shows ONE whole wallcovering (NOT mirrored).
+  If two leaves, left leaf = left half, right leaf = right half (continuous), not a
+  book-match mirror.
+
+## On load
+- **All wingboards closed except the MIDDLE one, which is open** showing its design
+  (Steve: "onload wings are all closed except for middle design opened").
+
+## Peruse (auto-flip)
+- Camera **4 ft back, centered** on the open board.
+- Open board N (flip to ~65°), present its full design isolated, **dwell 5 s**, then close
+  it and open board N+1 — an automated "flipping through wingboards." Pages window→window,
+  endless.
+- **Allow pause for more info:** Space pauses the tour on the current open board and
+  surfaces the full spec card / more info; resume continues.
+
+## Brand on the wall (Steve: "put the brand in high res on wall too")
+- A feature wall carries the house **brand wordmark in HIGH-RES** (crisp, large) — like
+  PJ's logo in their showrooms. Use a high-res canvas/texture (≥2048px) or vector-crisp
+  text; the current low-res "DESIGNER WALLCOVERINGS" sign is the placeholder to replace.
+
+## Aesthetic (per ART-DIRECTION.md)
+Restrained luxury gallery — limewash plaster, warm oak/stone, brushed-brass rail,
+soft museum lighting, engraved placards. Phillip-Jeffries-grade calm.
+
+## Verify (every pass)
+`node scripts/walkthrough-record.js` → FPS ≥ 50, specs intact (27"×27"), no console
+errors; LOOK at the screenshots; iterate on errors. Test over and over all night.
+
+## Design ideas: 4 Horsemen
+Use Figma (authenticated, Pro) + 21st Dev + Magic for the HUD + wingboard-browser design
+direction; implement in the showroom's vanilla CSS/HTML + Three.js.
diff --git a/public/js/showroom.js b/public/js/showroom.js
index 51180b6..6c9b92e 100644
--- a/public/js/showroom.js
+++ b/public/js/showroom.js
@@ -184,12 +184,12 @@ function srgb(tex) { if (tex && THREE.sRGBEncoding) tex.encoding = THREE.sRGBEnc
 // MATERIALS — all shared
 // ============================================================
 function initMaterials() {
-  MAT.floor = new THREE.MeshStandardMaterial({ color: 0x33333b, roughness: 0.35, metalness: 0.0, envMapIntensity: 0.7 }); // polished concrete
-  MAT.ceiling = new THREE.MeshStandardMaterial({ color: 0xdcdcd4, roughness: 0.95, metalness: 0.0 });
-  MAT.wall = new THREE.MeshStandardMaterial({ color: 0xccc7bc, roughness: 0.92, metalness: 0.0, envMapIntensity: 0.4 }); // matte gallery paint
-  MAT.chrome = new THREE.MeshStandardMaterial({ color: 0xcdcdd2, roughness: 0.22, metalness: 0.95, envMapIntensity: 1.0 }); // real brushed metal
+  MAT.floor = new THREE.MeshStandardMaterial({ color: 0x9a7a52, roughness: 0.42, metalness: 0.0, envMapIntensity: 0.5 }); // warm satin oak
+  MAT.ceiling = new THREE.MeshStandardMaterial({ color: 0xe7e1d6, roughness: 0.96, metalness: 0.0 }); // warm plaster
+  MAT.wall = new THREE.MeshStandardMaterial({ color: 0xe2dccf, roughness: 0.95, metalness: 0.0, envMapIntensity: 0.35 }); // matte limewash plaster
+  MAT.chrome = new THREE.MeshStandardMaterial({ color: 0xc9a96e, roughness: 0.25, metalness: 0.9, envMapIntensity: 1.0 }); // brushed brass
   MAT.wood = new THREE.MeshStandardMaterial({ color: 0x6b4a32, roughness: 0.5, metalness: 0.0, envMapIntensity: 0.5 }); // satin walnut
-  MAT.chair = new THREE.MeshStandardMaterial({ color: 0x3a3a44, roughness: 0.7, metalness: 0.1 });
+  MAT.chair = new THREE.MeshStandardMaterial({ color: 0x4a3528, roughness: 0.65, metalness: 0.05, envMapIntensity: 0.4 }); // cognac leather
   MAT.dark = new THREE.MeshStandardMaterial({ color: 0x0d0d10, roughness: 0.4, metalness: 0.3 });
   MAT.frame = new THREE.MeshStandardMaterial({ color: 0x32323a, roughness: 0.55, metalness: 0.3 });
   MAT.lightPanel = new THREE.MeshBasicMaterial({ color: 0xfffff5 });
@@ -199,20 +199,27 @@ function initMaterials() {
   MAT.books = [0x1a5276, 0x7d3c98, 0x1e8449, 0xb9770e, 0x922b21, 0x2c3e50, 0xd4ac0d, 0xa04000]
     .map(c => new THREE.MeshStandardMaterial({ color: c, roughness: 0.8, metalness: 0.0 }));
 
-  // Brick texture
+  // Limewash plaster — large-scale soft mottle (NOT brick). Matches the other walls
+  // but with a faint hand-troweled cloudiness so the left wall reads as real plaster.
   const bc = document.createElement('canvas');
-  bc.width = 128; bc.height = 128;
+  bc.width = 256; bc.height = 256;
   const bx = bc.getContext('2d');
-  bx.fillStyle = '#6b3a2a'; bx.fillRect(0, 0, 128, 128);
-  for (let row = 0; row < 4; row++) {
-    for (let col = 0; col < 2; col++) {
-      bx.fillStyle = `rgb(${100+Math.random()*40|0},${50+Math.random()*25|0},${35+Math.random()*20|0})`;
-      bx.fillRect(col * 64 + (row % 2 ? 32 : 0) + 2, row * 32 + 2, 60, 28);
-    }
+  bx.fillStyle = '#e2dccf'; bx.fillRect(0, 0, 256, 256);
+  // Soft overlapping translucent blobs = limewash cloudiness (warm + cool drift)
+  for (let i = 0; i < 40; i++) {
+    const cx = Math.random() * 256, cy = Math.random() * 256, r = 30 + Math.random() * 60;
+    const warm = Math.random() > 0.5;
+    const g = bx.createRadialGradient(cx, cy, 0, cx, cy, r);
+    const tint = warm ? '210,200,184' : '196,191,180';
+    g.addColorStop(0, `rgba(${tint},${0.05 + Math.random() * 0.06})`);
+    g.addColorStop(1, 'rgba(231,225,214,0)');
+    bx.fillStyle = g;
+    bx.beginPath(); bx.arc(cx, cy, r, 0, Math.PI * 2); bx.fill();
   }
   const bt = srgb(new THREE.CanvasTexture(bc));
-  bt.wrapS = bt.wrapT = THREE.RepeatWrapping; bt.repeat.set(3, 2);
-  MAT.brick = new THREE.MeshStandardMaterial({ map: bt, roughness: 0.95, metalness: 0.0 });
+  bt.wrapS = bt.wrapT = THREE.RepeatWrapping; bt.repeat.set(2, 2);
+  if (renderer.capabilities) bt.anisotropy = renderer.capabilities.getMaxAnisotropy();
+  MAT.brick = new THREE.MeshStandardMaterial({ map: bt, color: 0xe2dccf, roughness: 0.96, metalness: 0.0, envMapIntensity: 0.35 });
 }
 
 // ============================================================
@@ -309,18 +316,33 @@ function generatePatternCanvas(patternType, baseHex) {
 function buildRoom() {
   const W = CONFIG.room.width, D = CONFIG.room.depth, H = CONFIG.room.height;
 
-  // Floor — polished concrete with subtle grain
-  const fc = document.createElement('canvas'); fc.width = 256; fc.height = 256;
+  // Floor — warm wide-plank oak (satin). Planks run the room depth with subtle grain
+  // streaks + hairline seams between boards.
+  const fc = document.createElement('canvas'); fc.width = 256; fc.height = 512;
   const fx = fc.getContext('2d');
-  fx.fillStyle = '#3a3a42'; fx.fillRect(0, 0, 256, 256);
-  // Grain texture
-  for (let i = 0; i < 4000; i++) { fx.fillStyle = `rgba(${50+Math.random()*30|0},${50+Math.random()*30|0},${55+Math.random()*30|0},0.25)`; fx.fillRect(Math.random()*256, Math.random()*256, 2, 2); }
-  // Subtle tile grid
-  fx.strokeStyle = 'rgba(80,80,88,0.15)'; fx.lineWidth = 1;
-  for (let i = 0; i <= 4; i++) { fx.beginPath(); fx.moveTo(i*64,0); fx.lineTo(i*64,256); fx.stroke(); fx.beginPath(); fx.moveTo(0,i*64); fx.lineTo(256,i*64); fx.stroke(); }
-  const ft = srgb(new THREE.CanvasTexture(fc)); ft.wrapS = ft.wrapT = THREE.RepeatWrapping; ft.repeat.set(4, 4);
+  fx.fillStyle = '#9a7a52'; fx.fillRect(0, 0, 256, 512);
+  const planks = 4, pw = 256 / planks;
+  for (let p = 0; p < planks; p++) {
+    // Per-plank base tone variation
+    const base = 138 + (Math.random() * 26 - 13);
+    fx.fillStyle = `rgb(${base|0},${(base*0.79)|0},${(base*0.54)|0})`;
+    fx.fillRect(p * pw, 0, pw, 512);
+    // Lengthwise grain streaks
+    for (let s = 0; s < 36; s++) {
+      const gx = p * pw + Math.random() * pw;
+      fx.strokeStyle = `rgba(${(base*0.7)|0},${(base*0.55)|0},${(base*0.36)|0},${0.05 + Math.random() * 0.12})`;
+      fx.lineWidth = 0.5 + Math.random();
+      fx.beginPath(); fx.moveTo(gx, 0);
+      fx.bezierCurveTo(gx + (Math.random() - 0.5) * 6, 170, gx + (Math.random() - 0.5) * 6, 340, gx + (Math.random() - 0.5) * 4, 512);
+      fx.stroke();
+    }
+    // Hairline seam at plank edge
+    fx.strokeStyle = 'rgba(60,42,24,0.5)'; fx.lineWidth = 1;
+    fx.beginPath(); fx.moveTo(p * pw, 0); fx.lineTo(p * pw, 512); fx.stroke();
+  }
+  const ft = srgb(new THREE.CanvasTexture(fc)); ft.wrapS = ft.wrapT = THREE.RepeatWrapping; ft.repeat.set(3, 3);
   if (renderer.capabilities) ft.anisotropy = renderer.capabilities.getMaxAnisotropy();
-  const floorMesh = new THREE.Mesh(new THREE.PlaneGeometry(W, D), new THREE.MeshStandardMaterial({ map: ft, roughness: 0.34, metalness: 0.0, envMapIntensity: 0.7 }));
+  const floorMesh = new THREE.Mesh(new THREE.PlaneGeometry(W, D), new THREE.MeshStandardMaterial({ map: ft, roughness: 0.42, metalness: 0.0, envMapIntensity: 0.5 }));
   floorMesh.rotation.x = -Math.PI/2; floorMesh.receiveShadow = true; scene.add(floorMesh);
 
   // Ceiling
@@ -457,6 +479,14 @@ function buildLighting() {
   rim.position.set(0, CONFIG.room.height + 0.6, -CONFIG.room.depth / 2 - 1.2);
   rim.target.position.set(0, 1.2, 0); scene.add(rim); scene.add(rim.target);
 
+  // PICTURE LIGHT — a soft warm wash grazing the board wall from above, like a
+  // gallery picture light. No shadow (FPS-safe); wide penumbra so no hard hotspot.
+  const pic = new THREE.SpotLight(0xfff0d8, 0.55, 8, Math.PI * 0.42, 0.85, 1.2);
+  pic.position.set(0, CONFIG.room.height - 0.15, -CONFIG.room.depth / 2 + 1.0);
+  pic.target.position.set(0, 1.2, -CONFIG.room.depth / 2);
+  pic.castShadow = false;
+  scene.add(pic); scene.add(pic.target);
+
   // Light panel meshes (visual only, no light cost)
   const H = CONFIG.room.height;
   [[-1.5, -1.0], [1.5, -1.0], [-1.5, 1.5], [1.5, 1.5]].forEach(([x, z]) => {
@@ -489,7 +519,7 @@ function buildFurniture() {
     leg.position.set(-1.0+lx, 0.37, 1.5+lz); scene.add(leg);
   });
 
-  // 4 Chairs (simplified — just seat + back, shared material)
+  // 4 Chairs (simplified — leather-tone seat + back, shared material)
   [[-1.6,1.5,0],[-0.4,1.5,Math.PI],[-1.0,1.0,Math.PI/2],[-1.0,2.0,-Math.PI/2]].forEach(([cx,cz,rot]) => {
     const seat = new THREE.Mesh(new THREE.BoxGeometry(0.42, 0.04, 0.4), MAT.chair);
     seat.position.set(cx, 0.46, cz); seat.rotation.y = rot; scene.add(seat);
@@ -497,77 +527,60 @@ function buildFurniture() {
     back.position.set(cx - Math.sin(rot)*0.19, 0.68, cz - Math.cos(rot)*0.19); back.rotation.y = rot; scene.add(back);
   });
 
-  // TV on brick wall with dynamic slideshow
-  const tv = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.55, 0.9), MAT.dark);
-  tv.position.set(-W/2+0.06, 1.5, 0.5); scene.add(tv);
-  // TV bezel
-  const bezel = new THREE.Mesh(new THREE.BoxGeometry(0.01, 0.52, 0.87), new THREE.MeshLambertMaterial({ color: 0x1a1a1a }));
-  bezel.position.set(-W/2+0.075, 1.5, 0.5); scene.add(bezel);
-  // Screen canvas
-  const tvCanvas = document.createElement('canvas'); tvCanvas.width = 480; tvCanvas.height = 270;
-  const tvCtx = tvCanvas.getContext('2d');
-  const tvTex = new THREE.CanvasTexture(tvCanvas);
-  const scr = new THREE.Mesh(new THREE.PlaneGeometry(0.44, 0.78), new THREE.MeshBasicMaterial({ map: tvTex }));
-  scr.rotation.y = Math.PI/2; scr.position.set(-W/2+0.085, 1.5, 0.5); scene.add(scr);
-  // Store for slideshow updates
-  window._tvScreen = { canvas: tvCanvas, ctx: tvCtx, texture: tvTex, slideIdx: 0 };
-  drawTVSlide(0);
+  // (Removed: boxy TV + bezel + slideshow screen — the wallcoverings are the only
+  // loud thing in the room per the gallery art-direction.) The TV-slideshow helpers
+  // null-guard on window._tvScreen, so leaving it unset makes them no-op.
 
   // Bookshelves — near front wall, flanking doorway (don't block wing racks)
   buildBookShelf(-2.8, D/2-0.3, Math.PI, 1.2);
   buildBookShelf(2.8, D/2-0.3, Math.PI, 1.2);
 
-  // Rug under meeting table
-  const rugCanvas = document.createElement('canvas'); rugCanvas.width = 256; rugCanvas.height = 128;
-  const rc = rugCanvas.getContext('2d');
-  rc.fillStyle = '#4a3a2a'; rc.fillRect(0, 0, 256, 128);
-  // Border pattern
-  rc.strokeStyle = '#c9a96e'; rc.lineWidth = 6;
-  rc.strokeRect(8, 8, 240, 112);
-  rc.strokeStyle = '#8a7a5a'; rc.lineWidth = 2;
-  rc.strokeRect(16, 16, 224, 96);
-  // Center medallion
-  rc.fillStyle = '#6a5a3a'; rc.beginPath(); rc.ellipse(128, 64, 40, 25, 0, 0, Math.PI*2); rc.fill();
-  rc.strokeStyle = '#c9a96e'; rc.lineWidth = 1.5; rc.beginPath(); rc.ellipse(128, 64, 40, 25, 0, 0, Math.PI*2); rc.stroke();
-  const rugTex = srgb(new THREE.CanvasTexture(rugCanvas));
-  const rug = new THREE.Mesh(new THREE.PlaneGeometry(2.4, 1.4), new THREE.MeshStandardMaterial({ map: rugTex, roughness: 0.95, metalness: 0.0 }));
-  rug.rotation.x = -Math.PI/2; rug.position.set(-1.0, 0.005, 1.5); scene.add(rug);
-
-  // Potted plants — 2 corners
-  [[-W/2+0.4, D/2-0.4], [W/2-0.4, -D/2+0.4]].forEach(([px, pz]) => {
-    // Pot
-    const pot = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.08, 0.2, 16), new THREE.MeshStandardMaterial({ color: 0x8b5e3c, roughness: 0.85, metalness: 0.0 }));
-    pot.position.set(px, 0.1, pz); scene.add(pot);
-    // Soil
-    const soil = new THREE.Mesh(new THREE.CylinderGeometry(0.09, 0.09, 0.02, 16), new THREE.MeshStandardMaterial({ color: 0x3a2a1a, roughness: 1.0, metalness: 0.0 }));
-    soil.position.set(px, 0.21, pz); scene.add(soil);
-    // Foliage (sphere cluster)
-    const foliageMat = new THREE.MeshStandardMaterial({ color: 0x2d5a1e, roughness: 0.9, metalness: 0.0 });
-    const f1 = new THREE.Mesh(new THREE.SphereGeometry(0.15, 12, 9), foliageMat);
-    f1.position.set(px, 0.45, pz); scene.add(f1);
-    const f2 = new THREE.Mesh(new THREE.SphereGeometry(0.1, 10, 7), foliageMat);
-    f2.position.set(px+0.08, 0.55, pz-0.05); scene.add(f2);
+  // (Removed: procedural medallion rug — clean warm-oak floor reads more gallery.)
+
+  // ONE restrained olive in a honed-stone planter — quiet, expensive, off to the side
+  // by the left wall. Replaces the two sphere "toy" plants.
+  buildOliveTree(-W/2 + 0.55, 0, D/2 - 0.55);
+
+  // (Removed: framed abstract "art" on the right wall — the sample boards are the art.)
+}
+
+// A single olive/fig in a square honed-stone planter — soft layered canopy (not a
+// hard sphere), slim trunk, restrained. Built from a handful of low-poly meshes.
+function buildOliveTree(px, py, pz) {
+  const g = new THREE.Group();
+  g.position.set(px, py, pz);
+
+  // Square honed-stone planter (warm stone, matte)
+  const stoneMat = new THREE.MeshStandardMaterial({ color: 0xcfc7b8, roughness: 0.85, metalness: 0.0, envMapIntensity: 0.3 });
+  const planter = new THREE.Mesh(new THREE.BoxGeometry(0.34, 0.34, 0.34), stoneMat);
+  planter.position.set(0, 0.17, 0); planter.castShadow = true; planter.receiveShadow = true; g.add(planter);
+  // Soil cap
+  const soil = new THREE.Mesh(new THREE.BoxGeometry(0.30, 0.02, 0.30), new THREE.MeshStandardMaterial({ color: 0x2e261c, roughness: 1.0, metalness: 0.0 }));
+  soil.position.set(0, 0.345, 0); g.add(soil);
+
+  // Slim trunk
+  const trunkMat = new THREE.MeshStandardMaterial({ color: 0x6e5c45, roughness: 0.9, metalness: 0.0 });
+  const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.022, 0.03, 0.5, 7), trunkMat);
+  trunk.position.set(0, 0.6, 0); trunk.castShadow = true; g.add(trunk);
+
+  // Soft olive canopy — several low-poly tufts at varied scale = a layered, organic
+  // silhouette rather than one cartoon ball. Muted grey-green (olive foliage).
+  const leafMat = new THREE.MeshStandardMaterial({ color: 0x6f7a5a, roughness: 0.92, metalness: 0.0, envMapIntensity: 0.25 });
+  const tufts = [
+    [0.00, 0.92, 0.00, 0.20],
+    [0.11, 0.99, 0.04, 0.13],
+    [-0.10, 0.97, -0.05, 0.12],
+    [0.04, 1.08, -0.06, 0.11],
+    [-0.05, 1.05, 0.08, 0.10]
+  ];
+  tufts.forEach(([tx, ty, tz, r]) => {
+    const tuft = new THREE.Mesh(new THREE.IcosahedronGeometry(r, 0), leafMat);
+    tuft.position.set(tx, ty, tz);
+    tuft.rotation.set(Math.random(), Math.random(), Math.random());
+    tuft.castShadow = true; g.add(tuft);
   });
 
-  // Framed art on right wall
-  const artCanvas = document.createElement('canvas'); artCanvas.width = 256; artCanvas.height = 192;
-  const ax = artCanvas.getContext('2d');
-  // Generate an abstract wallcovering pattern
-  ax.fillStyle = '#1a2744'; ax.fillRect(0, 0, 256, 192);
-  ax.globalAlpha = 0.3; ax.fillStyle = '#c9a96e';
-  for (let y = 0; y < 6; y++) for (let x = 0; x < 4; x++) {
-    const cx = x*64+32+(y%2?32:0), cy = y*32+16;
-    ax.beginPath(); ax.moveTo(cx, cy-12); ax.quadraticCurveTo(cx+14, cy, cx, cy+12);
-    ax.quadraticCurveTo(cx-14, cy, cx, cy-12); ax.fill();
-  }
-  ax.globalAlpha = 1;
-  // Frame border
-  ax.strokeStyle = '#c9a96e'; ax.lineWidth = 4; ax.strokeRect(2, 2, 252, 188);
-  const artTex = new THREE.CanvasTexture(artCanvas);
-  const artFrame = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.55, 0.72), MAT.frame);
-  artFrame.position.set(W/2-0.04, 1.5, -0.5); scene.add(artFrame);
-  const artFace = new THREE.Mesh(new THREE.PlaneGeometry(0.5, 0.65), new THREE.MeshBasicMaterial({ map: artTex }));
-  artFace.rotation.y = -Math.PI/2; artFace.position.set(W/2-0.06, 1.5, -0.5); scene.add(artFace);
+  scene.add(g);
 }
 
 function buildBookShelf(x, z, rot, unitW) {
@@ -793,6 +806,43 @@ function buildWingWall() {
     planeR.userData = { isWingFace: true, parentPivot: pivot, leaf: 'R' };
     leafR.add(planeR); pivot.add(leafR);
 
+    // Thin brushed-brass surround behind the board (stays put when leaves fan open).
+    // Four slim bars frame the 30" board face like a gallery mount.
+    const frameT = 0.018, frameD = 0.01, frameZ = -THICK / 2 - 0.004;
+    const fpad = 0.012; // frame sits just outside the board edges
+    const fw = wingW + fpad * 2, fh = H + fpad * 2;
+    const brassBar = (w, h, x, y) => {
+      const bar = new THREE.Mesh(new THREE.BoxGeometry(w, h, frameD), MAT.chrome);
+      bar.position.set(x, yC + y, frameZ); bar.castShadow = false; bar.receiveShadow = true;
+      pivot.add(bar);
+    };
+    brassBar(fw, frameT, 0,  fh / 2 - frameT / 2);   // top
+    brassBar(fw, frameT, 0, -fh / 2 + frameT / 2);   // bottom
+    brassBar(frameT, fh, -fw / 2 + frameT / 2, 0);   // left
+    brassBar(frameT, fh,  fw / 2 - frameT / 2, 0);   // right
+
+    // Gallery placard — engraved-look brass-on-dark plate under the board.
+    const pName = (product.pattern_name || product.name || 'Pattern').toString();
+    const pColor = (product.color_name || product.color || '').toString();
+    const plc = document.createElement('canvas'); plc.width = 256; plc.height = 64;
+    const pcx = plc.getContext('2d');
+    pcx.fillStyle = '#1c1c22'; pcx.fillRect(0, 0, 256, 64);
+    pcx.strokeStyle = 'rgba(201,169,110,0.5)'; pcx.lineWidth = 2; pcx.strokeRect(3, 3, 250, 58);
+    pcx.fillStyle = '#c9a96e'; pcx.textAlign = 'center';
+    pcx.font = '600 22px Georgia, serif';
+    pcx.fillText(pName.length > 22 ? pName.slice(0, 21) + '…' : pName, 128, 28);
+    if (pColor) {
+      pcx.fillStyle = '#9a8c6e'; pcx.font = '12px sans-serif';
+      pcx.fillText(pColor.toUpperCase().slice(0, 30), 128, 48);
+    }
+    const plTex = new THREE.CanvasTexture(plc);
+    const placard = new THREE.Mesh(
+      new THREE.PlaneGeometry(Math.min(wingW * 0.9, 0.32), 0.075),
+      new THREE.MeshBasicMaterial({ map: plTex, transparent: true })
+    );
+    placard.position.set(0, 0.115, 0.012);
+    pivot.add(placard);
+
     const wingImg = product.tile || product.image;
 
     let widthInches = 27;

← 5921951 Boards 30"×6ft (real sample wings, 8/wall + paging); fan-out  ·  back to Quadrille Showroom  ·  Clean+clear pass: bright even gallery lighting, true-colour 15f8c33 →