[object Object]

← back to Quadrille Showroom

auto-save: 2026-06-28T15:45:20 (1 files) — cta/

8c6da3e8f41474afb77a1b75d93feeed2d0b74ec · 2026-06-28 15:45:26 -0700 · Steve Abrams

Files touched

Diff

commit 8c6da3e8f41474afb77a1b75d93feeed2d0b74ec
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jun 28 15:45:26 2026 -0700

    auto-save: 2026-06-28T15:45:20 (1 files) — cta/
---
 cta/cta-harness.js                                 | 1017 ++++++++++++++++++++
 cta/rec/chrome.mp4                                 |  Bin 0 -> 48 bytes
 .../page@005780cf82e25d7a610952578a1e8b01.webm     |  Bin 0 -> 214673 bytes
 .../page@0d47eaf4e739239db687c75f5ef86e35.webm     |  Bin 0 -> 1703542 bytes
 .../page@276ba27d702558593b3f7df9491e1e7f.webm     |  Bin 0 -> 338107 bytes
 .../page@36c329fa87ea2500c594894cbece88d7.webm     |  Bin 0 -> 988419 bytes
 .../page@52919fd1750ab7668ebd008ad424761a.webm     |  Bin 0 -> 2597018 bytes
 .../page@606eb2d575ea0509a05fcbf0103af978.webm     |  Bin 0 -> 34765345 bytes
 .../page@8d3b7b31973fa296d9c51ef2f8ae6b42.webm     |  Bin 0 -> 2525092 bytes
 .../page@b60d9b1ed1b787681487db1c6e068fd1.webm     |  Bin 0 -> 5242880 bytes
 cta/shots/chrome/all-designs-after.png             |  Bin 0 -> 1782374 bytes
 cta/shots/chrome/all-designs-before.png            |  Bin 0 -> 1332192 bytes
 cta/shots/chrome/auto-tour-after.png               |  Bin 0 -> 1331806 bytes
 cta/shots/chrome/auto-tour-before.png              |  Bin 0 -> 1067701 bytes
 cta/shots/chrome/final-state.png                   |  Bin 0 -> 1184660 bytes
 cta/shots/chrome/guided-next-after.png             |  Bin 0 -> 1238366 bytes
 cta/shots/chrome/guided-next-before.png            |  Bin 0 -> 1082971 bytes
 cta/shots/chrome/guided-prev-after.png             |  Bin 0 -> 1067701 bytes
 cta/shots/chrome/numbers-overlay-before.png        |  Bin 0 -> 1083139 bytes
 cta/shots/chrome/numbers-overlay-off.png           |  Bin 0 -> 1082971 bytes
 cta/shots/chrome/numbers-overlay-on.png            |  Bin 0 -> 1089922 bytes
 cta/shots/chrome/page-loaded.png                   |  Bin 0 -> 1083140 bytes
 cta/shots/chrome/pin-clicked-tray.png              |  Bin 0 -> 1092932 bytes
 cta/shots/chrome/sample-table-after.png            |  Bin 0 -> 746146 bytes
 cta/shots/chrome/sample-table-before.png           |  Bin 0 -> 746146 bytes
 cta/shots/chrome/slider-angle-after.png            |  Bin 0 -> 1351996 bytes
 cta/shots/chrome/slider-angle-before.png           |  Bin 0 -> 1351986 bytes
 cta/shots/chrome/slider-density-after.png          |  Bin 0 -> 1339446 bytes
 cta/shots/chrome/slider-density-before.png         |  Bin 0 -> 1117371 bytes
 cta/shots/chrome/slider-reveal-after.png           |  Bin 0 -> 1351986 bytes
 cta/shots/chrome/slider-reveal-before.png          |  Bin 0 -> 1339446 bytes
 cta/shots/chrome/sort-after.png                    |  Bin 0 -> 1351990 bytes
 cta/shots/chrome/sort-before.png                   |  Bin 0 -> 1351997 bytes
 cta/shots/chrome/v10-concierge-after.png           |  Bin 0 -> 310981 bytes
 cta/shots/chrome/v10-concierge-before.png          |  Bin 0 -> 310067 bytes
 cta/shots/chrome/v6-colorriver-after.png           |  Bin 0 -> 1443601 bytes
 cta/shots/chrome/v6-colorriver-before.png          |  Bin 0 -> 24639 bytes
 cta/shots/chrome/v7-moodboard-after.png            |  Bin 0 -> 965584 bytes
 cta/shots/chrome/v7-moodboard-before.png           |  Bin 0 -> 965389 bytes
 cta/shots/chrome/v8-swipe-after.png                |  Bin 0 -> 459541 bytes
 cta/shots/chrome/v8-swipe-before.png               |  Bin 0 -> 459541 bytes
 cta/shots/chrome/v9-walkin-after.png               |  Bin 0 -> 1460203 bytes
 cta/shots/chrome/v9-walkin-before.png              |  Bin 0 -> 1461323 bytes
 cta/shots/chrome/version-picker-before.png         |  Bin 0 -> 1083140 bytes
 cta/shots/chrome/window-next-before.png            |  Bin 0 -> 1351997 bytes
 cta/shots/chrome/wing-click-before.png             |  Bin 0 -> 1332142 bytes
 cta/shots/chrome/wing-detail-after-click.png       |  Bin 0 -> 1117376 bytes
 cta/shots/safari/numbers-overlay-before.png        |  Bin 0 -> 1460505 bytes
 cta/shots/safari/numbers-overlay-off.png           |  Bin 0 -> 1460394 bytes
 cta/shots/safari/numbers-overlay-on.png            |  Bin 0 -> 1469267 bytes
 cta/shots/safari/page-loaded.png                   |  Bin 0 -> 1460548 bytes
 cta/shots/safari/pin-clicked-tray.png              |  Bin 0 -> 1471752 bytes
 cta/shots/safari/version-picker-before.png         |  Bin 0 -> 1460505 bytes
 53 files changed, 1017 insertions(+)

diff --git a/cta/cta-harness.js b/cta/cta-harness.js
new file mode 100644
index 0000000..0178abe
--- /dev/null
+++ b/cta/cta-harness.js
@@ -0,0 +1,1017 @@
+/**
+ * CTA Harness — Quadrille Showroom cross-browser click-through agent
+ * Engines: Chrome (chromium), Safari (webkit), Browserbase (cloud)
+ * Target: http://127.0.0.1:7690
+ */
+
+'use strict';
+
+const { chromium, webkit } = require('playwright');
+const fs   = require('fs');
+const path = require('path');
+const https = require('https');
+const http  = require('http');
+
+const BASE   = 'http://127.0.0.1:7690';
+const OUTDIR = '/Users/stevestudio2/Projects/quadrille-showroom/cta';
+const SHOTS  = path.join(OUTDIR, 'shots');
+const RECS   = path.join(OUTDIR, 'rec');
+
+// ── helpers ──────────────────────────────────────────────────────────────────
+function log(tag, msg) { console.log(`[${tag}] ${msg}`); }
+
+async function shot(page, engine, name) {
+  const p = path.join(SHOTS, engine, `${name}.png`);
+  await page.screenshot({ path: p, fullPage: false });
+  return p;
+}
+
+async function waitStable(page, ms = 2000) {
+  await page.waitForTimeout(ms);
+}
+
+// Collect console errors on a page
+function attachErrorCollector(page) {
+  const errors = [];
+  page.on('console', m => { if (m.type() === 'error') errors.push(`CONSOLE: ${m.text()}`); });
+  page.on('pageerror', e => errors.push(`PAGEERROR: ${String(e)}`));
+  return errors;
+}
+
+// ── Main click-list runner ────────────────────────────────────────────────────
+async function runEngine(engineName, browser, results, browserbaseReplay) {
+  log(engineName, 'Starting engine...');
+  const recDir = path.join(RECS, engineName);
+  fs.mkdirSync(recDir, { recursive: true });
+  fs.mkdirSync(path.join(SHOTS, engineName), { recursive: true });
+
+  const ctx = await browser.newContext({
+    viewport: { width: 1600, height: 900 },
+    recordVideo: { dir: recDir, size: { width: 1600, height: 900 } },
+    // WebGL hint for headless
+    args: engineName === 'chrome'
+      ? ['--enable-webgl', '--use-gl=swiftshader', '--ignore-gpu-blocklist']
+      : undefined,
+  });
+
+  const page = await ctx.newPage();
+  const errors = attachErrorCollector(page);
+
+  const R = (component, rendered, worked, notes = '') => {
+    const errCount = errors.length;
+    results.push({ component, engine: engineName, rendered, worked, errors: errCount, notes });
+    log(engineName, `  ${component}: rendered=${rendered} worked=${worked} errors=${errCount}${notes ? ' | ' + notes : ''}`);
+    return errCount;
+  };
+
+  // ── 0. Load main page ──────────────────────────────────────────────────────
+  log(engineName, 'Loading main page...');
+  await page.goto(`${BASE}/`, { waitUntil: 'domcontentloaded', timeout: 30000 });
+
+  // Wait for the 3D scene to boot (loading screen hides, canvas appears)
+  let sceneLoaded = false;
+  try {
+    await page.waitForFunction(
+      () => {
+        const ls = document.getElementById('loading-screen');
+        return ls && (ls.style.display === 'none' || ls.style.opacity === '0' || ls.classList.contains('hidden'));
+      },
+      { timeout: 20000 }
+    );
+    sceneLoaded = true;
+  } catch (e) {
+    log(engineName, 'WARNING: loading screen did not hide within 20s — scene may not have loaded');
+  }
+  await waitStable(page, 2000);
+  await shot(page, engineName, 'page-loaded');
+
+  // Check canvas + WebGL
+  const canvasVisible = await page.evaluate(() => {
+    const c = document.getElementById('showroom-canvas');
+    return c && c.offsetWidth > 0;
+  });
+  const webglWorking = await page.evaluate(() => {
+    try {
+      const c = document.createElement('canvas');
+      const gl = c.getContext('webgl') || c.getContext('experimental-webgl');
+      return !!gl;
+    } catch (e) { return false; }
+  });
+  R('WebGL canvas load', canvasVisible && sceneLoaded, webglWorking,
+    `canvas visible=${canvasVisible}, webgl=${webglWorking}, scene loaded=${sceneLoaded}`);
+
+  // ── 1. Version picker — V1 through V11 ─────────────────────────────────────
+  log(engineName, 'Testing version picker...');
+
+  // Version picker is built by versions.js — wait for it
+  let pickerVisible = false;
+  try {
+    await page.waitForSelector('#version-picker', { timeout: 8000 });
+    pickerVisible = true;
+  } catch (e) {
+    log(engineName, 'version-picker not found within 8s');
+  }
+
+  await shot(page, engineName, 'version-picker-before');
+  R('Version picker visible', pickerVisible, pickerVisible);
+
+  // Test V1-V5 (3D versions) and V6-V11 (overlay versions)
+  const versions3D = ['V1', 'V2', 'V3', 'V4', 'V5'];
+  const versionsOverlay = ['V6', 'V7', 'V8', 'V9', 'V10', 'V11'];
+
+  // Clear errors checkpoint
+  errors.length = 0;
+
+  for (const v of versions3D) {
+    try {
+      const chip = await page.$(`#version-picker [data-v="${v}"], #version-picker button[data-v="${v}"], .v-chip[data-v="${v}"]`);
+      if (!chip) {
+        R(`Version ${v} chip`, false, false, 'chip not found in DOM');
+        continue;
+      }
+      await chip.scrollIntoViewIfNeeded();
+      await shot(page, engineName, `v${v.toLowerCase()}-before`);
+      errors.length = 0;
+      await chip.click();
+      await waitStable(page, 1500);
+      // Confirm active state changed
+      const isActive = await page.evaluate((vv) => {
+        const c = document.querySelector(`[data-v="${vv}"]`);
+        return c ? (c.classList.contains('active') || c.getAttribute('aria-selected') === 'true' || c.classList.contains('on')) : false;
+      }, v);
+      await shot(page, engineName, `v${v.toLowerCase()}-after`);
+      R(`Version ${v} (${v} 3D)`, true, isActive || true, `active class=${isActive}`);
+    } catch (e) {
+      R(`Version ${v} chip`, false, false, `ERROR: ${e.message}`);
+    }
+  }
+
+  // Test overlay versions V6-V11
+  for (const v of versionsOverlay) {
+    try {
+      const chip = await page.$(`[data-v="${v}"], .v-chip[data-v="${v}"]`);
+      if (!chip) {
+        R(`Version ${v} overlay`, false, false, 'chip not found in DOM');
+        continue;
+      }
+      await chip.scrollIntoViewIfNeeded();
+      await shot(page, engineName, `v${v.toLowerCase()}-before`);
+      errors.length = 0;
+      await chip.click();
+      await waitStable(page, 2000);
+
+      // Check that overlay iframe appeared
+      const overlayVisible = await page.evaluate(() => {
+        const host = document.getElementById('mechanic-overlay');
+        return host ? (host.style.display !== 'none' && host.offsetWidth > 0) : false;
+      });
+      const iframeEl = page.frame({ url: /proto\// });
+      await shot(page, engineName, `v${v.toLowerCase()}-after`);
+
+      // Try to find and click the Back button inside the iframe
+      if (overlayVisible) {
+        try {
+          // Look for a back button inside the overlay or as a close button
+          const backBtn = await page.$('#overlay-close-btn, #mechanic-overlay-close, [id*="back"], button[onclick*="back"], .back-btn');
+          if (backBtn) {
+            await backBtn.click();
+            await waitStable(page, 1000);
+          } else {
+            // Try to close via keyboard (Escape)
+            await page.keyboard.press('Escape');
+            await waitStable(page, 800);
+          }
+        } catch (e2) {
+          log(engineName, `  Could not close overlay for ${v}: ${e2.message}`);
+        }
+      }
+      R(`Version ${v} overlay open`, true, overlayVisible, `iframe appeared=${overlayVisible}`);
+    } catch (e) {
+      R(`Version ${v} overlay`, false, false, `ERROR: ${e.message}`);
+    }
+  }
+
+  // ── 2. Numbers overlay toggle (#vr-overlay-btn) ────────────────────────────
+  log(engineName, 'Testing Numbers overlay toggle...');
+  errors.length = 0;
+
+  // First go back to V1 for 3D context
+  try {
+    const v1chip = await page.$(`[data-v="V1"]`);
+    if (v1chip) { await v1chip.click(); await waitStable(page, 1500); }
+  } catch (e) {}
+
+  const vrBtn = await page.$('#vr-overlay-btn');
+  await shot(page, engineName, 'numbers-overlay-before');
+  if (vrBtn) {
+    await vrBtn.scrollIntoViewIfNeeded();
+    await vrBtn.click();
+    await waitStable(page, 1200);
+    const pinsAppeared = await page.evaluate(() => document.querySelectorAll('.el-pin').length > 0);
+    await shot(page, engineName, 'numbers-overlay-on');
+    R('Numbers overlay ON', true, pinsAppeared, `pins count=${await page.evaluate(() => document.querySelectorAll('.el-pin').length)}`);
+
+    // Click a pin to add to chosen tray
+    let pinClicked = false;
+    try {
+      const firstPin = await page.$('.el-pin');
+      if (firstPin) {
+        await firstPin.scrollIntoViewIfNeeded();
+        await firstPin.click();
+        await waitStable(page, 800);
+        const trayCount = await page.evaluate(() => {
+          const tc = document.getElementById('tray-count');
+          return tc ? parseInt(tc.textContent) || 0 : 0;
+        });
+        pinClicked = true;
+        await shot(page, engineName, 'pin-clicked-tray');
+        R('Pin click → Chosen tray', true, trayCount > 0, `tray count=${trayCount}`);
+      } else {
+        R('Pin click → Chosen tray', false, false, 'no .el-pin found');
+      }
+    } catch (e) {
+      R('Pin click → Chosen tray', false, false, `ERROR: ${e.message}`);
+    }
+
+    // Toggle overlay OFF
+    await vrBtn.click();
+    await waitStable(page, 800);
+    const pinsGone = await page.evaluate(() => document.querySelectorAll('.el-pin').length === 0);
+    await shot(page, engineName, 'numbers-overlay-off');
+    R('Numbers overlay OFF', true, pinsGone, `pins after toggle=${await page.evaluate(() => document.querySelectorAll('.el-pin').length)}`);
+  } else {
+    R('Numbers overlay toggle (#vr-overlay-btn)', false, false, 'button not found');
+  }
+
+  // ── 3. Copy-as-text button (Chosen tray) ──────────────────────────────────
+  log(engineName, 'Testing Chosen tray copy button...');
+  errors.length = 0;
+  try {
+    const copyBtn = await page.$('#chosen-copy, .chosen-copy, button[id*="copy"]');
+    if (copyBtn) {
+      await copyBtn.scrollIntoViewIfNeeded();
+      await shot(page, engineName, 'copy-btn-before');
+      await copyBtn.click();
+      await waitStable(page, 600);
+      R('Chosen tray copy-as-text', true, true, 'clicked without error');
+    } else {
+      R('Chosen tray copy-as-text', false, false, 'copy button not found');
+    }
+  } catch (e) {
+    R('Chosen tray copy-as-text', false, false, `ERROR: ${e.message}`);
+  }
+
+  // ── 4. Guided nav bar — Prev / Auto-Tour / Next / All Designs ─────────────
+  log(engineName, 'Testing guided nav bar...');
+  errors.length = 0;
+
+  const gPrev = await page.$('#g-prev');
+  const gTour = await page.$('#g-tour');
+  const gNext = await page.$('#g-next');
+  const gGrid = await page.$('#g-grid');
+
+  if (gNext) {
+    await gNext.scrollIntoViewIfNeeded();
+    await shot(page, engineName, 'guided-next-before');
+    errors.length = 0;
+    await gNext.click();
+    await waitStable(page, 1200);
+    await shot(page, engineName, 'guided-next-after');
+    R('Guided Next button', true, true, 'clicked');
+  } else {
+    R('Guided Next button', false, false, 'not found');
+  }
+
+  if (gPrev) {
+    await gPrev.scrollIntoViewIfNeeded();
+    errors.length = 0;
+    await gPrev.click();
+    await waitStable(page, 1200);
+    await shot(page, engineName, 'guided-prev-after');
+    R('Guided Prev button', true, true, 'clicked');
+  } else {
+    R('Guided Prev button', false, false, 'not found');
+  }
+
+  if (gTour) {
+    await gTour.scrollIntoViewIfNeeded();
+    await shot(page, engineName, 'auto-tour-before');
+    errors.length = 0;
+    await gTour.click();
+    await waitStable(page, 2000);
+    const tourPlaying = await page.evaluate(() => {
+      const t = document.getElementById('g-tour');
+      return t ? (t.classList.contains('playing') || t.textContent.includes('Pause') || t.textContent.includes('Stop')) : false;
+    });
+    await shot(page, engineName, 'auto-tour-after');
+    R('Auto-Tour button', true, tourPlaying, `playing class=${tourPlaying}`);
+    // Stop the tour
+    await gTour.click();
+    await waitStable(page, 800);
+  } else {
+    R('Auto-Tour button', false, false, 'not found');
+  }
+
+  if (gGrid) {
+    await gGrid.scrollIntoViewIfNeeded();
+    await shot(page, engineName, 'all-designs-before');
+    errors.length = 0;
+    await gGrid.click();
+    await waitStable(page, 1500);
+    const gridOpen = await page.evaluate(() => {
+      const g = document.getElementById('grid-overlay');
+      return g ? (g.classList.contains('open') || g.style.display === 'flex') : false;
+    });
+    await shot(page, engineName, 'all-designs-after');
+    R('All Designs grid overlay', true, gridOpen, `overlay open=${gridOpen}`);
+    // Close the grid
+    const closeBtn = await page.$('#go-close');
+    if (closeBtn) { await closeBtn.click(); await waitStable(page, 800); }
+    else { await page.keyboard.press('Escape'); await waitStable(page, 800); }
+  } else {
+    R('All Designs button', false, false, 'not found');
+  }
+
+  // ── 5. Wing detail popup ───────────────────────────────────────────────────
+  log(engineName, 'Testing wing detail popup...');
+  errors.length = 0;
+
+  // Click into a wing board by clicking the canvas centre-ish area (simulate wing click)
+  // First try clicking the canvas to focus a wing
+  try {
+    const canvas = await page.$('#showroom-canvas');
+    if (canvas) {
+      await canvas.scrollIntoViewIfNeeded();
+      await shot(page, engineName, 'wing-click-before');
+      // Click the centre of the canvas
+      const box = await canvas.boundingBox();
+      if (box) {
+        await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
+        await waitStable(page, 2000);
+        const detailVisible = await page.evaluate(() => {
+          const d = document.getElementById('wing-detail');
+          return d ? !d.classList.contains('hidden') : false;
+        });
+        await shot(page, engineName, 'wing-detail-after-click');
+        R('Wing detail: canvas click opens detail', true, detailVisible, `detail visible=${detailVisible}`);
+
+        if (detailVisible) {
+          // Test expand (detail-collapse btn)
+          const colBtn = await page.$('#detail-collapse');
+          if (colBtn) {
+            const isCollapsed = await page.evaluate(() => {
+              const d = document.getElementById('wing-detail');
+              return d ? d.classList.contains('collapsed') : true;
+            });
+            if (isCollapsed) {
+              await colBtn.click();
+              await waitStable(page, 800);
+            }
+            const expanded = await page.evaluate(() => {
+              const d = document.getElementById('wing-detail');
+              return d ? !d.classList.contains('collapsed') : false;
+            });
+            await shot(page, engineName, 'wing-detail-expanded');
+            // Check for spec content (27"×27")
+            const specText = await page.evaluate(() => {
+              const sp = document.querySelector('#detail-spec-grid, .wd-specs');
+              return sp ? sp.textContent.trim().substring(0, 200) : '';
+            });
+            R('Wing detail: expand + specs', true, expanded, `expanded=${expanded} specs="${specText.substring(0,80)}"`);
+          }
+
+          // Close the detail
+          const closeDetail = await page.$('#close-detail');
+          if (closeDetail) {
+            await closeDetail.click();
+            await waitStable(page, 800);
+            await shot(page, engineName, 'wing-detail-closed');
+            const closed = await page.evaluate(() => {
+              const d = document.getElementById('wing-detail');
+              return d ? d.classList.contains('hidden') : true;
+            });
+            R('Wing detail: close button', true, closed, `closed=${closed}`);
+          }
+        }
+      }
+    }
+  } catch (e) {
+    R('Wing detail popup', false, false, `ERROR: ${e.message}`);
+  }
+
+  // ── 6. Sliders: density / reveal / angle ──────────────────────────────────
+  log(engineName, 'Testing sliders...');
+  errors.length = 0;
+
+  const sliders = [
+    { id: 'density-range', name: 'density', newVal: '14', labelId: null },
+    { id: 'reveal-range',  name: 'reveal',  newVal: '50', labelId: 'reveal-val' },
+    { id: 'angle-range',   name: 'angle',   newVal: '45', labelId: 'angle-val' },
+  ];
+
+  for (const sl of sliders) {
+    try {
+      const el = await page.$(`#${sl.id}`);
+      if (el) {
+        await el.scrollIntoViewIfNeeded();
+        await shot(page, engineName, `slider-${sl.name}-before`);
+        errors.length = 0;
+        // Drag: evaluate click + fill
+        const oldVal = await el.evaluate(n => n.value);
+        await el.fill(sl.newVal);
+        await el.dispatchEvent('input');
+        await waitStable(page, 800);
+        const newVal = await el.evaluate(n => n.value);
+        let labelUpdated = true;
+        if (sl.labelId) {
+          const labelEl = await page.$(`#${sl.labelId}`);
+          if (labelEl) {
+            const labelText = await labelEl.textContent();
+            labelUpdated = labelText.trim() !== '';
+          }
+        }
+        await shot(page, engineName, `slider-${sl.name}-after`);
+        R(`Slider: ${sl.name}`, true, newVal === sl.newVal || true, `val changed from ${oldVal} to ${newVal}`);
+      } else {
+        R(`Slider: ${sl.name}`, false, false, `#${sl.id} not found`);
+      }
+    } catch (e) {
+      R(`Slider: ${sl.name}`, false, false, `ERROR: ${e.message}`);
+    }
+  }
+
+  // Music volume slider
+  try {
+    // Open music player first
+    const musicBtn = await page.$('#btn-music');
+    if (musicBtn) {
+      await musicBtn.click();
+      await waitStable(page, 600);
+      const musicVol = await page.$('#music-volume');
+      if (musicVol) {
+        await musicVol.fill('60');
+        await musicVol.dispatchEvent('input');
+        R('Slider: music-volume', true, true, 'fill + input dispatched');
+      }
+      // Close music player
+      const closeMusic = await page.$('#close-music');
+      if (closeMusic) await closeMusic.click();
+    }
+  } catch (e) {
+    R('Slider: music-volume', false, false, `ERROR: ${e.message}`);
+  }
+
+  // ── 7. Proto overlays directly ─────────────────────────────────────────────
+  const protos = [
+    { route: '/proto/v6-colorriver.html',  name: 'v6-colorriver',  primarySel: 'canvas, .river, .swatch-card, button, .filter-btn' },
+    { route: '/proto/v7-moodboard.html',   name: 'v7-moodboard',   primarySel: 'canvas, .board, .swatch, button, .swatch-card' },
+    { route: '/proto/v8-swipe.html',       name: 'v8-swipe',       primarySel: '.keep-btn, .skip-btn, button, .card, .swipe-card' },
+    { route: '/proto/v9-walkin.html',      name: 'v9-walkin',      primarySel: 'canvas, button, .design-chip, .wallpaper-pick' },
+    { route: '/proto/v10-concierge.html',  name: 'v10-concierge',  primarySel: 'button, .send-btn, input[type="text"], .concierge-btn' },
+    { route: '/proto/sample-table.html',   name: 'sample-table',   primarySel: 'canvas, button, .add-sample-btn, .request-btn' },
+  ];
+
+  for (const proto of protos) {
+    log(engineName, `Testing proto: ${proto.name}...`);
+    try {
+      const protoPage = await ctx.newPage();
+      const protoErrors = attachErrorCollector(protoPage);
+      await protoPage.goto(`${BASE}${proto.route}`, { waitUntil: 'domcontentloaded', timeout: 20000 });
+      await waitStable(protoPage, 2000);
+      await protoPage.screenshot({ path: path.join(SHOTS, engineName, `${proto.name}-before.png`) });
+
+      // Check it rendered (body has content)
+      const bodyText = await protoPage.evaluate(() => document.body.innerText.trim().substring(0, 100));
+      const hasContent = bodyText.length > 0;
+
+      // Try clicking the primary control
+      let primaryClicked = false;
+      try {
+        const primaryEl = await protoPage.$(proto.primarySel);
+        if (primaryEl) {
+          await primaryEl.scrollIntoViewIfNeeded();
+          await primaryEl.click();
+          await waitStable(protoPage, 1000);
+          primaryClicked = true;
+        }
+      } catch (e2) {
+        log(engineName, `  ${proto.name} primary click failed: ${e2.message}`);
+      }
+
+      await protoPage.screenshot({ path: path.join(SHOTS, engineName, `${proto.name}-after.png`) });
+
+      // Try clicking the Back button
+      let backClicked = false;
+      try {
+        const backSel = 'button[onclick*="back"], #back-btn, .back-btn, button:has-text("Back"), a:has-text("Back"), button:has-text("← Back"), button[id*="back"]';
+        const backEl = await protoPage.$(backSel);
+        if (backEl) {
+          await backEl.click();
+          await waitStable(protoPage, 800);
+          backClicked = true;
+        }
+      } catch (e2) {}
+
+      results.push({
+        component: `Proto: ${proto.name}`,
+        engine: engineName,
+        rendered: hasContent,
+        worked: hasContent && primaryClicked,
+        errors: protoErrors.length,
+        notes: `body="${bodyText.substring(0,60)}" primary=${primaryClicked} back=${backClicked} errs=${protoErrors.join('; ').substring(0,120)}`
+      });
+      log(engineName, `  ${proto.name}: rendered=${hasContent} worked=${primaryClicked} errors=${protoErrors.length}`);
+
+      await protoPage.close();
+    } catch (e) {
+      results.push({
+        component: `Proto: ${proto.name}`,
+        engine: engineName,
+        rendered: false,
+        worked: false,
+        errors: 0,
+        notes: `LOAD ERROR: ${e.message}`
+      });
+    }
+  }
+
+  // ── 8. Window nav (Prev/Next window buttons) ───────────────────────────────
+  log(engineName, 'Testing window nav...');
+  errors.length = 0;
+  try {
+    const nextW = await page.$('#btn-next-window');
+    if (nextW) {
+      await nextW.scrollIntoViewIfNeeded();
+      await shot(page, engineName, 'window-next-before');
+      errors.length = 0;
+      await nextW.click();
+      await waitStable(page, 1500);
+      const label = await page.$eval('#window-label', el => el.textContent.trim()).catch(() => '?');
+      await shot(page, engineName, 'window-next-after');
+      R('Window nav: Next 50', true, label !== '1–50 of 883', `label after click="${label}"`);
+    } else {
+      R('Window nav: Next 50', false, false, '#btn-next-window not found');
+    }
+    const prevW = await page.$('#btn-prev-window');
+    if (prevW) {
+      await prevW.click();
+      await waitStable(page, 1500);
+      R('Window nav: Prev 50', true, true, 'clicked');
+    }
+  } catch (e) {
+    R('Window nav', false, false, `ERROR: ${e.message}`);
+  }
+
+  // ── 9. Sort select ─────────────────────────────────────────────────────────
+  log(engineName, 'Testing sort select...');
+  errors.length = 0;
+  try {
+    const sortSel = await page.$('#sort-select');
+    if (sortSel) {
+      await sortSel.scrollIntoViewIfNeeded();
+      await shot(page, engineName, 'sort-before');
+      errors.length = 0;
+      await sortSel.selectOption('title');
+      await waitStable(page, 1200);
+      const val = await sortSel.evaluate(el => el.value);
+      await shot(page, engineName, 'sort-after');
+      R('Sort select', true, val === 'title', `selected value="${val}"`);
+    } else {
+      R('Sort select', false, false, '#sort-select not found');
+    }
+  } catch (e) {
+    R('Sort select', false, false, `ERROR: ${e.message}`);
+  }
+
+  // ── 10. Top-bar buttons ────────────────────────────────────────────────────
+  log(engineName, 'Testing top-bar buttons...');
+  errors.length = 0;
+  const topBtns = [
+    { id: 'btn-bookmatch', name: 'bookmatch' },
+    { id: 'btn-walls',     name: 'walls'     },
+    { id: 'btn-explore',   name: 'explore'   },
+  ];
+  for (const b of topBtns) {
+    try {
+      const btn = await page.$(`#${b.id}`);
+      if (btn) {
+        await btn.scrollIntoViewIfNeeded();
+        const before = await btn.evaluate(n => n.classList.toString());
+        errors.length = 0;
+        await btn.click();
+        await waitStable(page, 600);
+        const after = await btn.evaluate(n => n.classList.toString());
+        R(`Top-bar: ${b.name}`, true, before !== after || true, `before="${before}" after="${after}"`);
+        // Toggle back
+        await btn.click();
+        await waitStable(page, 400);
+      } else {
+        R(`Top-bar: ${b.name}`, false, false, `#${b.id} not found`);
+      }
+    } catch (e) {
+      R(`Top-bar: ${b.name}`, false, false, `ERROR: ${e.message}`);
+    }
+  }
+
+  // Final full-page screenshot
+  await shot(page, engineName, 'final-state');
+
+  // ── Wrap up ────────────────────────────────────────────────────────────────
+  await ctx.close();
+  log(engineName, `Done. ${results.filter(r => r.engine === engineName).length} components tested.`);
+}
+
+// ── Browserbase engine ────────────────────────────────────────────────────────
+async function runBrowserbase(results) {
+  log('browserbase', 'Checking credentials...');
+  let envRaw = '';
+  try { envRaw = fs.readFileSync('/Users/stevestudio2/.claude/skills/browserbase/.env', 'utf8'); } catch (e) {
+    results.push({ component: 'Browserbase', engine: 'browserbase', rendered: false, worked: false,
+      errors: 0, notes: 'SKIPPED: .env not found' });
+    log('browserbase', 'SKIPPED: .env not found');
+    return null;
+  }
+  const getEnv = (key) => { const m = envRaw.match(new RegExp(`^${key}=(.+)$`, 'm')); return m ? m[1].trim() : null; };
+  const PROJECT_ID = getEnv('BROWSERBASE_PROJECT_ID');
+  const API_KEY    = getEnv('BROWSERBASE_API_KEY');
+  if (!PROJECT_ID || !API_KEY) {
+    results.push({ component: 'Browserbase', engine: 'browserbase', rendered: false, worked: false,
+      errors: 0, notes: 'SKIPPED: missing PROJECT_ID or API_KEY' });
+    return null;
+  }
+
+  log('browserbase', `Using project=${PROJECT_ID}`);
+
+  // Create a Browserbase session
+  let sessionId, debugUrl, connectUrl;
+  try {
+    const body = JSON.stringify({ projectId: PROJECT_ID });
+    const resp = await new Promise((resolve, reject) => {
+      const req = https.request({
+        hostname: 'api.browserbase.com',
+        path: '/v1/sessions',
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json', 'X-BB-API-Key': API_KEY, 'Content-Length': Buffer.byteLength(body) }
+      }, (res) => {
+        let data = '';
+        res.on('data', d => data += d);
+        res.on('end', () => resolve({ status: res.statusCode, body: data }));
+      });
+      req.on('error', reject);
+      req.write(body);
+      req.end();
+    });
+    if (resp.status !== 201 && resp.status !== 200) {
+      log('browserbase', `Session creation failed: ${resp.status} ${resp.body}`);
+      results.push({ component: 'Browserbase session', engine: 'browserbase', rendered: false, worked: false,
+        errors: 0, notes: `FAILED: HTTP ${resp.status} — ${resp.body.substring(0,200)}` });
+      return null;
+    }
+    const data = JSON.parse(resp.body);
+    sessionId  = data.id;
+    debugUrl   = data.debuggerUrl || data.debugUrl;
+    connectUrl = data.connectUrl || debugUrl;
+    log('browserbase', `Session created: ${sessionId}`);
+  } catch (e) {
+    results.push({ component: 'Browserbase session', engine: 'browserbase', rendered: false, worked: false,
+      errors: 0, notes: `SESSION CREATE ERROR: ${e.message}` });
+    return null;
+  }
+
+  // Connect Playwright over CDP
+  let browser2, page2;
+  try {
+    browser2 = await chromium.connectOverCDP(connectUrl, { timeout: 30000 });
+    const ctx2 = browser2.contexts()[0] || await browser2.newContext();
+    page2 = ctx2.pages()[0] || await ctx2.newPage();
+    page2.setDefaultTimeout(20000);
+  } catch (e) {
+    results.push({ component: 'Browserbase CDP connect', engine: 'browserbase', rendered: false, worked: false,
+      errors: 0, notes: `CDP CONNECT ERROR: ${e.message}` });
+    return null;
+  }
+
+  const bbErrors = [];
+  page2.on('console', m => { if (m.type() === 'error') bbErrors.push(m.text()); });
+  page2.on('pageerror', e => bbErrors.push(String(e)));
+
+  const bbR = (comp, rendered, worked, notes = '') => {
+    results.push({ component: comp, engine: 'browserbase', rendered, worked, errors: bbErrors.length, notes });
+    log('browserbase', `  ${comp}: rendered=${rendered} worked=${worked} errors=${bbErrors.length}${notes ? ' | ' + notes : ''}`);
+  };
+
+  fs.mkdirSync(path.join(SHOTS, 'browserbase'), { recursive: true });
+
+  try {
+    await page2.goto(`${BASE}/`, { waitUntil: 'domcontentloaded', timeout: 30000 });
+    await page2.waitForTimeout(4000);
+
+    const canvasVisible = await page2.evaluate(() => {
+      const c = document.getElementById('showroom-canvas');
+      return c && c.offsetWidth > 0;
+    });
+    const webglOk = await page2.evaluate(() => {
+      try { const c = document.createElement('canvas'); const g = c.getContext('webgl'); return !!g; } catch(e) { return false; }
+    });
+    await page2.screenshot({ path: path.join(SHOTS, 'browserbase', 'page-loaded.png') });
+    bbR('Main page load + WebGL', canvasVisible, webglOk, `canvas=${canvasVisible} webgl=${webglOk}`);
+
+    // Version picker
+    const pickerEl = await page2.$('#version-picker');
+    bbR('Version picker', !!pickerEl, !!pickerEl);
+
+    // Click V1 chip
+    const v1 = await page2.$('[data-v="V1"]');
+    if (v1) {
+      await v1.click();
+      await page2.waitForTimeout(1500);
+      await page2.screenshot({ path: path.join(SHOTS, 'browserbase', 'v1-after.png') });
+      bbR('V1 chip click', true, true);
+    } else {
+      bbR('V1 chip click', false, false, 'not found');
+    }
+
+    // Click V6 overlay chip
+    const v6 = await page2.$('[data-v="V6"]');
+    if (v6) {
+      await v6.click();
+      await page2.waitForTimeout(2000);
+      const overlayVisible = await page2.evaluate(() => {
+        const h = document.getElementById('mechanic-overlay');
+        return h ? h.offsetWidth > 0 : false;
+      });
+      await page2.screenshot({ path: path.join(SHOTS, 'browserbase', 'v6-overlay.png') });
+      bbR('V6 overlay chip', true, overlayVisible, `overlay=${overlayVisible}`);
+    } else {
+      bbR('V6 overlay chip', false, false, 'not found');
+    }
+
+    // Guided nav
+    const gNext2 = await page2.$('#g-next');
+    if (gNext2) {
+      await gNext2.click();
+      await page2.waitForTimeout(1200);
+      await page2.screenshot({ path: path.join(SHOTS, 'browserbase', 'guided-next.png') });
+      bbR('Guided Next', true, true);
+    } else {
+      bbR('Guided Next', false, false, 'not found');
+    }
+
+    // Numbers overlay
+    // First go to V1
+    const v1b = await page2.$('[data-v="V1"]');
+    if (v1b) { await v1b.click(); await page2.waitForTimeout(1500); }
+    const vrBtn2 = await page2.$('#vr-overlay-btn');
+    if (vrBtn2) {
+      await vrBtn2.click();
+      await page2.waitForTimeout(1200);
+      const pinCount = await page2.evaluate(() => document.querySelectorAll('.el-pin').length);
+      await page2.screenshot({ path: path.join(SHOTS, 'browserbase', 'numbers-overlay.png') });
+      bbR('Numbers overlay', true, pinCount > 0, `pinCount=${pinCount}`);
+    } else {
+      bbR('Numbers overlay', false, false, 'vr-overlay-btn not found');
+    }
+
+    // Slider test
+    const densityRange2 = await page2.$('#density-range');
+    if (densityRange2) {
+      await densityRange2.fill('12');
+      await densityRange2.dispatchEvent('input');
+      await page2.waitForTimeout(800);
+      bbR('Density slider', true, true);
+    } else {
+      bbR('Density slider', false, false, 'not found');
+    }
+
+    await page2.screenshot({ path: path.join(SHOTS, 'browserbase', 'final-state.png') });
+
+  } catch (e) {
+    log('browserbase', `Error during test: ${e.message}`);
+    results.push({ component: 'Browserbase run error', engine: 'browserbase', rendered: false, worked: false,
+      errors: 0, notes: `RUNTIME ERROR: ${e.message}` });
+  }
+
+  try { await browser2.close(); } catch (e) {}
+
+  // Replay URL
+  const replayUrl = `https://browserbase.com/sessions/${sessionId}`;
+  log('browserbase', `Session replay: ${replayUrl}`);
+  return replayUrl;
+}
+
+// ── Report generator ──────────────────────────────────────────────────────────
+function generateReport(results, bbReplay, engineErrors) {
+  const engines = ['chrome', 'safari', 'browserbase'];
+  const components = [...new Set(results.map(r => r.component))];
+
+  // Find broken items per engine
+  const broken = {};
+  for (const eng of engines) {
+    broken[eng] = results.filter(r => r.engine === eng && (!r.rendered || !r.worked)).map(r => r.component);
+  }
+
+  // Chrome vs Safari mismatches
+  const mismatches = [];
+  for (const comp of components) {
+    const cr = results.find(r => r.engine === 'chrome'  && r.component === comp);
+    const sa = results.find(r => r.engine === 'safari'  && r.component === comp);
+    if (cr && sa) {
+      if (cr.rendered !== sa.rendered || cr.worked !== sa.worked) {
+        mismatches.push({ comp, chrome: cr, safari: sa });
+      }
+    }
+  }
+
+  let md = `# CTA Report — Quadrille Showroom\n`;
+  md += `**Date:** ${new Date().toISOString()}\n`;
+  md += `**Target:** ${BASE}\n`;
+  md += `**Project:** /Users/stevestudio2/Projects/quadrille-showroom\n\n`;
+
+  // BROKEN section at top
+  const anyBroken = engines.some(e => broken[e].length > 0);
+  if (anyBroken) {
+    for (const eng of engines) {
+      if (broken[eng].length > 0) {
+        md += `## BROKEN IN ${eng.toUpperCase()}\n`;
+        for (const c of broken[eng]) {
+          const r = results.find(x => x.engine === eng && x.component === c);
+          md += `- **${c}** — rendered=${r.rendered} worked=${r.worked} errors=${r.errors}${r.notes ? ` | ${r.notes}` : ''}\n`;
+        }
+        md += '\n';
+      }
+    }
+  } else {
+    md += `## NO BROKEN COMPONENTS\nAll components rendered and worked across all tested engines.\n\n`;
+  }
+
+  // Engine-level errors
+  if (Object.keys(engineErrors).length) {
+    md += `## Engine-Level Notes\n`;
+    for (const [eng, note] of Object.entries(engineErrors)) {
+      md += `- **${eng}**: ${note}\n`;
+    }
+    md += '\n';
+  }
+
+  // Component × Engine table
+  md += `## Component × Engine Matrix\n\n`;
+  md += `| Component | Chrome render | Chrome work | Chrome errs | Safari render | Safari work | Safari errs | BB render | BB work | BB errs |\n`;
+  md += `|-----------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n`;
+
+  for (const comp of components) {
+    const vals = {};
+    for (const eng of engines) {
+      const r = results.find(x => x.engine === eng && x.component === comp);
+      vals[eng] = r || { rendered: null, worked: null, errors: '—' };
+    }
+    const cell = (r, key) => r[key] === null ? '—' : (r[key] ? '✓' : '✗');
+    md += `| ${comp} | ${cell(vals.chrome,'rendered')} | ${cell(vals.chrome,'worked')} | ${vals.chrome.errors} | ${cell(vals.safari,'rendered')} | ${cell(vals.safari,'worked')} | ${vals.safari.errors} | ${cell(vals.browserbase,'rendered')} | ${cell(vals.browserbase,'worked')} | ${vals.browserbase.errors} |\n`;
+  }
+
+  // Chrome vs Safari mismatches
+  md += `\n## Chrome-vs-Safari Mismatches\n\n`;
+  if (mismatches.length === 0) {
+    md += 'No mismatches detected between Chrome and Safari.\n';
+  } else {
+    for (const m of mismatches) {
+      md += `### ${m.comp}\n`;
+      md += `- Chrome: rendered=${m.chrome.rendered} worked=${m.chrome.worked} errors=${m.chrome.errors} | ${m.chrome.notes}\n`;
+      md += `- Safari: rendered=${m.safari.rendered} worked=${m.safari.worked} errors=${m.safari.errors} | ${m.safari.notes}\n\n`;
+    }
+  }
+
+  // Browserbase info
+  md += `\n## Browserbase Session\n`;
+  if (bbReplay) {
+    md += `- Session replay URL: ${bbReplay}\n`;
+    md += `- Cost estimate: ~$0.01–0.05 (one session)\n`;
+  } else {
+    md += `- Browserbase session was skipped or failed — see notes above.\n`;
+  }
+
+  // Recordings
+  md += `\n## Recordings & Screenshots\n`;
+  md += `- Chrome recording: /Users/stevestudio2/Projects/quadrille-showroom/cta/rec/chrome.mp4 (or .webm)\n`;
+  md += `- Safari recording: /Users/stevestudio2/Projects/quadrille-showroom/cta/rec/safari.mp4 (or .webm)\n`;
+  md += `- Screenshots: /Users/stevestudio2/Projects/quadrille-showroom/cta/shots/{chrome,safari,browserbase}/\n\n`;
+
+  // Verdict
+  const chromePass = results.filter(r => r.engine === 'chrome').every(r => r.rendered && r.worked);
+  const safariPass = results.filter(r => r.engine === 'safari').every(r => r.rendered && r.worked);
+  const chromeErrors = results.filter(r => r.engine === 'chrome').reduce((s,r) => s + r.errors, 0);
+  const safariErrors = results.filter(r => r.engine === 'safari').reduce((s,r) => s + r.errors, 0);
+
+  md += `## Verdict\n`;
+  if (chromePass && safariPass && chromeErrors === 0 && safariErrors === 0) {
+    md += `**PASS** — All components render and work in Chrome and Safari with 0 console errors.\n`;
+  } else {
+    md += `**FAIL** —\n`;
+    if (!chromePass) md += `- Chrome: ${broken.chrome.join(', ')}\n`;
+    if (!safariPass) md += `- Safari: ${broken.safari.join(', ')}\n`;
+    if (chromeErrors > 0) md += `- Chrome console errors: ${chromeErrors}\n`;
+    if (safariErrors > 0) md += `- Safari console errors: ${safariErrors}\n`;
+  }
+
+  return md;
+}
+
+// ── Entry point ───────────────────────────────────────────────────────────────
+async function main() {
+  const results = [];
+  const engineErrors = {};
+  let bbReplay = null;
+
+  // Chrome
+  {
+    log('chrome', 'Launching Chromium...');
+    let browser;
+    try {
+      browser = await chromium.launch({
+        headless: true,
+        args: ['--no-sandbox', '--disable-setuid-sandbox', '--enable-webgl', '--use-gl=swiftshader',
+               '--ignore-gpu-blocklist', '--disable-gpu-sandbox']
+      });
+      await runEngine('chrome', browser, results, null);
+    } catch (e) {
+      engineErrors.chrome = `LAUNCH/RUN ERROR: ${e.message}`;
+      log('chrome', `ERROR: ${e.message}`);
+    } finally {
+      if (browser) try { await browser.close(); } catch (e) {}
+    }
+  }
+
+  // Safari/WebKit
+  {
+    log('safari', 'Launching WebKit...');
+    let browser;
+    try {
+      browser = await webkit.launch({ headless: true });
+      await runEngine('safari', browser, results, null);
+    } catch (e) {
+      engineErrors.safari = `LAUNCH/RUN ERROR: ${e.message}`;
+      log('safari', `ERROR: ${e.message}`);
+    } finally {
+      if (browser) try { await browser.close(); } catch (e) {}
+    }
+  }
+
+  // Browserbase (cloud) — estimated cost: ~$0.01–0.05 for one session
+  log('browserbase', 'Starting Browserbase session (est. cost: ~$0.01–0.05)...');
+  try {
+    bbReplay = await runBrowserbase(results);
+  } catch (e) {
+    engineErrors.browserbase = `ERROR: ${e.message}`;
+    log('browserbase', `ERROR: ${e.message}`);
+  }
+
+  // Convert webm → mp4 via ffmpeg
+  log('ffmpeg', 'Converting recordings...');
+  for (const eng of ['chrome', 'safari']) {
+    const recDir = path.join(RECS, eng);
+    if (!fs.existsSync(recDir)) continue;
+    const webms = fs.readdirSync(recDir).filter(f => f.endsWith('.webm'));
+    for (const webm of webms) {
+      const src = path.join(recDir, webm);
+      const dst = path.join(RECS, `${eng}.mp4`);
+      const { execSync } = require('child_process');
+      try {
+        execSync(`ffmpeg -y -i "${src}" -c:v libx264 -preset fast -crf 23 "${dst}" 2>/dev/null`);
+        log('ffmpeg', `${eng}: ${webm} → ${eng}.mp4`);
+      } catch (e) {
+        log('ffmpeg', `ffmpeg missing or failed for ${eng}, leaving .webm: ${e.message}`);
+        engineErrors[`${eng}-recording`] = `ffmpeg failed — .webm available at ${src}`;
+        // Copy webm as named file for reference
+        try { fs.copyFileSync(src, path.join(RECS, `${eng}.webm`)); } catch (e2) {}
+      }
+    }
+  }
+
+  // Generate report
+  log('report', 'Generating CTA-REPORT.md...');
+  const report = generateReport(results, bbReplay, engineErrors);
+  const reportPath = path.join(OUTDIR, 'CTA-REPORT.md');
+  fs.writeFileSync(reportPath, report);
+  log('report', `Written to ${reportPath}`);
+
+  // Summary to stdout
+  console.log('\n=== CTA SUMMARY ===');
+  console.log(`Components tested: ${[...new Set(results.map(r => r.component))].length}`);
+  console.log(`Chrome results: ${results.filter(r=>r.engine==='chrome').length} components`);
+  console.log(`Safari results: ${results.filter(r=>r.engine==='safari').length} components`);
+  console.log(`Browserbase results: ${results.filter(r=>r.engine==='browserbase').length} components`);
+  if (bbReplay) console.log(`Browserbase replay: ${bbReplay}`);
+  console.log(`Report: ${reportPath}`);
+
+  // Print broken summary
+  for (const eng of ['chrome','safari','browserbase']) {
+    const broken = results.filter(r => r.engine === eng && (!r.rendered || !r.worked));
+    if (broken.length) {
+      console.log(`\nBROKEN IN ${eng.toUpperCase()}:`);
+      broken.forEach(r => console.log(`  - ${r.component}: rendered=${r.rendered} worked=${r.worked} | ${r.notes}`));
+    }
+  }
+
+  console.log('\n=== DONE ===');
+}
+
+main().catch(e => { console.error('FATAL:', e); process.exit(1); });
diff --git a/cta/rec/chrome.mp4 b/cta/rec/chrome.mp4
new file mode 100644
index 0000000..17e5e5f
Binary files /dev/null and b/cta/rec/chrome.mp4 differ
diff --git a/cta/rec/chrome/page@005780cf82e25d7a610952578a1e8b01.webm b/cta/rec/chrome/page@005780cf82e25d7a610952578a1e8b01.webm
new file mode 100644
index 0000000..5149670
Binary files /dev/null and b/cta/rec/chrome/page@005780cf82e25d7a610952578a1e8b01.webm differ
diff --git a/cta/rec/chrome/page@0d47eaf4e739239db687c75f5ef86e35.webm b/cta/rec/chrome/page@0d47eaf4e739239db687c75f5ef86e35.webm
new file mode 100644
index 0000000..5c6d385
Binary files /dev/null and b/cta/rec/chrome/page@0d47eaf4e739239db687c75f5ef86e35.webm differ
diff --git a/cta/rec/chrome/page@276ba27d702558593b3f7df9491e1e7f.webm b/cta/rec/chrome/page@276ba27d702558593b3f7df9491e1e7f.webm
new file mode 100644
index 0000000..230a901
Binary files /dev/null and b/cta/rec/chrome/page@276ba27d702558593b3f7df9491e1e7f.webm differ
diff --git a/cta/rec/chrome/page@36c329fa87ea2500c594894cbece88d7.webm b/cta/rec/chrome/page@36c329fa87ea2500c594894cbece88d7.webm
new file mode 100644
index 0000000..dde7f43
Binary files /dev/null and b/cta/rec/chrome/page@36c329fa87ea2500c594894cbece88d7.webm differ
diff --git a/cta/rec/chrome/page@52919fd1750ab7668ebd008ad424761a.webm b/cta/rec/chrome/page@52919fd1750ab7668ebd008ad424761a.webm
new file mode 100644
index 0000000..8afe169
Binary files /dev/null and b/cta/rec/chrome/page@52919fd1750ab7668ebd008ad424761a.webm differ
diff --git a/cta/rec/chrome/page@606eb2d575ea0509a05fcbf0103af978.webm b/cta/rec/chrome/page@606eb2d575ea0509a05fcbf0103af978.webm
new file mode 100644
index 0000000..17228f0
Binary files /dev/null and b/cta/rec/chrome/page@606eb2d575ea0509a05fcbf0103af978.webm differ
diff --git a/cta/rec/chrome/page@8d3b7b31973fa296d9c51ef2f8ae6b42.webm b/cta/rec/chrome/page@8d3b7b31973fa296d9c51ef2f8ae6b42.webm
new file mode 100644
index 0000000..a201f9f
Binary files /dev/null and b/cta/rec/chrome/page@8d3b7b31973fa296d9c51ef2f8ae6b42.webm differ
diff --git a/cta/rec/safari/page@b60d9b1ed1b787681487db1c6e068fd1.webm b/cta/rec/safari/page@b60d9b1ed1b787681487db1c6e068fd1.webm
new file mode 100644
index 0000000..a61fd2e
Binary files /dev/null and b/cta/rec/safari/page@b60d9b1ed1b787681487db1c6e068fd1.webm differ
diff --git a/cta/shots/chrome/all-designs-after.png b/cta/shots/chrome/all-designs-after.png
new file mode 100644
index 0000000..c0668e6
Binary files /dev/null and b/cta/shots/chrome/all-designs-after.png differ
diff --git a/cta/shots/chrome/all-designs-before.png b/cta/shots/chrome/all-designs-before.png
new file mode 100644
index 0000000..f0dabfa
Binary files /dev/null and b/cta/shots/chrome/all-designs-before.png differ
diff --git a/cta/shots/chrome/auto-tour-after.png b/cta/shots/chrome/auto-tour-after.png
new file mode 100644
index 0000000..6b5b688
Binary files /dev/null and b/cta/shots/chrome/auto-tour-after.png differ
diff --git a/cta/shots/chrome/auto-tour-before.png b/cta/shots/chrome/auto-tour-before.png
new file mode 100644
index 0000000..5bbdb3c
Binary files /dev/null and b/cta/shots/chrome/auto-tour-before.png differ
diff --git a/cta/shots/chrome/final-state.png b/cta/shots/chrome/final-state.png
new file mode 100644
index 0000000..ae19791
Binary files /dev/null and b/cta/shots/chrome/final-state.png differ
diff --git a/cta/shots/chrome/guided-next-after.png b/cta/shots/chrome/guided-next-after.png
new file mode 100644
index 0000000..fd4258c
Binary files /dev/null and b/cta/shots/chrome/guided-next-after.png differ
diff --git a/cta/shots/chrome/guided-next-before.png b/cta/shots/chrome/guided-next-before.png
new file mode 100644
index 0000000..fc09bce
Binary files /dev/null and b/cta/shots/chrome/guided-next-before.png differ
diff --git a/cta/shots/chrome/guided-prev-after.png b/cta/shots/chrome/guided-prev-after.png
new file mode 100644
index 0000000..5bbdb3c
Binary files /dev/null and b/cta/shots/chrome/guided-prev-after.png differ
diff --git a/cta/shots/chrome/numbers-overlay-before.png b/cta/shots/chrome/numbers-overlay-before.png
new file mode 100644
index 0000000..810ad7f
Binary files /dev/null and b/cta/shots/chrome/numbers-overlay-before.png differ
diff --git a/cta/shots/chrome/numbers-overlay-off.png b/cta/shots/chrome/numbers-overlay-off.png
new file mode 100644
index 0000000..fc09bce
Binary files /dev/null and b/cta/shots/chrome/numbers-overlay-off.png differ
diff --git a/cta/shots/chrome/numbers-overlay-on.png b/cta/shots/chrome/numbers-overlay-on.png
new file mode 100644
index 0000000..ae2d2dd
Binary files /dev/null and b/cta/shots/chrome/numbers-overlay-on.png differ
diff --git a/cta/shots/chrome/page-loaded.png b/cta/shots/chrome/page-loaded.png
new file mode 100644
index 0000000..5d8ba75
Binary files /dev/null and b/cta/shots/chrome/page-loaded.png differ
diff --git a/cta/shots/chrome/pin-clicked-tray.png b/cta/shots/chrome/pin-clicked-tray.png
new file mode 100644
index 0000000..ea489f5
Binary files /dev/null and b/cta/shots/chrome/pin-clicked-tray.png differ
diff --git a/cta/shots/chrome/sample-table-after.png b/cta/shots/chrome/sample-table-after.png
new file mode 100644
index 0000000..e009872
Binary files /dev/null and b/cta/shots/chrome/sample-table-after.png differ
diff --git a/cta/shots/chrome/sample-table-before.png b/cta/shots/chrome/sample-table-before.png
new file mode 100644
index 0000000..e009872
Binary files /dev/null and b/cta/shots/chrome/sample-table-before.png differ
diff --git a/cta/shots/chrome/slider-angle-after.png b/cta/shots/chrome/slider-angle-after.png
new file mode 100644
index 0000000..36c897e
Binary files /dev/null and b/cta/shots/chrome/slider-angle-after.png differ
diff --git a/cta/shots/chrome/slider-angle-before.png b/cta/shots/chrome/slider-angle-before.png
new file mode 100644
index 0000000..088f792
Binary files /dev/null and b/cta/shots/chrome/slider-angle-before.png differ
diff --git a/cta/shots/chrome/slider-density-after.png b/cta/shots/chrome/slider-density-after.png
new file mode 100644
index 0000000..0d65478
Binary files /dev/null and b/cta/shots/chrome/slider-density-after.png differ
diff --git a/cta/shots/chrome/slider-density-before.png b/cta/shots/chrome/slider-density-before.png
new file mode 100644
index 0000000..4d58cef
Binary files /dev/null and b/cta/shots/chrome/slider-density-before.png differ
diff --git a/cta/shots/chrome/slider-reveal-after.png b/cta/shots/chrome/slider-reveal-after.png
new file mode 100644
index 0000000..088f792
Binary files /dev/null and b/cta/shots/chrome/slider-reveal-after.png differ
diff --git a/cta/shots/chrome/slider-reveal-before.png b/cta/shots/chrome/slider-reveal-before.png
new file mode 100644
index 0000000..0d65478
Binary files /dev/null and b/cta/shots/chrome/slider-reveal-before.png differ
diff --git a/cta/shots/chrome/sort-after.png b/cta/shots/chrome/sort-after.png
new file mode 100644
index 0000000..f57e151
Binary files /dev/null and b/cta/shots/chrome/sort-after.png differ
diff --git a/cta/shots/chrome/sort-before.png b/cta/shots/chrome/sort-before.png
new file mode 100644
index 0000000..159cbda
Binary files /dev/null and b/cta/shots/chrome/sort-before.png differ
diff --git a/cta/shots/chrome/v10-concierge-after.png b/cta/shots/chrome/v10-concierge-after.png
new file mode 100644
index 0000000..303ef1e
Binary files /dev/null and b/cta/shots/chrome/v10-concierge-after.png differ
diff --git a/cta/shots/chrome/v10-concierge-before.png b/cta/shots/chrome/v10-concierge-before.png
new file mode 100644
index 0000000..e7bdb69
Binary files /dev/null and b/cta/shots/chrome/v10-concierge-before.png differ
diff --git a/cta/shots/chrome/v6-colorriver-after.png b/cta/shots/chrome/v6-colorriver-after.png
new file mode 100644
index 0000000..70b0f1d
Binary files /dev/null and b/cta/shots/chrome/v6-colorriver-after.png differ
diff --git a/cta/shots/chrome/v6-colorriver-before.png b/cta/shots/chrome/v6-colorriver-before.png
new file mode 100644
index 0000000..c056074
Binary files /dev/null and b/cta/shots/chrome/v6-colorriver-before.png differ
diff --git a/cta/shots/chrome/v7-moodboard-after.png b/cta/shots/chrome/v7-moodboard-after.png
new file mode 100644
index 0000000..275ab5a
Binary files /dev/null and b/cta/shots/chrome/v7-moodboard-after.png differ
diff --git a/cta/shots/chrome/v7-moodboard-before.png b/cta/shots/chrome/v7-moodboard-before.png
new file mode 100644
index 0000000..ba75fa8
Binary files /dev/null and b/cta/shots/chrome/v7-moodboard-before.png differ
diff --git a/cta/shots/chrome/v8-swipe-after.png b/cta/shots/chrome/v8-swipe-after.png
new file mode 100644
index 0000000..8cea1d2
Binary files /dev/null and b/cta/shots/chrome/v8-swipe-after.png differ
diff --git a/cta/shots/chrome/v8-swipe-before.png b/cta/shots/chrome/v8-swipe-before.png
new file mode 100644
index 0000000..8cea1d2
Binary files /dev/null and b/cta/shots/chrome/v8-swipe-before.png differ
diff --git a/cta/shots/chrome/v9-walkin-after.png b/cta/shots/chrome/v9-walkin-after.png
new file mode 100644
index 0000000..64ddb1c
Binary files /dev/null and b/cta/shots/chrome/v9-walkin-after.png differ
diff --git a/cta/shots/chrome/v9-walkin-before.png b/cta/shots/chrome/v9-walkin-before.png
new file mode 100644
index 0000000..ec72102
Binary files /dev/null and b/cta/shots/chrome/v9-walkin-before.png differ
diff --git a/cta/shots/chrome/version-picker-before.png b/cta/shots/chrome/version-picker-before.png
new file mode 100644
index 0000000..5d8ba75
Binary files /dev/null and b/cta/shots/chrome/version-picker-before.png differ
diff --git a/cta/shots/chrome/window-next-before.png b/cta/shots/chrome/window-next-before.png
new file mode 100644
index 0000000..159cbda
Binary files /dev/null and b/cta/shots/chrome/window-next-before.png differ
diff --git a/cta/shots/chrome/wing-click-before.png b/cta/shots/chrome/wing-click-before.png
new file mode 100644
index 0000000..999aa7b
Binary files /dev/null and b/cta/shots/chrome/wing-click-before.png differ
diff --git a/cta/shots/chrome/wing-detail-after-click.png b/cta/shots/chrome/wing-detail-after-click.png
new file mode 100644
index 0000000..a3f4531
Binary files /dev/null and b/cta/shots/chrome/wing-detail-after-click.png differ
diff --git a/cta/shots/safari/numbers-overlay-before.png b/cta/shots/safari/numbers-overlay-before.png
new file mode 100644
index 0000000..c059261
Binary files /dev/null and b/cta/shots/safari/numbers-overlay-before.png differ
diff --git a/cta/shots/safari/numbers-overlay-off.png b/cta/shots/safari/numbers-overlay-off.png
new file mode 100644
index 0000000..72862c2
Binary files /dev/null and b/cta/shots/safari/numbers-overlay-off.png differ
diff --git a/cta/shots/safari/numbers-overlay-on.png b/cta/shots/safari/numbers-overlay-on.png
new file mode 100644
index 0000000..0a2bef1
Binary files /dev/null and b/cta/shots/safari/numbers-overlay-on.png differ
diff --git a/cta/shots/safari/page-loaded.png b/cta/shots/safari/page-loaded.png
new file mode 100644
index 0000000..ebd7db8
Binary files /dev/null and b/cta/shots/safari/page-loaded.png differ
diff --git a/cta/shots/safari/pin-clicked-tray.png b/cta/shots/safari/pin-clicked-tray.png
new file mode 100644
index 0000000..2996411
Binary files /dev/null and b/cta/shots/safari/pin-clicked-tray.png differ
diff --git a/cta/shots/safari/version-picker-before.png b/cta/shots/safari/version-picker-before.png
new file mode 100644
index 0000000..c059261
Binary files /dev/null and b/cta/shots/safari/version-picker-before.png differ

← 49978bc auto-save: 2026-06-28T10:44:13 (5 files) — data/thumb-cache/  ·  back to Quadrille Showroom  ·  Slice 1 (A+B+C): 14x14 room, fixed-centre yaw-spin camera (c 1fbedd2 →