[object Object]

← back to Quadrille Showroom

V10 Concierge Nook: boards now genuinely LIE on the table — share the table's exact perspective + 58deg rake + vanishing point (coplanar with the wood), counter-rotate ONLY the placard so labels stay readable, in-plane foreshortened cast shadows seat each board on the felt; placard color dot grounded on enrichment hue

27c72af4fce25ec0ecd1efbe71974725520f2f7b · 2026-06-28 09:35:31 -0700 · Steve

Files touched

Diff

commit 27c72af4fce25ec0ecd1efbe71974725520f2f7b
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Jun 28 09:35:31 2026 -0700

    V10 Concierge Nook: boards now genuinely LIE on the table — share the table's exact perspective + 58deg rake + vanishing point (coplanar with the wood), counter-rotate ONLY the placard so labels stay readable, in-plane foreshortened cast shadows seat each board on the felt; placard color dot grounded on enrichment hue
---
 public/proto/shots/v10-concierge.png | Bin 0 -> 295960 bytes
 public/proto/v10-concierge.html      |  81 +++++++++++++++++++++++++++++------
 2 files changed, 68 insertions(+), 13 deletions(-)

diff --git a/public/proto/shots/v10-concierge.png b/public/proto/shots/v10-concierge.png
new file mode 100644
index 0000000..4aa48cb
Binary files /dev/null and b/public/proto/shots/v10-concierge.png differ
diff --git a/public/proto/v10-concierge.html b/public/proto/v10-concierge.html
index db1089b..c4d63ad 100644
--- a/public/proto/v10-concierge.html
+++ b/public/proto/v10-concierge.html
@@ -148,35 +148,73 @@
     border-radius:50%; width:46px; height:46px; font-size:18px; flex:0 0 auto; transition:.18s; }
   .ask button:hover{ background:var(--brass); }
 
-  /* ===== Boards gliding onto the table ===== */
+  /* ===== Boards LYING ON the table (CONTRARIAN FIX) =====
+     The table surface is rotateX(58deg). The boards now share that same 3D
+     perspective + vanishing point — they're raked back ONTO the felt (rotateX
+     ~46deg, a touch flatter than the table so the placard stays readable), laid
+     out in receding rows that get smaller/dimmer toward the back, each casting a
+     contact shadow on the table. They are physical sample boards on a table, not
+     flat cards floating above a table photo. */
+  /* The stage shares the ROOM's exact perspective context so the boards converge
+     to the SAME vanishing point as the table (perspective 1400px, origin 50% 38%). */
   .stage{ position:fixed; left:380px; right:0; top:0; bottom:0; z-index:10;
-    display:flex; align-items:center; justify-content:center; pointer-events:none; }
+    display:flex; align-items:flex-end; justify-content:center; pointer-events:none;
+    perspective:1400px; perspective-origin:50% 38%; }
   .boards{
     pointer-events:auto;
-    display:flex; gap:30px; flex-wrap:wrap; justify-content:center; align-content:center;
-    max-width:1100px; max-height:88vh; overflow-y:auto; padding:40px 30px 20px;
-    perspective:1200px;
+    display:flex; gap:30px 28px; flex-wrap:wrap; justify-content:center; align-content:flex-end;
+    /* clamped to roughly the felt-runner width so the boards sit ON the mat */
+    width:min(760px,82%); max-height:150vh; overflow-y:auto;
+    padding:9vh 30px 18vh;
+    transform-style:preserve-3d;
+    /* lay the tableau onto the table's ACTUAL plane: same 58deg rake, anchored to
+       the near table edge → genuinely coplanar with the wood, one shared VP. */
+    transform: rotateX(58deg);
+    transform-origin:50% 100%;
+    /* far rows fade into the table's shadow toward the vanishing point */
+    -webkit-mask-image:linear-gradient(180deg, transparent 0%, rgba(0,0,0,.5) 8%, #000 22%, #000 100%);
+            mask-image:linear-gradient(180deg, transparent 0%, rgba(0,0,0,.5) 8%, #000 22%, #000 100%);
   }
   .boards::-webkit-scrollbar{width:8px} .boards::-webkit-scrollbar-thumb{background:rgba(201,169,110,.4);border-radius:4px}
 
   .board{
-    width:210px; flex:0 0 auto;
+    width:200px; flex:0 0 auto;
+    transform-style:preserve-3d;
     transform-origin:50% 100%;
-    animation:glide .7s cubic-bezier(.18,.8,.28,1) backwards;
+    animation:glide .75s cubic-bezier(.18,.8,.28,1) backwards;
     cursor:pointer;
   }
+  /* boards slide IN from the near edge (toward the viewer) and settle onto the felt */
   @keyframes glide{
-    0%{ opacity:0; transform:translateY(70px) rotateX(28deg) scale(.86); }
-    100%{ opacity:1; transform:translateY(0) rotateX(0) scale(1); }
+    0%{ opacity:0; transform:translateZ(130px) scale(1.05); }
+    100%{ opacity:1; transform:translateZ(0) scale(1); }
   }
-  /* brass-framed sample board with gallery placard */
+  /* brass frame lies FLAT on the felt (coplanar with the table). Its cast shadow is
+     a separate in-plane element (.board::after) — NOT a screen-space drop shadow. */
   .board .frame{
     position:relative; border-radius:4px;
     padding:9px; background:linear-gradient(155deg,#d8c79c,var(--brass) 55%,#7e6526);
-    box-shadow:0 22px 44px rgba(0,0,0,.5), 0 2px 0 rgba(255,255,255,.3) inset;
+    box-shadow:0 1px 0 rgba(255,255,255,.3) inset, 0 0 0 1px rgba(0,0,0,.25);
     transition:transform .35s cubic-bezier(.2,.7,.3,1), box-shadow .35s;
   }
-  .board:hover .frame{ transform:translateY(-10px) rotateX(-4deg); box-shadow:0 34px 60px rgba(0,0,0,.6); }
+  /* IN-PLANE cast shadow: a dark, blurred quad lying on the felt, offset toward the
+     vanishing point (up-screen = "back" on the raked plane) and foreshortened. */
+  .board .frame::before{
+    content:""; position:absolute; inset:0; z-index:-1; border-radius:6px;
+    background:rgba(0,0,0,.5); filter:blur(14px);
+    transform:translateY(-10%) scaleY(.82);   /* push back + foreshorten in-plane */
+  }
+  /* CONTRARIAN FIX — counter-rotate ONLY the placard back toward the viewer so the
+     label tips up and stays readable while the swatch lies flat on the table, just
+     like a real tented sample-board placard. */
+  .board .placard{
+    transform: rotateX(-46deg);
+    transform-origin:50% 0%;
+    box-shadow:0 10px 22px -6px rgba(0,0,0,.55);
+  }
+  /* hover = lift the whole board OFF the table toward the viewer (pick it up) */
+  .board:hover{ transform:translateZ(60px) rotateX(-30deg); z-index:5; }
+  .board:hover .frame::before{ background:rgba(0,0,0,.6); filter:blur(20px); transform:translateY(6%) scaleY(.9); }
   .board .swatch{
     width:100%; aspect-ratio:1/1; border-radius:2px; object-fit:cover;
     background:var(--stone); display:block; box-shadow:0 0 0 1px rgba(0,0,0,.25) inset;
@@ -307,6 +345,8 @@
    absolute backend URL is kept only for opening the file standalone.) */
 const API = '/api/showroom/products?limit=200';
 const API_FALLBACK = 'http://127.0.0.1:7690/api/showroom/products?limit=200';
+const ENRICH_URL = './enrichment.json';   // REAL hue / color_bucket / family per sku
+let ENRICH = {};
 let PRODUCTS = [];
 
 /* ---- Color lexicon: word -> {bucket, hex (for the chip swatch)} ---- */
@@ -572,8 +612,20 @@ function renderTray(){
   });
 }
 
-/* pick a representative swatch hex for a product from its color words */
+/* hsl(h 0-360,s,l) -> hex */
+function hslHex(h,s,l){ h=((h%360)+360)%360/360;
+  const f=n=>{const k=(n+h*12)%12,a=s*Math.min(l,1-l);
+    return l-a*Math.max(-1,Math.min(k-3,9-k,1));};
+  const to=v=>('0'+Math.round(Math.max(0,Math.min(1,v))*255).toString(16)).slice(-2);
+  return '#'+to(f(0))+to(f(8))+to(f(4)); }
+/* CONTRARIAN/enrichment — the placard dot is now grounded on the REAL enrichment
+   hue (pixel-derived upstream), not a guess from a colour word in the name. */
 function swatchHex(p){
+  const e=ENRICH[p.sku];
+  if(e && typeof e.hue==='number'){
+    const muted=e.color_bucket==null||e.color_bucket==='neutral'||e.color_bucket==='grey';
+    return hslHex(e.hue, muted?0.12:0.5, 0.5);
+  }
   const n=(p.pattern_name||'').toLowerCase();
   for(const [w,[,hex]] of Object.entries(COLOR_WORDS)){ if(n.includes(w)) return hex; }
   return '#cfc7b8';
@@ -631,6 +683,9 @@ async function boot(){
   buildChips();
   addMsg('assist', `Welcome — make yourself comfortable. I'm your design concierge for the
     <em>China Seas</em> atelier. Tell me a colour, a mood, or a pattern you love, and I'll pull the boards.`);
+  // load REAL enrichment first so the placard dots are grounded, not name-guessed
+  try{ const er=await fetch(ENRICH_URL); const ej=await er.json();
+       ENRICH=(ej&&ej.enrich)?ej.enrich:(ej||{}); }catch(_){ ENRICH={}; }
   try{
     let r;
     try{ r=await fetch(API); if(!r.ok) throw new Error(r.status); }

← cbee4bb V9 Walk-In Room: photo-real depth — soft contact shadows gro  ·  back to Quadrille Showroom  ·  Proto V6-V10: refresh final verification screenshots (0 cons 765e004 →