[object Object]

← back to Quadrille Showroom

Senior-first Guided Mode: big no-keyboard nav (Prev/Next/Auto Tour/All Designs), Explore toggle off by default, large title+instruction

2b0c269a536f47ef789d8cc24bbc85a2559690a1 · 2026-06-27 17:19:40 -0700 · Steve

Files touched

Diff

commit 2b0c269a536f47ef789d8cc24bbc85a2559690a1
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Jun 27 17:19:40 2026 -0700

    Senior-first Guided Mode: big no-keyboard nav (Prev/Next/Auto Tour/All Designs), Explore toggle off by default, large title+instruction
---
 public/js/showroom.js  | 205 +++++++++++++++++++++++++++++++++++++++++++++++--
 public/showroom.html   | 164 +++++++++++++++++++++++++++++++++++++--
 scripts/test-senior.js | 175 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 530 insertions(+), 14 deletions(-)

diff --git a/public/js/showroom.js b/public/js/showroom.js
index c8e3fec..9196d1f 100644
--- a/public/js/showroom.js
+++ b/public/js/showroom.js
@@ -58,6 +58,14 @@ let TEXTURE_POOL = [];
 const keysDown = {};
 const WALK_SPEED = 2.5; // meters per second
 
+// GUIDED MODE (DTD verdict A, 2026-06-27) — senior-first default. The boot
+// experience is GUIDED: camera flies to the middle board open, big bottom-centre
+// Previous/Next/Auto-Tour buttons drive everything, NO keyboard + NO 3D walking
+// required. Free-walk (WASD/orbit) is demoted behind an 'Explore' toggle that is
+// OFF by default. exploreMode gates all walking/orbit/proximity input.
+let exploreMode = (localStorage.getItem('qh_explore') === '1');
+let guidedReady = false; // set true once the middle board has been auto-focused on load
+
 // ============================================================
 // INIT
 // ============================================================
@@ -98,6 +106,11 @@ function init() {
   controls.maxDistance = 8.0;
   controls.target.set(0, 1.2, -0.5);
   controls.panSpeed = 0.5;
+  // GUIDED MODE default: orbit/pan/zoom OFF unless Explore is on. applyExploreMode()
+  // is the single source of truth for this; called again after init() below.
+  controls.enableRotate = exploreMode;
+  controls.enablePan = exploreMode;
+  controls.enableZoom = exploreMode;
 
   updateLoadStatus('Building materials...', 10);
   initMaterials();
@@ -128,11 +141,13 @@ function init() {
     } else if (e.code === 'KeyB') {
       e.preventDefault(); toggleProximity();
     } else if (e.code === 'Escape') {
-      if (peruseActive) stopPeruse();
-      else if (focusedWing) unfocusWing();
+      const gridOpen = document.getElementById('grid-overlay');
+      if (gridOpen && gridOpen.classList.contains('open')) { closeAllDesignsGrid(); return; }
+      if (peruseActive) { stopPeruse(exploreMode ? false : true); if (!exploreMode) enterGuidedDefault(); }
+      else if (focusedWing) { unfocusWing(); if (!exploreMode) enterGuidedDefault(); } // guided mode never lands on an empty room
     }
-    // Manual walking interrupts an active peruse
-    if (peruseActive && (e.code.indexOf('Arrow') === 0 || ['KeyW','KeyA','KeyS','KeyD'].indexOf(e.code) >= 0)) stopPeruse();
+    // Manual walking interrupts an active peruse (only when Explore is on)
+    if (exploreMode && peruseActive && (e.code.indexOf('Arrow') === 0 || ['KeyW','KeyA','KeyS','KeyD'].indexOf(e.code) >= 0)) stopPeruse();
   });
   window.addEventListener('keyup', e => { keysDown[e.code] = false; });
   initHUD();
@@ -808,10 +823,14 @@ async function loadProducts() {
   updateLoadStatus('Ready!', 100);
   setTimeout(() => {
     document.getElementById('loading-screen').classList.add('fade-out');
+    // GUIDED MODE default (DTD verdict A): unless the user has switched to Explore,
+    // fly to the middle board open so the big Previous/Next/Auto-Tour buttons drive
+    // everything from the first second — no keyboard, no walking.
+    setTimeout(() => { if (!exploreMode) enterGuidedDefault(); }, 700);
     setTimeout(() => {
       const tip = document.getElementById('welcome-tip');
       if (tip) tip.classList.add('fade-out');
-    }, 6000);
+    }, 9000);
   }, 400);
   populateVendorSidebar();
   startTVSlideshow();
@@ -1466,6 +1485,7 @@ function unfocusWing(skipPanel) {
   if (!skipPanel) revertWalls();
   if (!skipPanel) document.getElementById('wing-detail').classList.add('hidden');
   if (!skipPanel) hideNowViewing();   // tuck the navigator strip away on a true exit
+  if (!skipPanel) hideGuidedTitle();  // and the big top-centre title
 
   // Unlock first so smoothCameraTo can move
   unlockControls();
@@ -1546,6 +1566,8 @@ function showWingDetail(product) {
 
   // Feed the NOW VIEWING media-player strip in lock-step with the detail card.
   updateNowViewing(product);
+  // GUIDED MODE: large readable title + N-of-N up top, for senior legibility.
+  updateGuidedTitle(product);
 }
 
 // NOW VIEWING navigator — populate the museum media-player strip from the focused
@@ -1654,6 +1676,7 @@ function onCanvasMouseMove(event) {
 // WASD WALKING — first-person navigation
 // ============================================================
 function updateWASD(dt) {
+  if (!exploreMode) return;                  // GUIDED MODE: keyboard walking is off unless Explore is on
   if (controlsLocked || cameraAnim) return; // no walking when focused on wing or animating
 
   let moveX = 0, moveZ = 0;
@@ -1714,7 +1737,7 @@ const _bookWP = new THREE.Vector3();
 function updateBooks(dt) {
   if (!wingBoards.length) return;
   const backZ = -CONFIG.room.depth / 2 + 0.2;
-  const interactive = proximityEnabled && !controlsLocked && !peruseActive && !cameraAnim;
+  const interactive = exploreMode && proximityEnabled && !controlsLocked && !peruseActive && !cameraAnim;
 
   // Find the single wing nearest the camera, but only once we're close to the wall.
   let nearest = null;
@@ -1910,6 +1933,157 @@ function setPeruseBtn(on) {
     b.innerHTML = on ? '■ Stop Perusing' : '▷ Peruse Collection';
   }
   syncNowViewingPlay();   // keep the navigator strip's play glyph in lock-step
+  syncGuidedTourBtn();    // keep the big Auto-Tour button in lock-step
+}
+
+// ============================================================
+// GUIDED MODE — senior-first navigation (DTD verdict A, 2026-06-27)
+// ============================================================
+
+// Apply the Explore on/off state everywhere: orbit controls, the toggle button
+// label/look, and the bottom instruction. OFF (default) = guided, no keyboard.
+function applyExploreMode() {
+  if (controls && !controlsLocked) {
+    controls.enableRotate = exploreMode;
+    controls.enablePan = exploreMode;
+    controls.enableZoom = exploreMode;
+  }
+  const btn = document.getElementById('btn-explore');
+  if (btn) {
+    btn.classList.toggle('active', exploreMode);
+    btn.textContent = exploreMode ? 'Explore: On' : 'Explore: Off';
+    btn.title = exploreMode
+      ? 'Free walking is ON — use W A S D to walk and drag to look around. Tap to turn off.'
+      : 'Turn on free walking with the keyboard (W A S D). Off by default.';
+  }
+  const bm = document.getElementById('btn-bookmatch');
+  if (bm) bm.style.display = exploreMode ? '' : 'none';   // walk-up book-match is an explore-only affordance
+  // Body class drives which nav surface shows: guided (big bar + title) vs explore
+  // (the compact Now-Viewing strip + bottom HUD). They share screen real estate, so
+  // only one set is visible at a time. The big detail card stays in both.
+  document.body.classList.toggle('explore-mode', exploreMode);
+  document.body.classList.toggle('guided-mode', !exploreMode);
+  updateGuidedInstruction();
+}
+
+function toggleExplore() {
+  exploreMode = !exploreMode;
+  localStorage.setItem('qh_explore', exploreMode ? '1' : '0');
+  applyExploreMode();
+  if (exploreMode) {
+    // Stepping into Explore: release any guided focus so the user can walk freely.
+    if (peruseActive) stopPeruse(true);
+    if (focusedWing) unfocusWing();
+    const t = document.getElementById('info-text');
+    if (t) t.textContent = 'Explore mode — W A S D to walk, drag to look around. Click a board to view it.';
+  } else {
+    // Back to guided: refly to a board so the big buttons have something to drive.
+    enterGuidedDefault();
+  }
+}
+
+function updateGuidedInstruction() {
+  const el = document.getElementById('guided-instruction');
+  if (!el) return;
+  if (peruseActive) {
+    el.innerHTML = 'Watching all designs&hellip; tap <strong>Pause</strong> to stop, or <strong>Next</strong> to step ahead.';
+  } else if (exploreMode) {
+    el.innerHTML = 'Explore mode: use <strong>W A S D</strong> to walk, or use the big buttons below.';
+  } else {
+    el.innerHTML = 'Tap <strong>Next</strong> to see the next design &mdash; or <strong>Auto Tour</strong> to watch them all.';
+  }
+}
+
+// Large readable pattern title (top-centre) + N-of-N position. Driven from the
+// focused board so it stays in lock-step with Now Viewing + the detail card.
+function updateGuidedTitle(product) {
+  const wrap = document.getElementById('guided-title');
+  if (!wrap) return;
+  if (!product) { wrap.classList.add('hidden'); return; }
+  wrap.classList.remove('hidden');
+  const { pattern, colorway } = splitPatternColor(product.pattern_name || product.name);
+  const nm = document.getElementById('gt-name');
+  if (nm) nm.textContent = pattern || 'Pattern';
+  const cw = product.color || colorway || product.collection || '';
+  const colEl = document.getElementById('gt-color');
+  const dot = document.getElementById('gt-dot');
+  if (colEl) colEl.textContent = cw;
+  const idx = (typeof windowOffset === 'number' ? windowOffset : 0) +
+              (focusedWing && focusedWing.userData ? focusedWing.userData.index : 0) + 1;
+  const total = windowTotal || products.length || 0;
+  const posEl = document.getElementById('gt-pos');
+  if (posEl) posEl.textContent = total ? ('Design ' + idx + ' of ' + total) : '';
+  if (dot) dot.style.display = (cw && total) ? '' : 'none';
+}
+
+function hideGuidedTitle() {
+  const wrap = document.getElementById('guided-title');
+  if (wrap) wrap.classList.add('hidden');
+}
+
+// Keep the big Auto-Tour button glyph/label + state in sync with Peruse.
+function syncGuidedTourBtn() {
+  const b = document.getElementById('g-tour');
+  if (!b) return;
+  b.classList.toggle('playing', !!peruseActive);
+  b.innerHTML = peruseActive ? '❚❚ Pause' : '▷ Auto Tour';
+  b.title = peruseActive ? 'Pause the tour' : 'Watch every design hands-free';
+  updateGuidedInstruction();
+}
+
+// On load (and when leaving Explore), fly to the MIDDLE board, open it, and present
+// its design — so the big Previous/Next/Auto-Tour buttons immediately have a hero.
+function enterGuidedDefault() {
+  if (!wingBoards.length) return;
+  const mid = Math.floor(wingBoards.length / 2);
+  restoreBoards();
+  focusOnWing(wingBoards[mid]);
+  guidedReady = true;
+}
+
+// ALL DESIGNS — big-thumbnail grid overlay. Builds from the live window products;
+// tapping a card closes the overlay and flies to that board. Familiar web-grid
+// browse path for users who find spatial navigation hard.
+function buildAllDesignsGrid() {
+  const host = document.getElementById('grid-items');
+  if (!host) return;
+  host.innerHTML = '';
+  const list = products.slice(0, windowSize);
+  list.forEach((p, i) => {
+    const card = document.createElement('div');
+    card.className = 'go-card';
+    const img = document.createElement('img');
+    img.className = 'go-img';
+    img.loading = 'lazy';
+    const src = p.tile || p.image || p.room;
+    if (src) img.src = src.charAt(0) === '/' ? src : ('/api/proxy/image?url=' + encodeURIComponent(src));
+    img.alt = (p.pattern_name || p.name || 'Design');
+    const cap = document.createElement('div'); cap.className = 'go-cap';
+    const { pattern, colorway } = splitPatternColor(p.pattern_name || p.name);
+    const pat = document.createElement('div'); pat.className = 'go-pat'; pat.textContent = pattern || 'Pattern';
+    const col = document.createElement('div'); col.className = 'go-col';
+    col.textContent = p.color_name || p.color || colorway || '';
+    cap.appendChild(pat); cap.appendChild(col);
+    card.appendChild(img); card.appendChild(cap);
+    card.addEventListener('click', () => {
+      closeAllDesignsGrid();
+      if (peruseActive) stopPeruse(true);
+      restoreBoards();
+      if (wingBoards[i]) focusOnWing(wingBoards[i]);
+    });
+    host.appendChild(card);
+  });
+}
+
+function openAllDesignsGrid() {
+  if (peruseActive) stopPeruse(true);
+  buildAllDesignsGrid();
+  const ov = document.getElementById('grid-overlay');
+  if (ov) ov.classList.add('open');
+}
+function closeAllDesignsGrid() {
+  const ov = document.getElementById('grid-overlay');
+  if (ov) ov.classList.remove('open');
 }
 
 // ============================================================
@@ -2017,6 +2191,25 @@ function initHUD() {
     stopPeruse(); windowSize = parseInt(dens.value) || 50; await fetchWindow(0); rebuildWingWall();
   });
 
+  // ---- GUIDED MODE wiring (senior-first big buttons) ----
+  const gPrev = document.getElementById('g-prev');
+  const gNext = document.getElementById('g-next');
+  const gTour = document.getElementById('g-tour');
+  const gGrid = document.getElementById('g-grid');
+  if (gPrev) gPrev.addEventListener('click', () => { stopPeruse(true); gotoBoardOffset(-1); });
+  if (gNext) gNext.addEventListener('click', () => { stopPeruse(true); gotoBoardOffset(1); });
+  if (gTour) gTour.addEventListener('click', () => togglePeruse());
+  if (gGrid) gGrid.addEventListener('click', () => openAllDesignsGrid());
+
+  const goClose = document.getElementById('go-close');
+  if (goClose) goClose.addEventListener('click', () => closeAllDesignsGrid());
+
+  const exp = document.getElementById('btn-explore');
+  if (exp) exp.addEventListener('click', () => toggleExplore());
+
+  // Reflect the persisted Explore state on the controls + toggle button now.
+  applyExploreMode();
+
   // Mobile d-pad → synthesized WASD
   document.querySelectorAll('#dpad button').forEach(btn => {
     const code = { f:'KeyW', b:'KeyS', l:'KeyA', r:'KeyD' }[btn.dataset.dir];
diff --git a/public/showroom.html b/public/showroom.html
index f2f5ef0..0440a43 100644
--- a/public/showroom.html
+++ b/public/showroom.html
@@ -9,6 +9,128 @@
   <style>
     /* ---- Quadrille House additions (peruse, window controls, mobile d-pad) ---- */
     :root { --qh-gold:#c9a96e; }
+
+    /* ============================================================
+       GUIDED MODE — senior-first primary navigation (DTD verdict A)
+       Big, high-contrast, word-labelled controls at bottom-centre so a
+       65-year-old needs NO keyboard and NO 3D walking. Tap targets >=56px tall.
+       ============================================================ */
+    #guided-bar {
+      position:fixed; left:50%; bottom:22px; transform:translateX(-50%);
+      z-index:40; display:flex; align-items:center; gap:14px;
+      background:rgba(12,12,17,0.90); border:1.5px solid rgba(201,169,110,0.45);
+      border-radius:18px; padding:12px 16px; backdrop-filter:blur(14px);
+      box-shadow:0 18px 50px -12px rgba(0,0,0,0.75);
+    }
+    #guided-bar.hidden { display:none; }
+    .g-btn {
+      min-height:60px; min-width:140px; padding:0 24px;
+      display:inline-flex; align-items:center; justify-content:center; gap:8px;
+      font-size:20px; font-weight:700; letter-spacing:.5px; cursor:pointer;
+      border-radius:12px; border:2px solid rgba(201,169,110,0.55);
+      background:rgba(255,255,255,0.06); color:#f0e6cf;
+      transition:background .15s, transform .1s, border-color .15s;
+      -webkit-user-select:none; user-select:none;
+    }
+    .g-btn:hover { background:rgba(201,169,110,0.22); border-color:#c9a96e; }
+    .g-btn:active { transform:translateY(1px); }
+    .g-btn:disabled { opacity:.4; cursor:default; }
+    /* The big primary Auto-Tour button — brass fill so it's the obvious hero action */
+    #g-tour {
+      min-width:200px; font-size:21px;
+      background:linear-gradient(180deg,#d8bd84,#b9933f); color:#1a1206;
+      border-color:#b9933f;
+    }
+    #g-tour:hover { background:linear-gradient(180deg,#e3cb95,#c5a04a); }
+    #g-tour.playing { background:linear-gradient(180deg,#d06a5a,#a8392b); color:#fff; border-color:#a8392b; }
+    /* The 'All Designs' grid button */
+    #g-grid { min-width:130px; }
+    /* Always-on plain instruction strip above the guided bar */
+    #guided-instruction {
+      position:fixed; left:50%; bottom:96px; transform:translateX(-50%);
+      z-index:39; max-width:90vw; text-align:center;
+      font-size:18px; font-weight:600; letter-spacing:.3px; color:#f5efe2;
+      background:rgba(12,12,17,0.74); border:1px solid rgba(201,169,110,0.3);
+      padding:9px 22px; border-radius:24px; backdrop-filter:blur(8px);
+      text-shadow:0 1px 3px rgba(0,0,0,0.8);
+    }
+    #guided-instruction.hidden { display:none; }
+    /* Large readable pattern title shown top-centre in guided mode */
+    #guided-title {
+      position:fixed; left:50%; top:64px; transform:translateX(-50%);
+      z-index:38; text-align:center; pointer-events:none;
+      background:rgba(12,12,17,0.62); border:1px solid rgba(201,169,110,0.25);
+      border-radius:14px; padding:10px 28px; backdrop-filter:blur(8px);
+      box-shadow:0 8px 30px -10px rgba(0,0,0,0.6);
+    }
+    #guided-title.hidden { display:none; }
+    #guided-title .gt-name {
+      font-family:Georgia,'Times New Roman',serif; font-size:30px; font-weight:600;
+      color:#f5efdc; line-height:1.1; text-shadow:0 2px 6px rgba(0,0,0,0.7);
+    }
+    #guided-title .gt-sub {
+      font-size:16px; color:#d8cfba; margin-top:4px; letter-spacing:1px;
+      font-variant-numeric:tabular-nums;
+    }
+    /* Explore toggle — small, top-right, OFF by default (demotes WASD) */
+    #btn-explore {
+      font-size:12px; letter-spacing:1px; text-transform:uppercase;
+      padding:6px 14px; min-height:36px; border-radius:18px; cursor:pointer;
+      border:1px solid rgba(201,169,110,0.4); background:rgba(255,255,255,0.05);
+      color:#c9a96e; font-weight:600;
+    }
+    #btn-explore.active { background:rgba(201,169,110,0.85); color:#1a1206; border-color:#c9a96e; }
+
+    /* ALL DESIGNS — big-thumbnail grid overlay (even-simpler browse path) */
+    #grid-overlay {
+      position:fixed; inset:0; z-index:60; background:rgba(8,8,12,0.96);
+      display:none; flex-direction:column;
+    }
+    #grid-overlay.open { display:flex; }
+    .go-head {
+      display:flex; align-items:center; justify-content:space-between;
+      padding:18px 28px; border-bottom:1px solid rgba(201,169,110,0.2); flex-shrink:0;
+    }
+    .go-title { font-family:Georgia,serif; font-size:26px; color:#f0e6cf; letter-spacing:1px; }
+    .go-close {
+      min-height:52px; min-width:120px; font-size:18px; font-weight:700; cursor:pointer;
+      border-radius:12px; border:2px solid rgba(201,169,110,0.5);
+      background:rgba(201,169,110,0.12); color:#f0e6cf;
+    }
+    .go-close:hover { background:rgba(201,169,110,0.28); }
+    #grid-items {
+      flex:1; overflow-y:auto; padding:24px 28px;
+      display:grid; gap:20px;
+      grid-template-columns:repeat(auto-fill, minmax(220px, 1fr));
+      align-content:start;
+    }
+    .go-card {
+      cursor:pointer; border-radius:12px; overflow:hidden; background:#15151c;
+      border:1.5px solid rgba(201,169,110,0.22); transition:transform .12s, border-color .15s;
+    }
+    .go-card:hover { transform:translateY(-3px); border-color:#c9a96e; }
+    .go-card .go-img { width:100%; aspect-ratio:1/1.25; object-fit:cover; display:block; background:#1c1c24; }
+    .go-card .go-cap { padding:10px 12px; }
+    .go-card .go-pat { font-family:Georgia,serif; font-size:17px; color:#f0e6cf; line-height:1.2;
+      white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
+    .go-card .go-col { font-size:13px; color:#a7a299; margin-top:2px; letter-spacing:.5px; }
+
+    /* Which nav surface shows depends on mode. Guided (default) = big bar + title;
+       Explore = compact Now-Viewing strip + bottom HUD instruction. One at a time. */
+    body.guided-mode #now-viewing { display:none !important; }
+    body.guided-mode #bottom-bar #info-text { visibility:hidden; }       /* keep bar height + FPS, drop the keyboard hint */
+    body.guided-mode #minimap { display:none; }                          /* spatial minimap is an explore aid */
+    body.explore-mode #guided-bar,
+    body.explore-mode #guided-instruction,
+    body.explore-mode #guided-title { display:none !important; }
+    /* On small screens the big guided bar wraps; let it */
+    @media (max-width:720px){
+      #guided-bar { flex-wrap:wrap; width:92vw; justify-content:center; }
+      .g-btn { min-width:120px; font-size:17px; }
+      #g-tour { min-width:160px; }
+      #guided-title .gt-name { font-size:23px; }
+    }
+
     .loader-logo, .logo-mark { letter-spacing:.04em; }
     #btn-peruse {
       background:linear-gradient(180deg,#d8bd84,#b9933f); color:#1a1206; border:none;
@@ -55,7 +177,7 @@
     }
 </style>
 </head>
-<body>
+<body class="guided-mode">
   <!-- Loading Screen -->
   <div id="loading-screen">
     <div class="loader-content">
@@ -80,6 +202,7 @@
         <button class="nav-btn active" data-section="overview" title="Full View">Overview</button>
       </div>
       <div class="top-right">
+        <button id="btn-explore" title="Turn on free walking with the keyboard (W A S D). Off by default.">Explore: Off</button>
         <button id="btn-bookmatch" class="icon-btn active" title="Open board — wings open within 4 ft to present the full continuous design (B)">&#128214;</button>
         <button id="btn-walls" class="icon-btn active" title="Wallpaper the room walls on focus">&#9638;</button>
         <button id="btn-music" class="icon-btn" title="Listen to Music">&#9835;</button>
@@ -176,16 +299,41 @@
       <span id="info-text">WASD to walk up to a wing &middot; within 4 ft it opens to the full continuous design &middot; Click to inspect &middot; P to Peruse</span>
       <span id="fps-counter">60 FPS</span>
     </div>
+
+    <!-- GUIDED MODE — large readable pattern title (top-centre) -->
+    <div id="guided-title">
+      <div class="gt-name" id="gt-name">&nbsp;</div>
+      <div class="gt-sub"><span id="gt-color"></span><span id="gt-dot"> &middot; </span><span id="gt-pos"></span></div>
+    </div>
+
+    <!-- GUIDED MODE — always-visible one-line plain instruction -->
+    <div id="guided-instruction">Tap <strong>Next</strong> to see the next design &mdash; or <strong>Auto Tour</strong> to watch them all.</div>
+
+    <!-- GUIDED MODE — big senior-first control bar (no keyboard, no walking) -->
+    <div id="guided-bar">
+      <button class="g-btn" id="g-prev" title="See the previous design">&#9664; Previous</button>
+      <button class="g-btn" id="g-tour" title="Watch every design hands-free">&#9655; Auto Tour</button>
+      <button class="g-btn" id="g-next" title="See the next design">Next &#9654;</button>
+      <button class="g-btn" id="g-grid" title="See all designs as big pictures">&#9783; All Designs</button>
+    </div>
+  </div>
+
+  <!-- ALL DESIGNS — big-thumbnail grid overlay (even-simpler browse path) -->
+  <div id="grid-overlay">
+    <div class="go-head">
+      <span class="go-title">All Designs &mdash; tap a picture to view it</span>
+      <button class="go-close" id="go-close">&times; Close</button>
+    </div>
+    <div id="grid-items"></div>
   </div>
 
-  <!-- Welcome Tooltip -->
+  <!-- Welcome Tooltip — senior-first: explains the big buttons, not the keyboard -->
   <div id="welcome-tip">
-    <div class="tip-title">SHOWROOM CONTROLS</div>
-    <div class="tip-row"><kbd>W A S D</kbd> Walk around</div>
-    <div class="tip-row"><kbd>P</kbd> Peruse the collection (auto-tour)</div>
-    <div class="tip-row"><kbd>Walk up</kbd> Within 4 ft a wing opens &mdash; full continuous design</div>
-    <div class="tip-row"><kbd>Click</kbd> Inspect a wing &middot; <kbd>B</kbd> open board</div>
-    <div class="tip-row"><kbd>Space</kbd> Stop &amp; inspect &middot; <kbd>Esc</kbd> back</div>
+    <div class="tip-title">HOW TO BROWSE</div>
+    <div class="tip-row"><kbd>Next &#9654;</kbd> See the next design</div>
+    <div class="tip-row"><kbd>&#9664; Previous</kbd> See the design before</div>
+    <div class="tip-row"><kbd>&#9655; Auto Tour</kbd> Watch them all by itself</div>
+    <div class="tip-row"><kbd>&#9783; All Designs</kbd> See every design as big pictures</div>
   </div>
 
   <canvas id="showroom-canvas"></canvas>
diff --git a/scripts/test-senior.js b/scripts/test-senior.js
new file mode 100644
index 0000000..25f5d1c
--- /dev/null
+++ b/scripts/test-senior.js
@@ -0,0 +1,175 @@
+#!/usr/bin/env node
+/**
+ * test-senior.js — SENIOR-STYLE walkthrough. Drives ONLY the big on-screen guided
+ * buttons (Previous / Next / Auto Tour / All Designs) with the MOUSE. NO keyboard,
+ * NO 3D walking — proves a 65-year-old can browse the whole collection hands-on-mouse.
+ *
+ * Verifies: guided mode boots focused on a board; Next/Previous fly board-to-board;
+ * Auto Tour runs hands-free and Pause stops it; All Designs grid opens + a card jumps
+ * to a board; the big buttons are >=56px tall; the China Seas spec card stays intact;
+ * FPS stays healthy; zero console errors.
+ *
+ * Run: NODE_PATH=$HOME/.npm-global/lib/node_modules node scripts/test-senior.js
+ */
+const path = require('path');
+const fs = require('fs');
+const { chromium } = require('playwright');
+
+const URL = process.env.SHOWROOM_URL || 'http://127.0.0.1:7690/';
+const OUT = path.join(__dirname, '..', 'recordings');
+const SHOTS = path.join(OUT, 'shots');
+fs.mkdirSync(SHOTS, { recursive: true });
+const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+
+(async () => {
+  const browser = await chromium.launch({ args: ['--use-gl=angle', '--enable-webgl', '--ignore-gpu-blocklist'] });
+  const ctx = await browser.newContext({
+    viewport: { width: 1600, height: 900 },
+    recordVideo: { dir: OUT, size: { width: 1600, height: 900 } },
+  });
+  const page = await ctx.newPage();
+  const errors = [], fpsLog = [];
+  page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); });
+  page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message));
+
+  // Ensure a clean GUIDED default (no persisted Explore from a prior run).
+  await page.addInitScript(() => { try { localStorage.removeItem('qh_explore'); } catch(e){} });
+
+  console.log('→ loading', URL);
+  await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
+  await page.waitForFunction(() => window._wingBoards && window._wingBoards.length > 0, { timeout: 25000 }).catch(() => {});
+  await sleep(3500); // loader fade + guided auto-focus on the middle board
+
+  const sampleFps = async (tag) => {
+    const fps = await page.evaluate(() => { const el = document.getElementById('fps-counter'); return el ? parseInt(el.textContent) : null; });
+    if (fps > 0) fpsLog.push({ tag, fps });
+    return fps;
+  };
+
+  // --- Assert guided mode booted on a focused board (big title visible) ---
+  const guided = await page.evaluate(() => {
+    const t = document.getElementById('guided-title');
+    const bar = document.getElementById('guided-bar');
+    const exploreOff = !document.body.classList.contains('explore-mode');
+    return {
+      titleVisible: t && !t.classList.contains('hidden'),
+      barVisible: !!bar && getComputedStyle(bar).display !== 'none',
+      exploreOff,
+      titleText: document.getElementById('gt-name') ? document.getElementById('gt-name').textContent : null,
+      pos: document.getElementById('gt-pos') ? document.getElementById('gt-pos').textContent : null,
+    };
+  });
+  console.log('  guided boot:', JSON.stringify(guided));
+  await page.screenshot({ path: path.join(SHOTS, 'sr-01-guided-boot.png') });
+  await sampleFps('boot');
+
+  // --- Measure the big-button tap-target heights (must be >= 56px) ---
+  const btnSizes = await page.evaluate(() => {
+    const ids = ['g-prev', 'g-tour', 'g-next', 'g-grid'];
+    const out = {};
+    ids.forEach(id => { const el = document.getElementById(id); out[id] = el ? Math.round(el.getBoundingClientRect().height) : 0; });
+    return out;
+  });
+  console.log('  button heights:', JSON.stringify(btnSizes));
+
+  // --- Browse FORWARD with the big "Next ▶" button only (mouse clicks) ---
+  console.log('→ NEXT × 6 (mouse only, no keyboard)');
+  const seen = [];
+  for (let i = 0; i < 6; i++) {
+    await page.click('#g-next');
+    await sleep(1500);
+    const name = await page.evaluate(() => document.getElementById('gt-name') ? document.getElementById('gt-name').textContent : '');
+    const pos = await page.evaluate(() => document.getElementById('gt-pos') ? document.getElementById('gt-pos').textContent : '');
+    seen.push(pos);
+    if (i === 2) await page.screenshot({ path: path.join(SHOTS, 'sr-02-after-next.png') });
+    await sampleFps('next');
+  }
+  console.log('  positions seen via Next:', JSON.stringify(seen));
+
+  // --- Browse BACKWARD with "◀ Previous" ---
+  console.log('→ PREVIOUS × 3 (mouse only)');
+  for (let i = 0; i < 3; i++) { await page.click('#g-prev'); await sleep(1400); await sampleFps('prev'); }
+  await page.screenshot({ path: path.join(SHOTS, 'sr-03-after-prev.png') });
+
+  // --- AUTO TOUR (hands-free) then PAUSE — all via the one big brass button ---
+  console.log('→ AUTO TOUR (hands-free) for ~14s, then PAUSE');
+  await page.click('#g-tour');                      // start
+  await sleep(2000);
+  const touring = await page.evaluate(() => document.getElementById('g-tour').classList.contains('playing'));
+  await sleep(12000);
+  await page.screenshot({ path: path.join(SHOTS, 'sr-04-auto-tour.png') });
+  await sampleFps('tour');
+  await page.click('#g-tour');                      // pause (same button)
+  await sleep(1200);
+  const paused = await page.evaluate(() => !document.getElementById('g-tour').classList.contains('playing'));
+  console.log('  auto-tour started:', touring, '| paused via same button:', paused);
+
+  // --- ALL DESIGNS grid — open, then tap a big picture to jump to it ---
+  console.log('→ ALL DESIGNS grid: open + tap a card');
+  await page.click('#g-grid');
+  await sleep(1200);
+  const cardCount = await page.evaluate(() => document.querySelectorAll('#grid-items .go-card').length);
+  await page.screenshot({ path: path.join(SHOTS, 'sr-05-all-designs.png') });
+  console.log('  grid cards:', cardCount);
+  // Tap the 4th card
+  await page.click('#grid-items .go-card:nth-child(4)').catch(() => {});
+  await sleep(1800);
+  const gridClosed = await page.evaluate(() => !document.getElementById('grid-overlay').classList.contains('open'));
+  const afterCardName = await page.evaluate(() => document.getElementById('gt-name') ? document.getElementById('gt-name').textContent : '');
+  await page.screenshot({ path: path.join(SHOTS, 'sr-06-after-grid-pick.png') });
+  console.log('  grid closed after pick:', gridClosed, '| now showing:', afterCardName);
+
+  // --- Page across the WHOLE collection with Next, confirming the window advances ---
+  console.log('→ paging the full collection with Next (window should advance past 8)');
+  let maxPos = 0;
+  for (let i = 0; i < 12; i++) {
+    await page.click('#g-next');
+    await sleep(1100);
+    const idx = await page.evaluate(() => {
+      const el = document.getElementById('gt-pos'); if (!el) return 0;
+      const m = el.textContent.match(/Design\s+(\d+)/); return m ? parseInt(m[1]) : 0;
+    });
+    if (idx > maxPos) maxPos = idx;
+  }
+  console.log('  highest design index reached via Next:', maxPos);
+  await sampleFps('paging');
+
+  // --- Spec card integrity (China Seas) ---
+  const specText = await page.evaluate(() => {
+    const grid = document.getElementById('detail-spec-grid');
+    if (!grid) return null;
+    return [...grid.querySelectorAll('.wd-spec')].map(r => r.querySelector('dt').textContent + ': ' + r.querySelector('dd').textContent);
+  });
+  console.log('  SPEC SHEET:', JSON.stringify(specText));
+
+  const stats = await page.evaluate(() => (window._getSceneStats ? window._getSceneStats() : null));
+  const fpsVals = fpsLog.map(f => f.fps).filter(n => n > 0);
+  const report = {
+    when: new Date().toISOString(),
+    mode: 'senior-buttons-only',
+    guidedBoot: guided,
+    buttonHeights: btnSizes,
+    buttonsMeetTarget: Object.values(btnSizes).every(h => h >= 56),
+    autoTourWorked: !!touring && !!paused,
+    gridCards: cardCount,
+    gridPickJumped: gridClosed,
+    highestIndexViaNext: maxPos,
+    specText,
+    errors, errorCount: errors.length,
+    fpsMin: fpsVals.length ? Math.min(...fpsVals) : null,
+    fpsAvg: fpsVals.length ? Math.round(fpsVals.reduce((a, b) => a + b, 0) / fpsVals.length) : null,
+    fpsSamples: fpsLog,
+    sceneStats: stats,
+  };
+  fs.writeFileSync(path.join(OUT, 'test-senior-report.json'), JSON.stringify(report, null, 2));
+  console.log('\n=== SENIOR REPORT ===');
+  console.log('errors:', report.errorCount, '| fps min/avg:', report.fpsMin, '/', report.fpsAvg,
+    '| buttons>=56:', report.buttonsMeetTarget, '| autoTour:', report.autoTourWorked,
+    '| gridPick:', report.gridPickJumped, '| reachedIndex:', report.highestIndexViaNext);
+  if (errors.length) console.log('ERRORS:', errors.slice(0, 8).join(' || '));
+
+  await page.close();
+  const vid = await page.video().path().catch(() => null);
+  await ctx.close(); await browser.close();
+  if (vid && fs.existsSync(vid)) { fs.renameSync(vid, path.join(OUT, 'test-senior.webm')); console.log('✓ test-senior.webm'); }
+})().catch(e => { console.error('SENIOR TEST FAILED:', e.message); process.exit(1); });

← d7f73cb REVIEW.md: pass 6 — gap A blocked (no hi-res source, 1000px  ·  back to Quadrille Showroom  ·  Realism: subtle additive-blend bloom halo on ceiling cove st ed4ad46 →