[object Object]

← back to Quadrille Showroom

auto-save: 2026-07-02T10:47:21 (3 files) — package-lock.json public/js/dock.js public/showroom.html

b657af00c9811d16309d230a54f37da70263eb5a · 2026-07-02 10:47:24 -0700 · Steve Abrams

Files touched

Diff

commit b657af00c9811d16309d230a54f37da70263eb5a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 2 10:47:24 2026 -0700

    auto-save: 2026-07-02T10:47:21 (3 files) — package-lock.json public/js/dock.js public/showroom.html
---
 package-lock.json    |   4 +-
 public/js/dock.js    | 437 ++++++++++++++++++++++++++++++---------------------
 public/showroom.html |  36 +++--
 3 files changed, 281 insertions(+), 196 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 0257f15..154fb7e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "quadrille-showroom",
-  "version": "1.3.1",
+  "version": "1.3.2",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "quadrille-showroom",
-      "version": "1.3.1",
+      "version": "1.3.2",
       "license": "ISC",
       "dependencies": {
         "compression": "^1.8.1",
diff --git a/public/js/dock.js b/public/js/dock.js
index 13408cf..db383a2 100644
--- a/public/js/dock.js
+++ b/public/js/dock.js
@@ -1,54 +1,67 @@
 /* ============================================================================
- * dock.js — The Quadrille House: HAMBURGER DOCK.
+ * dock.js — The Quadrille House: 4-CORNER HAMBURGER DOCK.
  *
- * Standing ask (Steve, port 7690): on load the showroom is a bare 3D view with
- * ONE hamburger in the corner. Every control surface — Versions, View & Theme,
- * the bottom Controls bar, the Navigate bar, the Collection list — and every
- * top-toolbar tool (Peruse, Wings/Overview, Explore, Music, Lighting, Age View,
- * Fullscreen) is STOWED on load and re-launches from the hamburger as its own
- * draggable / resizable / position-persisted modal.
+ * Standing ask (Steve, port 7690, 2026-07-02): the single top-right hamburger is
+ * replaced by FOUR corner hamburgers — one in each screen corner — and every
+ * control function is SPLIT across the corners by theme. Everything is COLLAPSED
+ * and clean on load (DTD verdict A); each corner's hamburger expands only its own
+ * group. Controls scale with viewport width (clamp) and the bottom pager always
+ * shows cleanly.
+ *
+ *   TOP-LEFT   → Collection (vendor list) + Wings / Overview / Peruse
+ *   TOP-RIGHT  → View & Theme + display tools (Explore, Open Board, Walls,
+ *                Lighting, Music, Fullscreen)
+ *   BOTTOM-LEFT→ Furniture Style (the style tray, now collapsible)
+ *   BOTTOM-RIGHT→ Versions + Controls (sort, density, wing sliders, room size,
+ *                window pager)
  *
  * Non-destructive by design: the original panels keep their IDs and ALL of their
- * existing wiring (showroom.js / viewmodes.js / versions.js bind by ID). We only
- *   (a) hide them on load (`.qh-stowed`), and
- *   (b) on launch, wrap each in a `.qh-modal` shell + ModalRig.rig() the shell —
- *       so the panel's own flex/transform layout is never fought.
+ * existing wiring (showroom.js / viewmodes.js / versions.js / furniture-styles.js
+ * bind by ID). We only (a) stow them on load (`.qh-stowed`), and (b) on launch
+ * wrap each in a `.qh-modal` shell (ModalRig-rigged), seeded to open ANCHORED to
+ * its owning corner. Panels carry a collapse (–) toggle so any element can be
+ * folded to its title bar.
  *
  * Depends on /js/modal-rig.js (window.ModalRig). Loaded LAST so every panel the
- * other bolt-ons inject already exists; a MutationObserver mops up late arrivals
- * (e.g. ageview's #btn-ageview, which is added on DOMContentLoaded).
+ * other bolt-ons inject already exists; a MutationObserver + a dedicated poller
+ * mop up late arrivals (ageview's #btn-ageview; furniture-styles' #qh-style-tray,
+ * which is built async once the furniture API is live).
  * ========================================================================== */
 (function () {
   'use strict';
 
-  // --- LAUNCHABLE PANELS: each becomes a movable modal -----------------------
-  // sel = the existing element; label = menu/header text; w/h = first-open size.
-  var PANELS = [
-    // #guided-bar (◀ Previous / Next ▶ pager) is intentionally NOT docked — Steve wants it
-    // ALWAYS visible while walking in book mode, so it stays a permanent bottom-centre fixture.
-    { sel: '#window-bar',     label: 'Controls',        w: 760, h: 240 },
-    { sel: '#vm-panel',       label: 'View & Theme',    w: 300, h: 380 },
-    { sel: '#ver-rail',       label: 'Versions',        w: 250, h: 440 },
-    { sel: '#vendor-sidebar', label: 'Collection',      w: 250, h: 420 }
-  ];
-
-  // --- TOP-BAR TOOLS: menu items that just proxy-click the original button ---
-  // (the buttons stay in the DOM, only their containers are hidden)
-  var TOOLS = [
-    { sel: '#btn-peruse',              label: '▷ Peruse Collection' },
-    { sel: '#top-bar [data-section="wings"]',    label: 'Wing Boards' },
-    { sel: '#top-bar [data-section="overview"]', label: 'Overview' },
-    { sel: '#btn-explore',             label: 'Explore (WASD)' },
-    { sel: '#btn-bookmatch',           label: 'Open Board' },
-    { sel: '#btn-walls',               label: 'Wallpaper Walls' },
-    { sel: '#btn-music',               label: 'Music' },
-    { sel: '#btn-lighting',            label: 'Lighting' },
-    { sel: '#btn-fullscreen',          label: 'Fullscreen' }
+  // --- CORNERS: each owns a themed group of panels + tool proxy-clicks --------
+  // panels: existing elements launched as movable modals.
+  // tools : menu items that proxy-click an existing button (button stays in DOM).
+  var CORNERS = [
+    { id: 'tl', pos: 'top-left',     label: 'Collection & Tour',
+      panels: [ { sel: '#vendor-sidebar', label: 'Collection', w: 250, h: 420 } ],
+      tools:  [ { sel: '#top-bar [data-section="wings"]',    label: 'Wing Boards' },
+                { sel: '#top-bar [data-section="overview"]', label: 'Overview' },
+                { sel: '#btn-peruse',                        label: '▷ Peruse Collection' } ] },
+
+    { id: 'tr', pos: 'top-right',    label: 'View & Tools',
+      panels: [ { sel: '#vm-panel', label: 'View & Theme', w: 300, h: 380 } ],
+      tools:  [ { sel: '#btn-explore',    label: 'Explore (WASD)' },
+                { sel: '#btn-bookmatch',  label: 'Open Board' },
+                { sel: '#btn-walls',      label: 'Wallpaper Walls' },
+                { sel: '#btn-lighting',   label: 'Lighting' },
+                { sel: '#btn-music',      label: 'Music' },
+                { sel: '#btn-fullscreen', label: 'Fullscreen' } ] },
+
+    { id: 'bl', pos: 'bottom-left',  label: 'Furniture Style',
+      panels: [ { sel: '#qh-style-tray', label: 'Furniture Style', w: 260, h: 200 } ],
+      tools:  [] },
+
+    { id: 'br', pos: 'bottom-right', label: 'Versions & Controls',
+      panels: [ { sel: '#ver-rail',   label: 'Versions', w: 250, h: 440 },
+                { sel: '#window-bar', label: 'Controls', w: 760, h: 260 } ],
+      tools:  [] }
   ];
 
-  // --- STOW-ONLY: passive chrome hidden for a clean "just a hamburger" screen --
+  // --- STOW-ONLY: passive chrome hidden for a clean "just hamburgers" screen ---
   var STOW_ONLY = [
-    '#top-bar',                                   // whole top toolbar (tools folded into menu)
+    '#top-bar',                                   // whole top toolbar (tools folded into corner menus)
     '#guided-instruction', '#guided-title',       // guided-mode overlays
     '#bottom-bar', '#now-viewing',                 // bottom navigator chatter (caption stays — book HUD)
     '#minimap',                                   // spatial minimap (explore aid)
@@ -57,8 +70,15 @@
   ];
 
   var Z_BASE = 5000, zTop = Z_BASE;       // modal stacking
-  var openOrder = [];                     // selectors, in focus order (last = top)
-  var LS_OPEN = 'qh_dock_open';
+  var openOrder = [];                     // panel selectors, in focus order (last = top)
+  var LS_OPEN = 'qh_dock_open_v2';
+
+  // flat index of every panel across corners (sel -> {panel, corner})
+  var PANEL_INDEX = {};
+  CORNERS.forEach(function (c) {
+    c.panels.forEach(function (p) { PANEL_INDEX[p.sel] = { p: p, corner: c }; });
+  });
+  function allPanels() { return CORNERS.reduce(function (a, c) { return a.concat(c.panels); }, []); }
 
   // ---------------------------------------------------------------- styles ----
   function injectStyles() {
@@ -67,37 +87,42 @@
     st.id = 'qh-dock-styles';
     st.textContent = [
       '.qh-stowed{display:none !important;}',
-      /* hamburger — top-right corner, always on top */
-      '#qh-burger{position:fixed;top:14px;right:16px;z-index:9000;width:48px;height:48px;',
-        'display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;',
+      /* corner hamburgers — one per corner, responsive size */
+      '.qh-cburger{position:fixed;z-index:9000;',
+        'width:clamp(40px,5.2vw,48px);height:clamp(40px,5.2vw,48px);',
+        'display:flex;flex-direction:column;align-items:center;justify-content:center;gap:clamp(4px,0.6vw,5px);',
         'background:rgba(12,12,17,0.86);border:1.5px solid rgba(201,169,110,0.5);border-radius:13px;',
         'cursor:pointer;backdrop-filter:blur(12px);box-shadow:0 10px 30px -8px rgba(0,0,0,0.7);',
         'transition:border-color .15s, transform .1s;}',
-      '#qh-burger:hover{border-color:#c9a96e;}',
-      '#qh-burger:active{transform:translateY(1px);}',
-      '#qh-burger span{display:block;width:22px;height:2.5px;border-radius:2px;background:#c9a96e;transition:.2s;}',
-      '#qh-burger.open span:nth-child(1){transform:translateY(7.5px) rotate(45deg);}',
-      '#qh-burger.open span:nth-child(2){opacity:0;}',
-      '#qh-burger.open span:nth-child(3){transform:translateY(-7.5px) rotate(-45deg);}',
-      /* first-visit discoverability: gentle attention pulse + hint pill pointing at the ☰ */
-      '#qh-burger.qh-attn{animation:qh-attn-pulse 1.9s ease-out infinite;}',
+      '.qh-cburger:hover{border-color:#c9a96e;}',
+      '.qh-cburger:active{transform:translateY(1px);}',
+      '.qh-cburger span{display:block;width:clamp(18px,2.4vw,22px);height:2.5px;border-radius:2px;',
+        'background:#c9a96e;transition:.2s;}',
+      '.qh-cburger.open span:nth-child(1){transform:translateY(7.5px) rotate(45deg);}',
+      '.qh-cburger.open span:nth-child(2){opacity:0;}',
+      '.qh-cburger.open span:nth-child(3){transform:translateY(-7.5px) rotate(-45deg);}',
+      /* corner placement */
+      '#qh-cb-tl{top:14px;left:16px;} #qh-cb-tr{top:14px;right:16px;}',
+      '#qh-cb-bl{bottom:16px;left:16px;} #qh-cb-br{bottom:16px;right:16px;}',
+      /* first-visit attention pulse (one-time) */
+      '.qh-cburger.qh-attn{animation:qh-attn-pulse 1.9s ease-out infinite;}',
       '@keyframes qh-attn-pulse{0%{box-shadow:0 10px 30px -8px rgba(0,0,0,0.7),0 0 0 0 rgba(201,169,110,0.5);}' +
-        '70%{box-shadow:0 10px 30px -8px rgba(0,0,0,0.7),0 0 0 15px rgba(201,169,110,0);}' +
+        '70%{box-shadow:0 10px 30px -8px rgba(0,0,0,0.7),0 0 0 13px rgba(201,169,110,0);}' +
         '100%{box-shadow:0 10px 30px -8px rgba(0,0,0,0.7),0 0 0 0 rgba(201,169,110,0);}}',
-      '#qh-burger-hint{position:fixed;top:25px;right:74px;z-index:9000;pointer-events:none;',
-        'font-family:Georgia,serif;font-size:13px;letter-spacing:.4px;color:#f0e6cf;',
-        'background:rgba(12,12,17,0.88);border:1px solid rgba(201,169,110,0.5);',
-        'padding:6px 13px;border-radius:9px;backdrop-filter:blur(12px);white-space:nowrap;',
-        'box-shadow:0 10px 30px -8px rgba(0,0,0,0.7);transition:opacity .4s, transform .4s;}',
-      '#qh-burger-hint::after{content:"";position:absolute;top:50%;right:-6px;transform:translateY(-50%);',
-        'border-top:6px solid transparent;border-bottom:6px solid transparent;border-left:6px solid rgba(201,169,110,0.5);}',
-      '#qh-burger-hint.qh-hint-gone{opacity:0;transform:translateX(10px);}',
-      /* launcher menu */
-      '#qh-menu{position:fixed;top:70px;right:16px;z-index:8999;width:236px;max-height:78vh;overflow:auto;',
+      /* corner labels (tiny eyebrow under/over each burger, shown on hover) */
+      '.qh-clabel{position:fixed;z-index:8998;pointer-events:none;font-family:Georgia,serif;',
+        'font-size:10px;letter-spacing:1px;text-transform:uppercase;color:#c9b98a;',
+        'background:rgba(12,12,17,0.8);border:1px solid rgba(201,169,110,0.3);',
+        'padding:3px 8px;border-radius:7px;opacity:0;transition:opacity .18s;white-space:nowrap;}',
+      '.qh-cburger:hover + .qh-clabel{opacity:1;}',
+      /* per-corner launcher menu */
+      '.qh-cmenu{position:fixed;z-index:8999;width:min(230px,78vw);max-height:70vh;overflow:auto;',
         'background:rgba(14,14,20,0.94);border:1px solid rgba(201,169,110,0.3);border-radius:14px;',
         'padding:8px;backdrop-filter:blur(14px);box-shadow:0 18px 50px -12px rgba(0,0,0,0.8);',
         'display:none;font-family:inherit;}',
-      '#qh-menu.open{display:block;}',
+      '.qh-cmenu.open{display:block;}',
+      '#qh-cm-tl{top:70px;left:16px;} #qh-cm-tr{top:70px;right:16px;}',
+      '#qh-cm-bl{bottom:74px;left:16px;} #qh-cm-br{bottom:74px;right:16px;}',
       '.qh-menu-eyebrow{font-size:10px;letter-spacing:1.5px;text-transform:uppercase;color:#8c8678;',
         'padding:8px 10px 4px;font-weight:700;}',
       '.qh-menu-item{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;',
@@ -110,18 +135,21 @@
       '.qh-menu-item.active{border-color:rgba(201,169,110,0.55);}',
       /* modal shell */
       '.qh-modal{position:absolute;z-index:5000;display:flex;flex-direction:column;',
-        'min-width:200px;min-height:90px;overflow:hidden;border-radius:13px;',
+        'min-width:200px;min-height:44px;overflow:hidden;border-radius:13px;',
+        'max-width:96vw;',
         'background:rgba(12,12,17,0.92);border:1px solid rgba(201,169,110,0.34);',
         'box-shadow:0 22px 60px -14px rgba(0,0,0,0.8);backdrop-filter:blur(10px);}',
+      '.qh-modal.qh-collapsed{height:auto !important;resize:none !important;}',
+      '.qh-modal.qh-collapsed > .qh-pane-host{display:none !important;}',
       '.qh-modal-head{flex:0 0 auto;display:flex;align-items:center;justify-content:space-between;gap:8px;',
         'padding:8px 12px;cursor:move;user-select:none;border-bottom:1px solid rgba(201,169,110,0.22);',
         'background:rgba(201,169,110,0.10);}',
       '.qh-modal-title{font-family:Georgia,serif;font-size:14px;font-weight:600;color:#f0e6cf;letter-spacing:.4px;',
         'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
-      '.qh-modal-tools{flex:0 0 auto;display:flex;align-items:center;gap:2px;color:#c9a96e;}',
-      '.qh-modal-x{cursor:pointer;opacity:.75;font-size:19px;line-height:1;padding:0 4px;}',
-      '.qh-modal-x:hover{opacity:1;color:#fff;}',
-      /* the original panel, neutralized, fills the shell body */
+      '.qh-modal-tools{flex:0 0 auto;display:flex;align-items:center;gap:6px;color:#c9a96e;}',
+      '.qh-modal-min,.qh-modal-x{cursor:pointer;opacity:.75;line-height:1;padding:0 2px;}',
+      '.qh-modal-min{font-size:17px;} .qh-modal-x{font-size:19px;}',
+      '.qh-modal-min:hover,.qh-modal-x:hover{opacity:1;color:#fff;}',
       '.qh-modal > .qh-pane-host{flex:1 1 auto;overflow:auto;min-height:0;}'
     ].join('\n');
     document.head.appendChild(st);
@@ -135,20 +163,24 @@
     if (el) el.classList.add('qh-stowed');
   }
 
-  // P2: give each panel a staggered FIRST-OPEN position so launching several
-  // doesn't pile them centered on top of each other. We pre-seed ModalRig's
-  // saved-rect key only when none exists yet — so a position the user has
-  // dragged (which ModalRig.saveRect writes to the same key) always wins on
-  // subsequent opens. Cascade stays clamped inside the viewport.
-  function seedCascadeRect(p) {
-    var key = 'modalrig:qhdock:' + p.sel;
+  // Seed a panel's FIRST-OPEN rect anchored to its owning corner (so it reads as
+  // "belonging" to the hamburger it launched from). A position the user later
+  // drags wins on subsequent opens (ModalRig.saveRect writes the same key).
+  function seedCornerRect(p, corner) {
+    var key = 'modalrig:qhdock2:' + p.sel;
     try { if (localStorage.getItem(key)) return; } catch (e) {}
-    var i = PANELS.findIndex(function (x) { return x.sel === p.sel; });
-    if (i < 0) i = openOrder.length;
-    var step = 36, baseL = 90, baseT = 84;
-    var w = p.w || 760, h = p.h || 420;
-    var left = Math.max(0, Math.min(baseL + i * step, window.innerWidth - w - 16, window.innerWidth - 80));
-    var top  = Math.max(0, Math.min(baseT + i * step, window.innerHeight - h - 16, window.innerHeight - 40));
+    var w = p.w || 320, h = p.h || 360;
+    var m = 16, burgerZone = 70;   // clear the corner burger
+    var vw = window.innerWidth, vh = window.innerHeight;
+    // stagger multiple panels in the same corner so they don't stack
+    var siblings = corner.panels;
+    var idx = siblings.findIndex(function (x) { return x.sel === p.sel; });
+    var off = (idx < 0 ? 0 : idx) * 34;
+    var left, top;
+    if (corner.pos.indexOf('left') > -1)  left = m + off; else left = Math.max(m, vw - w - m - off);
+    if (corner.pos.indexOf('top') > -1)   top = burgerZone + off; else top = Math.max(m, vh - h - m - off);
+    left = Math.max(0, Math.min(left, vw - 80));
+    top  = Math.max(0, Math.min(top,  vh - 40));
     try { localStorage.setItem(key, JSON.stringify({ left: left, top: top, w: w, h: h })); } catch (e) {}
   }
 
@@ -156,16 +188,20 @@
   function launch(p) {
     var el = $(p.sel);
     if (!el) return;
+    var meta = PANEL_INDEX[p.sel];
+    var corner = meta ? meta.corner : CORNERS[0];
     var wrap = el.closest('.qh-modal');
     if (!wrap) {
-      // first launch: build the shell, move the panel inside, neutralize it
       wrap = document.createElement('div');
       wrap.className = 'qh-modal';
       wrap.setAttribute('data-qh-key', p.sel);
       var head = document.createElement('div');
       head.className = 'qh-modal-head';
       head.innerHTML = '<span class="qh-modal-title"></span>' +
-                       '<span class="qh-modal-tools"><span class="qh-modal-x" title="Close (Esc)">×</span></span>';
+                       '<span class="qh-modal-tools">' +
+                         '<span class="qh-modal-min" title="Collapse">–</span>' +
+                         '<span class="qh-modal-x" title="Close (Esc)">×</span>' +
+                       '</span>';
       head.querySelector('.qh-modal-title').textContent = p.label;
       el.parentNode.insertBefore(wrap, el);
       wrap.appendChild(head);
@@ -179,25 +215,31 @@
       s.left = s.right = s.top = s.bottom = 'auto';
       s.transform = 'none'; s.margin = '0';
       s.width = '100%'; s.maxWidth = 'none'; s.maxHeight = 'none';
-      s.display = ''; // clear any inline display:none from native collapse logic
-
-      head.querySelector('.qh-modal-x').addEventListener('click', function (e) {
-        e.stopPropagation(); hide(p);
+      s.display = '';
+
+      var xBtn = head.querySelector('.qh-modal-x');
+      xBtn.setAttribute('data-rig-btn', '1');
+      xBtn.addEventListener('click', function (e) { e.stopPropagation(); hide(p); });
+
+      var minBtn = head.querySelector('.qh-modal-min');
+      minBtn.setAttribute('data-rig-btn', '1');
+      minBtn.addEventListener('click', function (e) {
+        e.stopPropagation();
+        var collapsed = wrap.classList.toggle('qh-collapsed');
+        minBtn.textContent = collapsed ? '▸' : '–';   // ▸ when folded
+        minBtn.title = collapsed ? 'Expand' : 'Collapse';
       });
-      // keep the close × from initiating a drag
-      head.querySelector('.qh-modal-x').setAttribute('data-rig-btn', '1');
+
       wrap.addEventListener('mousedown', function () { bringToFront(p.sel, wrap); });
 
-      seedCascadeRect(p);   // P2: stagger first-open position so panels don't all stack centered
-      ModalRig.rig(wrap, { key: 'qhdock:' + p.sel, handle: head, width: p.w, height: p.h });
+      seedCornerRect(p, corner);
+      ModalRig.rig(wrap, { key: 'qhdock2:' + p.sel, handle: head, width: p.w, height: p.h });
     }
     wrap.classList.remove('qh-stowed');
-    el.classList.remove('qh-stowed');   // the PANEL itself was stowed (display:none!important) on boot;
-    //                                     removing it from the wrap alone left every launched panel's
-    //                                     CONTENT invisible/0×0 — only the header showed. (Phase-5 /5x.)
+    el.classList.remove('qh-stowed');
     bringToFront(p.sel, wrap);
     if (openOrder.indexOf(p.sel) === -1) openOrder.push(p.sel);
-    saveOpen(); reflectMenu();
+    saveOpen(); reflectMenus();
   }
 
   function hide(p) {
@@ -205,7 +247,7 @@
     var wrap = el.closest('.qh-modal');
     if (wrap) wrap.classList.add('qh-stowed');
     var i = openOrder.indexOf(p.sel); if (i > -1) openOrder.splice(i, 1);
-    saveOpen(); reflectMenu();
+    saveOpen(); reflectMenus();
   }
 
   function isOpen(p) {
@@ -226,107 +268,132 @@
     try { localStorage.setItem(LS_OPEN, JSON.stringify(openOrder)); } catch (e) {}
   }
 
-  // ----------------------------------------------------------------- menu -----
-  var burger, menu;
+  // ----------------------------------------------------------------- menus ----
+  var burgers = {}, menus = {};
 
-  function closeMenu() {
-    if (menu) menu.classList.remove('open');
-    if (burger) burger.classList.remove('open');
+  function closeAllMenus(except) {
+    CORNERS.forEach(function (c) {
+      if (c.id === except) return;
+      if (menus[c.id]) menus[c.id].classList.remove('open');
+      if (burgers[c.id]) burgers[c.id].classList.remove('open');
+    });
   }
 
-  // First-visit hint: stop the pulse, fade the pill out, and never show it again.
-  function dismissHint() {
-    if (burger) burger.classList.remove('qh-attn');
-    var h = document.getElementById('qh-burger-hint');
-    if (h) { h.classList.add('qh-hint-gone'); setTimeout(function () { if (h.parentNode) h.remove(); }, 450); }
-    try { localStorage.setItem('qh_burger_seen', '1'); } catch (e) {}
+  function dismissAttn() {
+    CORNERS.forEach(function (c) { if (burgers[c.id]) burgers[c.id].classList.remove('qh-attn'); });
+    try { localStorage.setItem('qh_corners_seen', '1'); } catch (e) {}
   }
 
-  function buildMenu() {
-    burger = document.createElement('div');
-    burger.id = 'qh-burger';
-    burger.title = 'Showroom controls';
-    burger.innerHTML = '<span></span><span></span><span></span>';
-    document.body.appendChild(burger);
-
-    // First-visit discoverability: pulse the ☰ + show a labeled hint pill pointing at it.
-    // Both are one-time — dismissed on first interaction and remembered so it never nags again.
-    var seenBurger = false;
-    try { seenBurger = !!localStorage.getItem('qh_burger_seen'); } catch (e) {}
-    if (!seenBurger) {
-      burger.classList.add('qh-attn');
-      var hint = document.createElement('div');
-      hint.id = 'qh-burger-hint';
-      hint.textContent = 'Controls & Views';
-      document.body.appendChild(hint);
+  function buildCorner(c) {
+    var b = document.createElement('div');
+    b.className = 'qh-cburger';
+    b.id = 'qh-cb-' + c.id;
+    b.title = c.label;
+    b.innerHTML = '<span></span><span></span><span></span>';
+    document.body.appendChild(b);
+    burgers[c.id] = b;
+
+    var lab = document.createElement('div');
+    lab.className = 'qh-clabel';
+    lab.textContent = c.label;
+    // position the label just inboard of the burger
+    var m = 16, gap = 70;
+    if (c.pos === 'top-left')     { lab.style.top = '22px';  lab.style.left = gap + 'px'; }
+    else if (c.pos === 'top-right')    { lab.style.top = '22px';  lab.style.right = gap + 'px'; }
+    else if (c.pos === 'bottom-left')  { lab.style.bottom = '24px'; lab.style.left = gap + 'px'; }
+    else                               { lab.style.bottom = '24px'; lab.style.right = gap + 'px'; }
+    document.body.appendChild(lab);
+
+    var menu = document.createElement('div');
+    menu.className = 'qh-cmenu';
+    menu.id = 'qh-cm-' + c.id;
+    var html = '';
+    if (c.panels.length) {
+      html += '<div class="qh-menu-eyebrow">Panels</div>';
+      c.panels.forEach(function (p) {
+        html += '<button class="qh-menu-item" data-panel="' + p.sel + '">' +
+                  '<span>' + p.label + '</span><span class="qh-dot">●</span></button>';
+      });
+    }
+    if (c.tools.length) {
+      html += '<div class="qh-menu-eyebrow">Tools</div>';
+      c.tools.forEach(function (t, i) {
+        html += '<button class="qh-menu-item" data-tool-idx="' + i + '">' +
+                  '<span>' + t.label + '</span><span class="qh-dot">●</span></button>';
+      });
     }
-
-    menu = document.createElement('div');
-    menu.id = 'qh-menu';
-    document.body.appendChild(menu);
-
-    var html = '<div class="qh-menu-eyebrow">Panels</div>';
-    PANELS.forEach(function (p) {
-      html += '<button class="qh-menu-item" data-panel="' + p.sel + '">' +
-                '<span>' + p.label + '</span><span class="qh-dot">●</span></button>';
-    });
-    html += '<div class="qh-menu-eyebrow">Tools</div>';
-    // NOTE: a TOOL selector may contain quotes/spaces (e.g. #top-bar [data-section="wings"]).
-    // Storing the raw selector in a data-* attribute breaks the HTML (inner quotes close the
-    // attribute early) AND breaks querySelector('[data-tool="<sel>"]') ("is not a valid
-    // selector"). So we key tools by INDEX and resolve TOOLS[idx].sel in JS. (Phase-5 /5x.)
-    TOOLS.forEach(function (t, i) {
-      html += '<button class="qh-menu-item" data-tool-idx="' + i + '">' +
-                '<span>' + t.label + '</span><span class="qh-dot">●</span></button>';
-    });
     menu.innerHTML = html;
+    document.body.appendChild(menu);
+    menus[c.id] = menu;
 
     menu.addEventListener('click', function (e) {
       var item = e.target.closest('.qh-menu-item'); if (!item) return;
       var pSel = item.getAttribute('data-panel');
       var tIdx = item.getAttribute('data-tool-idx');
       if (pSel) {
-        var p = PANELS.filter(function (x) { return x.sel === pSel; })[0];
+        var p = c.panels.filter(function (x) { return x.sel === pSel; })[0];
         if (p) toggle(p);
-        closeMenu();   // P3 (DTD A): launching a panel auto-closes the menu so the modal is unobstructed
+        closeAllMenus();
       } else if (tIdx !== null) {
-        var t = TOOLS[+tIdx];
+        var t = c.tools[+tIdx];
         var btn = t && $(t.sel);
-        if (btn) { btn.click(); setTimeout(reflectMenu, 30); }
-        closeMenu();
+        if (btn) { btn.click(); setTimeout(reflectMenus, 30); }
+        closeAllMenus();
       }
     });
 
-    burger.addEventListener('click', function () {
-      dismissHint();
+    b.addEventListener('click', function () {
+      dismissAttn();
+      // single-item corner (1 panel, no tools) → toggle it directly, no menu
+      if (c.panels.length === 1 && c.tools.length === 0) {
+        toggle(c.panels[0]);
+        b.classList.toggle('open', isOpen(c.panels[0]));
+        closeAllMenus();
+        return;
+      }
       var open = menu.classList.toggle('open');
-      burger.classList.toggle('open', open);
-      if (open) reflectMenu();
+      b.classList.toggle('open', open);
+      closeAllMenus(c.id);
+      if (open) reflectMenus();
     });
-    // click-away closes the menu (but not the modals)
+  }
+
+  function buildCorners() {
+    CORNERS.forEach(buildCorner);
+
+    var seen = false;
+    try { seen = !!localStorage.getItem('qh_corners_seen'); } catch (e) {}
+    if (!seen) CORNERS.forEach(function (c) { if (burgers[c.id]) burgers[c.id].classList.add('qh-attn'); });
+
+    // click-away closes menus (never the modals)
     document.addEventListener('mousedown', function (e) {
-      if (!menu.classList.contains('open')) return;
-      if (e.target.closest('#qh-menu') || e.target.closest('#qh-burger')) return;
-      menu.classList.remove('open'); burger.classList.remove('open');
+      if (e.target.closest('.qh-cmenu') || e.target.closest('.qh-cburger')) return;
+      closeAllMenus();
     });
   }
 
-  function reflectMenu() {
-    if (!menu) return;
-    PANELS.forEach(function (p) {
-      var it = menu.querySelector('[data-panel="' + p.sel + '"]');
-      if (it) it.classList.toggle('active', isOpen(p));
-    });
-    TOOLS.forEach(function (t, i) {
-      var it = menu.querySelector('[data-tool-idx="' + i + '"]');   // index key — selector may contain quotes
-      var btn = $(t.sel);
-      if (it) it.classList.toggle('active', !!btn && btn.classList.contains('active'));
+  function reflectMenus() {
+    CORNERS.forEach(function (c) {
+      var menu = menus[c.id]; if (!menu) return;
+      c.panels.forEach(function (p) {
+        var it = menu.querySelector('[data-panel="' + p.sel + '"]');
+        if (it) it.classList.toggle('active', isOpen(p));
+      });
+      c.tools.forEach(function (t, i) {
+        var it = menu.querySelector('[data-tool-idx="' + i + '"]');
+        var btn = $(t.sel);
+        if (it) it.classList.toggle('active', !!btn && btn.classList.contains('active'));
+      });
+      // single-item corner: reflect open-state on the burger itself
+      if (c.panels.length === 1 && c.tools.length === 0 && burgers[c.id]) {
+        burgers[c.id].classList.toggle('open', isOpen(c.panels[0]));
+      }
     });
   }
 
   // ------------------------------------------------------------- stow + boot --
   function stowAll() {
-    PANELS.forEach(function (p) {
+    allPanels().forEach(function (p) {
       var el = $(p.sel);
       if (el && !el.closest('.qh-modal')) stow(el);   // don't re-stow a launched modal's pane
     });
@@ -337,33 +404,47 @@
     var saved = [];
     try { saved = JSON.parse(localStorage.getItem(LS_OPEN) || '[]') || []; } catch (e) {}
     saved.forEach(function (sel) {
-      var p = PANELS.filter(function (x) { return x.sel === sel; })[0];
-      if (p) launch(p);
+      var meta = PANEL_INDEX[sel];
+      if (meta) launch(meta.p);
     });
   }
 
-  function boot() {
-    if (!window.ModalRig) {            // modal-rig.js must load first
-      return setTimeout(boot, 60);
+  // The furniture-style tray is built async (furniture-styles.js waits for the
+  // furniture API). Poll for it, stow it on arrival, and if it was open last
+  // session re-launch it.
+  function ownStyleTray(tries) {
+    tries = tries || 0;
+    var el = $('#qh-style-tray');
+    if (el) {
+      if (!el.closest('.qh-modal')) stow(el);
+      var saved = [];
+      try { saved = JSON.parse(localStorage.getItem(LS_OPEN) || '[]') || []; } catch (e) {}
+      if (saved.indexOf('#qh-style-tray') > -1) launch(PANEL_INDEX['#qh-style-tray'].p);
+      return;
     }
+    if (tries > 240) return;   // ~14s
+    setTimeout(function () { ownStyleTray(tries + 1); }, 60);
+  }
+
+  function boot() {
+    if (!window.ModalRig) return setTimeout(boot, 60);
     injectStyles();
     stowAll();
-    buildMenu();
+    buildCorners();
 
-    // Late-arriving nodes (ageview's #btn-ageview, or any panel injected on a
-    // later tick) — keep them stowed until first launch.
     var mo = new MutationObserver(function () { stowAll(); });
     mo.observe(document.body, { childList: true, subtree: true });
-    setTimeout(function () { mo.disconnect(); }, 4000);   // settle window
+    setTimeout(function () { mo.disconnect(); }, 4000);
 
     restoreOpen();
+    ownStyleTray(0);
 
     // Esc closes the top-most open modal.
     document.addEventListener('keydown', function (e) {
       if (e.key !== 'Escape' || !openOrder.length) return;
       var sel = openOrder[openOrder.length - 1];
-      var p = PANELS.filter(function (x) { return x.sel === sel; })[0];
-      if (p) { hide(p); e.stopPropagation(); }
+      var meta = PANEL_INDEX[sel];
+      if (meta) { hide(meta.p); e.stopPropagation(); }
     }, true);
   }
 
diff --git a/public/showroom.html b/public/showroom.html
index 3fafdbb..dc225db 100644
--- a/public/showroom.html
+++ b/public/showroom.html
@@ -18,36 +18,42 @@
        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.
        ============================================================ */
+    /* Bottom pager — smaller + viewport-responsive (Steve 2026-07-02: reduce the
+       button size, scale with website width, always show cleanly at the bottom).
+       Sits centred between the bottom-left/right corner hamburgers with margin to
+       spare; wraps on narrow screens via the media query below. */
     #guided-bar {
-      position:fixed; left:50%; bottom:22px; transform:translateX(-50%);
-      z-index:40; display:flex; align-items:center; gap:14px;
+      position:fixed; left:50%; bottom:16px; transform:translateX(-50%);
+      z-index:40; display:flex; align-items:center; gap:clamp(6px,1vw,12px);
+      max-width:calc(100vw - 148px);   /* clear the ~64px corner burgers on each side */
       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);
+      border-radius:16px; padding:clamp(6px,0.9vw,10px) clamp(8px,1.2vw,14px);
+      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;
+      min-height:clamp(38px,5vw,48px); min-width:clamp(82px,11vw,124px);
+      padding:0 clamp(10px,1.5vw,18px);
+      display:inline-flex; align-items:center; justify-content:center; gap:6px;
+      font-size:clamp(12px,1.45vw,16px); font-weight:700; letter-spacing:.4px; cursor:pointer;
+      border-radius:11px; border:2px solid rgba(201,169,110,0.55);
+      background:rgba(255,255,255,0.06); color:#f0e6cf; white-space:nowrap;
       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 */
+    /* The primary Auto-Tour button — brass fill so it's the obvious hero action */
     #g-tour {
-      min-width:200px; font-size:21px;
+      min-width:clamp(120px,15vw,168px); font-size:clamp(13px,1.5vw,17px);
       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; }
+    #g-grid { min-width:clamp(88px,11vw,118px); }
     /* Always-on plain instruction strip above the guided bar */
     #guided-instruction {
       position:fixed; left:50%; bottom:96px; transform:translateX(-50%);
@@ -130,11 +136,9 @@
        so paging one design at a time through all 883 is always available. Placed AFTER the
        explore-mode hide so equal-specificity !important resolves in book mode's favour. */
     body.book-mode #guided-bar { display:flex !important; }
-    /* On small screens the big guided bar wraps; let it */
+    /* On small screens the pager wraps; clamp() already handles button sizing */
     @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-bar { flex-wrap:wrap; max-width:calc(100vw - 24px); justify-content:center; }
       #guided-title .gt-name { font-size:23px; }
     }
 

← bf8db8a chore: v1.3.2 (session close — furniture style selector + ar  ·  back to Quadrille Showroom  ·  5x sweep 1: keep bottom-corner panels clear of the centre pa a156052 →