[object Object]

← back to Model Arena

Agent-driven game: DW This or That (frontend-developer) — 15-round taste game → taste profile + shareable result. Registered.

12b9b8df96891465ae52b20f91b609d77e97baa5 · 2026-07-25 11:57:13 -0700 · Steve Abrams

Files touched

Diff

commit 12b9b8df96891465ae52b20f91b609d77e97baa5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Jul 25 11:57:13 2026 -0700

    Agent-driven game: DW This or That (frontend-developer) — 15-round taste game → taste profile + shareable result. Registered.
---
 data/arcade-games.json             |   6 +
 public/games/dw-palette-mixer.html | 883 +++++++++++++++++++++++++++++++++++
 public/games/dw-this-or-that.html  | 922 +++++++++++++++++++++++++++++++++++++
 3 files changed, 1811 insertions(+)

diff --git a/data/arcade-games.json b/data/arcade-games.json
index 4e2a57e..1d6d5a9 100644
--- a/data/arcade-games.json
+++ b/data/arcade-games.json
@@ -95,6 +95,12 @@
       "title": "DW Pattern Runner",
       "credit": "frontend-developer",
       "note": "Agent-built endless-runner game \u2014 decorator dodges obstacles, collects rolls, wall papers grasscloth\u2192damask\u2192chinoiserie"
+    },
+    {
+      "slug": "dw-this-or-that",
+      "title": "DW This or That",
+      "credit": "frontend-developer",
+      "note": "Agent-built taste game \u2014 pick between two patterns 15 rounds, builds a taste profile + shareable result"
     }
   ]
 }
\ No newline at end of file
diff --git a/public/games/dw-palette-mixer.html b/public/games/dw-palette-mixer.html
new file mode 100644
index 0000000..3b4ca67
--- /dev/null
+++ b/public/games/dw-palette-mixer.html
@@ -0,0 +1,883 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Palette Mixer — Designer Wallcoverings</title>
+<style>
+:root{
+  --bg:#f4f1ea;--bg2:#ece8df;--bg3:#e4dfd4;
+  --ink:#1c1a17;--ink2:#4a453e;--ink3:#7a7368;
+  --accent:#c9a961;--accent2:#b8945a;--accent3:#e8c97a;
+  --surface:#fff;--surface2:#f8f5f0;
+  --border:#ddd8ce;--border2:#c8c2b6;
+  --serif:'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif;
+  --sans:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
+  --ease:cubic-bezier(.4,0,.2,1);
+  --r:8px;--r2:4px;
+  --shadow:0 2px 12px rgba(28,26,23,.10);
+  --shadow2:0 8px 32px rgba(28,26,23,.18);
+}
+*{box-sizing:border-box;margin:0;padding:0}
+body{background:var(--bg);color:var(--ink);font-family:var(--sans);min-height:100vh;overflow-x:hidden}
+
+/* HEADER */
+header{
+  display:flex;align-items:center;justify-content:space-between;
+  padding:18px 32px 14px;border-bottom:1px solid var(--border);
+  background:var(--bg);position:sticky;top:0;z-index:100;
+}
+.wordmark{
+  font-family:var(--serif);font-size:clamp(20px,2.5vw,28px);
+  font-weight:600;text-transform:uppercase;letter-spacing:.18em;
+  color:var(--ink);line-height:1;
+}
+.tagline{font-size:11px;letter-spacing:.12em;color:var(--ink3);margin-top:3px;text-transform:uppercase}
+.header-badge{
+  font-family:var(--serif);font-size:13px;letter-spacing:.08em;
+  color:var(--accent2);text-transform:uppercase;
+}
+
+/* LAYOUT */
+main{max-width:1100px;margin:0 auto;padding:28px 24px 60px}
+.layout{display:grid;grid-template-columns:320px 1fr;gap:28px}
+@media(max-width:800px){.layout{grid-template-columns:1fr}}
+
+/* PANELS */
+.panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:22px;box-shadow:var(--shadow)}
+.panel-title{
+  font-family:var(--serif);font-size:18px;font-weight:600;
+  letter-spacing:.08em;color:var(--ink);margin-bottom:16px;
+  text-transform:uppercase;
+}
+.panel-sub{font-size:11px;letter-spacing:.1em;color:var(--ink3);text-transform:uppercase;margin-bottom:12px}
+
+/* COLOR PICKER */
+.picker-wrap{position:relative}
+.color-field{
+  width:100%;height:180px;border-radius:var(--r2);cursor:crosshair;
+  position:relative;touch-action:none;border:1px solid var(--border);
+  flex-shrink:0;
+}
+.field-thumb{
+  position:absolute;width:18px;height:18px;
+  border-radius:50%;border:3px solid #fff;
+  box-shadow:0 0 0 1.5px rgba(0,0,0,.3),0 2px 6px rgba(0,0,0,.3);
+  transform:translate(-50%,-50%);pointer-events:none;transition:none;
+}
+.hue-rail{
+  width:100%;height:14px;border-radius:7px;margin-top:12px;
+  cursor:pointer;touch-action:none;position:relative;
+  background:linear-gradient(to right,
+    hsl(0,100%,50%),hsl(30,100%,50%),hsl(60,100%,50%),
+    hsl(90,100%,50%),hsl(120,100%,50%),hsl(150,100%,50%),
+    hsl(180,100%,50%),hsl(210,100%,50%),hsl(240,100%,50%),
+    hsl(270,100%,50%),hsl(300,100%,50%),hsl(330,100%,50%),hsl(360,100%,50%));
+  border:1px solid var(--border);
+}
+.hue-thumb{
+  position:absolute;top:50%;width:18px;height:18px;
+  border-radius:50%;border:3px solid #fff;
+  box-shadow:0 0 0 1.5px rgba(0,0,0,.3);
+  transform:translate(-50%,-50%);pointer-events:none;
+}
+.hex-row{display:flex;gap:8px;margin-top:12px;align-items:center}
+.hex-input{
+  flex:1;padding:7px 10px;border:1px solid var(--border);border-radius:var(--r2);
+  font-family:var(--sans);font-size:13px;color:var(--ink);background:var(--surface2);
+  letter-spacing:.06em;outline:none;
+}
+.hex-input:focus{border-color:var(--accent)}
+.hex-swatch{width:36px;height:36px;border-radius:var(--r2);border:1px solid var(--border);flex-shrink:0}
+
+/* MODE PILLS */
+.mode-row{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:18px}
+.pill{
+  padding:5px 13px;border-radius:20px;font-size:11px;
+  letter-spacing:.08em;text-transform:uppercase;cursor:pointer;
+  border:1px solid var(--border2);color:var(--ink2);background:transparent;
+  transition:all .2s var(--ease);font-family:var(--sans);
+}
+.pill:hover{border-color:var(--accent);color:var(--accent)}
+.pill.active{background:var(--accent);border-color:var(--accent);color:#fff}
+
+/* PALETTE STRIP */
+.palette-strip{display:flex;gap:10px;margin-bottom:18px}
+.swatch-card{
+  flex:1;border-radius:var(--r);overflow:hidden;
+  box-shadow:var(--shadow);position:relative;
+  display:flex;flex-direction:column;cursor:default;
+  transition:transform .18s var(--ease),box-shadow .18s var(--ease);
+  min-height:200px;
+}
+.swatch-card:hover{transform:translateY(-2px);box-shadow:var(--shadow2)}
+.swatch-texture{flex:1;min-height:100px;position:relative;overflow:hidden}
+.swatch-texture svg{width:100%;height:100%}
+.swatch-info{
+  background:var(--surface);padding:8px 10px;
+  border-top:1px solid var(--border);min-height:72px;
+}
+.swatch-name{
+  font-family:var(--serif);font-size:13px;font-weight:600;
+  color:var(--ink);letter-spacing:.04em;margin-bottom:2px;
+  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
+}
+.swatch-hex{font-size:10px;color:var(--ink3);letter-spacing:.06em;font-family:monospace}
+.swatch-actions{display:flex;gap:4px;margin-top:6px}
+.swatch-btn{
+  padding:2px 6px;border-radius:3px;font-size:10px;letter-spacing:.05em;
+  border:1px solid var(--border);background:transparent;color:var(--ink3);
+  cursor:pointer;transition:all .15s var(--ease);font-family:var(--sans);
+}
+.swatch-btn:hover{border-color:var(--accent);color:var(--accent)}
+.swatch-btn.locked{background:var(--accent);color:#fff;border-color:var(--accent)}
+.swatch-btn.copied{background:#4caf50;color:#fff;border-color:#4caf50}
+
+/* LIGHTNESS SLIDER per swatch */
+.swatch-slider{
+  -webkit-appearance:none;appearance:none;
+  width:100%;height:4px;border-radius:2px;outline:none;
+  background:var(--border);cursor:pointer;margin-top:6px;
+}
+.swatch-slider::-webkit-slider-thumb{
+  -webkit-appearance:none;appearance:none;
+  width:14px;height:14px;border-radius:50%;background:var(--accent);
+  border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,.2);cursor:pointer;
+}
+.swatch-slider::-moz-range-thumb{
+  width:14px;height:14px;border-radius:50%;background:var(--accent);
+  border:2px solid #fff;cursor:pointer;
+}
+
+/* PREVIEW STRIP */
+.preview-strip{display:flex;height:60px;border-radius:var(--r);overflow:hidden;border:1px solid var(--border);margin-bottom:16px}
+.preview-stripe{flex:1;transition:background .35s var(--ease)}
+
+/* HARMONY */
+.harmony-row{display:flex;gap:12px;align-items:center;margin-bottom:18px;flex-wrap:wrap}
+.harmony-badge{
+  padding:6px 14px;border-radius:20px;font-size:11px;
+  letter-spacing:.08em;text-transform:uppercase;
+  border:1px solid var(--border2);color:var(--ink2);background:var(--surface2);
+}
+.harmony-badge b{color:var(--accent2)}
+.contrast-bar-wrap{flex:1;min-width:120px}
+.contrast-label{font-size:10px;color:var(--ink3);letter-spacing:.08em;text-transform:uppercase;margin-bottom:3px}
+.contrast-bar{height:6px;border-radius:3px;background:var(--border);overflow:hidden}
+.contrast-fill{height:100%;border-radius:3px;background:linear-gradient(to right,var(--accent),var(--accent2));transition:width .4s var(--ease)}
+
+/* ACTIONS */
+.actions{display:flex;gap:10px;flex-wrap:wrap}
+.btn{
+  padding:9px 20px;border-radius:var(--r2);font-size:12px;
+  letter-spacing:.1em;text-transform:uppercase;cursor:pointer;
+  border:none;font-family:var(--sans);transition:all .2s var(--ease);
+}
+.btn-primary{background:var(--accent);color:var(--ink);font-weight:600}
+.btn-primary:hover{background:var(--accent2);color:#fff}
+.btn-ghost{background:transparent;border:1px solid var(--border2);color:var(--ink2)}
+.btn-ghost:hover{border-color:var(--accent);color:var(--accent)}
+
+/* TOAST */
+.toast{
+  position:fixed;bottom:28px;left:50%;transform:translateX(-50%) translateY(20px);
+  background:var(--ink);color:var(--bg);padding:10px 22px;border-radius:20px;
+  font-size:12px;letter-spacing:.08em;text-transform:uppercase;
+  opacity:0;transition:all .3s var(--ease);pointer-events:none;z-index:999;
+  white-space:nowrap;
+}
+.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
+
+/* EXPORT CANVAS hidden */
+#export-canvas{display:none}
+
+/* SECTION SPACING */
+.regen-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}
+.section-label{font-family:var(--serif);font-size:15px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--ink)}
+
+hr.divider{border:none;border-top:1px solid var(--border);margin:20px 0}
+</style>
+</head>
+<body>
+
+<header>
+  <div>
+    <div class="wordmark">Designer Wallcoverings</div>
+    <div class="tagline">Palette Mixer</div>
+  </div>
+  <div class="header-badge">Palette Mixer</div>
+</header>
+
+<main>
+  <div class="layout">
+
+    <!-- LEFT: PICKER -->
+    <div>
+      <div class="panel">
+        <div class="panel-title">Base Color</div>
+        <div style="position:relative;display:block">
+          <canvas class="color-field" id="colorField" width="280" height="180" style="display:block;width:100%"></canvas>
+          <div class="field-thumb" id="fieldThumb" style="position:absolute"></div>
+        </div>
+        <div class="hue-rail" id="hueRail">
+          <div class="hue-thumb" id="hueThumb"></div>
+        </div>
+        <div class="hex-row">
+          <div class="hex-swatch" id="hexSwatch"></div>
+          <input class="hex-input" id="hexInput" type="text" value="#c9a961" maxlength="7" spellcheck="false" aria-label="Hex color value">
+        </div>
+      </div>
+
+      <div class="panel" style="margin-top:16px">
+        <div class="panel-title">Mode</div>
+        <div class="mode-row" id="modeRow">
+          <button class="pill active" data-mode="monochrome">Monochrome</button>
+          <button class="pill" data-mode="analogous">Analogous</button>
+          <button class="pill" data-mode="complementary">Complementary</button>
+          <button class="pill" data-mode="triadic">Triadic</button>
+          <button class="pill" data-mode="curated">Curated</button>
+        </div>
+        <div class="panel-sub" style="margin-bottom:0">Adjust swatch lightness/saturation below</div>
+      </div>
+    </div>
+
+    <!-- RIGHT: PALETTE -->
+    <div>
+      <div class="panel">
+        <div class="regen-row">
+          <div class="section-label">Your Palette</div>
+          <button class="btn btn-primary" id="regenBtn">Regenerate</button>
+        </div>
+
+        <div class="palette-strip" id="paletteStrip">
+          <!-- swatches injected -->
+        </div>
+
+        <hr class="divider">
+
+        <!-- PREVIEW STRIP -->
+        <div class="panel-sub">Wallcovering Preview</div>
+        <div class="preview-strip" id="previewStrip">
+          <!-- stripes injected -->
+        </div>
+
+        <hr class="divider">
+
+        <!-- HARMONY -->
+        <div class="harmony-row" id="harmonyRow">
+          <div class="harmony-badge" id="harmonyScore">Harmony: <b>—</b></div>
+          <div class="contrast-bar-wrap">
+            <div class="contrast-label">Contrast spread</div>
+            <div class="contrast-bar"><div class="contrast-fill" id="contrastFill" style="width:0%"></div></div>
+          </div>
+        </div>
+
+        <!-- ACTIONS -->
+        <div class="actions">
+          <button class="btn btn-primary" id="exportBtn">Export Card</button>
+          <button class="btn btn-ghost" id="copyAllBtn">Copy All Hex</button>
+          <button class="btn btn-ghost" id="resetBtn">Reset</button>
+        </div>
+      </div>
+    </div>
+
+  </div>
+</main>
+
+<div class="toast" id="toast" role="status" aria-live="polite"></div>
+<canvas id="export-canvas" aria-hidden="true"></canvas>
+
+<script>
+// ── COLOR MATH ────────────────────────────────────────────────────────────────
+function hslToRgb(h,s,l){
+  s/=100;l/=100;
+  const k=n=>(n+h/30)%12;
+  const a=s*Math.min(l,1-l);
+  const f=n=>l-a*Math.max(-1,Math.min(k(n)-3,Math.min(9-k(n),1)));
+  return [Math.round(f(0)*255),Math.round(f(8)*255),Math.round(f(4)*255)];
+}
+function rgbToHsl(r,g,b){
+  r/=255;g/=255;b/=255;
+  const max=Math.max(r,g,b),min=Math.min(r,g,b),d=max-min;
+  let h=0,s=0,l=(max+min)/2;
+  if(d){
+    s=d/(1-Math.abs(2*l-1));
+    switch(max){case r:h=((g-b)/d+6)%6;break;case g:h=(b-r)/d+2;break;case b:h=(r-g)/d+4;break}
+    h*=60;
+  }
+  return [Math.round(h),Math.round(s*100),Math.round(l*100)];
+}
+function hexToRgb(hex){
+  hex=hex.replace('#','');
+  if(hex.length===3)hex=hex.split('').map(c=>c+c).join('');
+  const n=parseInt(hex,16);
+  return [n>>16&255,n>>8&255,n&255];
+}
+function rgbToHex(r,g,b){return'#'+[r,g,b].map(v=>v.toString(16).padStart(2,'0')).join('')}
+function hslToHex(h,s,l){return rgbToHex(...hslToRgb(h,s,l))}
+function hexToHsl(hex){return rgbToHsl(...hexToRgb(hex))}
+function clamp(v,a,b){return Math.max(a,Math.min(b,v))}
+function luminance(r,g,b){
+  const srgb=[r,g,b].map(v=>{v/=255;return v<=.03928?v/12.92:Math.pow((v+.055)/1.055,2.4)});
+  return .2126*srgb[0]+.7152*srgb[1]+.0722*srgb[2];
+}
+function contrastRatio(hex1,hex2){
+  const l1=luminance(...hexToRgb(hex1)),l2=luminance(...hexToRgb(hex2));
+  return (Math.max(l1,l2)+.05)/(Math.min(l1,l2)+.05);
+}
+
+// ── DESIGNER COLOR NAMES ─────────────────────────────────────────────────────
+const COLOR_NAMES=[
+  {name:'Ink',h:[200,260],s:[30,100],l:[0,20]},
+  {name:'Charcoal',h:[200,240],s:[10,40],l:[20,35]},
+  {name:'Slate',h:[210,230],s:[15,40],l:[35,50]},
+  {name:'Steel',h:[200,220],s:[10,30],l:[50,65]},
+  {name:'Mist',h:[200,220],s:[8,25],l:[65,82]},
+  {name:'Plaster',h:[20,40],s:[10,30],l:[75,90]},
+  {name:'Cream',h:[35,55],s:[30,70],l:[85,95]},
+  {name:'Ivory',h:[40,60],s:[20,60],l:[90,98]},
+  {name:'Oatmeal',h:[30,50],s:[15,40],l:[75,88]},
+  {name:'Linen',h:[38,55],s:[25,55],l:[80,92]},
+  {name:'Parchment',h:[40,60],s:[30,65],l:[82,93]},
+  {name:'Sand',h:[35,50],s:[30,60],l:[65,80]},
+  {name:'Dune',h:[30,45],s:[25,55],l:[55,72]},
+  {name:'Toffee',h:[25,38],s:[40,75],l:[40,58]},
+  {name:'Caramel',h:[28,42],s:[60,90],l:[45,62]},
+  {name:'Brass',h:[38,52],s:[55,85],l:[45,62]},
+  {name:'Gold',h:[42,55],s:[70,100],l:[48,65]},
+  {name:'Amber',h:[35,50],s:[80,100],l:[45,60]},
+  {name:'Terracotta',h:[10,25],s:[50,80],l:[45,62]},
+  {name:'Clay',h:[15,30],s:[35,65],l:[42,58]},
+  {name:'Blush',h:[0,15],s:[30,60],l:[72,88]},
+  {name:'Rose',h:[340,360],s:[30,65],l:[65,80]},
+  {name:'Mauve',h:[320,345],s:[20,50],l:[55,72]},
+  {name:'Aubergine',h:[280,310],s:[40,70],l:[22,40]},
+  {name:'Plum',h:[290,320],s:[35,65],l:[30,48]},
+  {name:'Lavender',h:[250,280],s:[25,55],l:[65,82]},
+  {name:'Iris',h:[245,265],s:[35,65],l:[50,68]},
+  {name:'Indigo',h:[235,255],s:[50,85],l:[28,45]},
+  {name:'Navy',h:[210,235],s:[55,90],l:[18,32]},
+  {name:'Cobalt',h:[220,240],s:[60,100],l:[35,55]},
+  {name:'Aegean',h:[195,215],s:[45,80],l:[38,58]},
+  {name:'Celestial',h:[200,218],s:[40,75],l:[55,72]},
+  {name:'Celadon',h:[140,165],s:[20,50],l:[62,80]},
+  {name:'Sage',h:[100,140],s:[20,45],l:[48,68]},
+  {name:'Fern',h:[110,135],s:[30,60],l:[38,55]},
+  {name:'Juniper',h:[155,175],s:[25,55],l:[28,45]},
+  {name:'Forest',h:[120,145],s:[35,65],l:[22,38]},
+  {name:'Emerald',h:[140,160],s:[55,90],l:[28,45]},
+  {name:'Moss',h:[80,110],s:[25,55],l:[30,50]},
+  {name:'Olive',h:[65,85],s:[30,65],l:[30,50]},
+  {name:'Khaki',h:[55,75],s:[20,50],l:[55,72]},
+  {name:'Chamomile',h:[50,70],s:[35,70],l:[68,85]},
+];
+function nameForHsl(h,s,l){
+  // normalize hue to 0-360
+  h=((h%360)+360)%360;
+  // find closest match by weighted distance
+  let best=null,bestD=1e9;
+  for(const c of COLOR_NAMES){
+    const hMin=c.h[0],hMax=c.h[1];
+    // handle wraparound
+    let hd;
+    const mid=(hMin+hMax)/2;
+    const diff=Math.min(Math.abs(h-mid),360-Math.abs(h-mid));
+    const hRange=(hMax-hMin)/2+15;
+    hd=Math.max(0,diff-hRange);
+    const sd=Math.max(0,Math.max(c.s[0]-s,s-c.s[1]));
+    const ld=Math.max(0,Math.max(c.l[0]-l,l-c.l[1]));
+    const d=hd*1.5+sd+ld*.8;
+    if(d<bestD){bestD=d;best=c}
+  }
+  return best?best.name:'Bespoke';
+}
+
+// ── PALETTE GENERATION ───────────────────────────────────────────────────────
+const MODES=['monochrome','analogous','complementary','triadic','curated'];
+function generatePalette(h,s,l,mode,locked){
+  const raw=[];
+  switch(mode){
+    case 'monochrome':{
+      const ls=[l-30,l-15,l,l+15,l+28].map(v=>clamp(v,8,95));
+      for(let i=0;i<5;i++)raw.push([h,Math.max(8,s-(i*3)),ls[i]]);
+      break;
+    }
+    case 'analogous':{
+      const offsets=[-36,-18,0,18,36];
+      for(const o of offsets)raw.push([(h+o+360)%360,s,l]);
+      break;
+    }
+    case 'complementary':{
+      const comp=(h+180)%360;
+      raw.push([h,s,clamp(l-15,8,92)]);
+      raw.push([h,s,l]);
+      raw.push([h,Math.max(8,s-20),clamp(l+20,8,95)]);
+      raw.push([comp,s,l]);
+      raw.push([comp,s,clamp(l+15,8,92)]);
+      break;
+    }
+    case 'triadic':{
+      const t1=(h+120)%360,t2=(h+240)%360;
+      raw.push([h,s,l]);
+      raw.push([h,Math.max(8,s-15),clamp(l+15,8,95)]);
+      raw.push([t1,s,l]);
+      raw.push([t1,Math.max(8,s-15),clamp(l+15,8,95)]);
+      raw.push([t2,s,l]);
+      break;
+    }
+    case 'curated':{
+      // split-complementary + accent
+      const c1=(h+150)%360,c2=(h+210)%360;
+      raw.push([h,s,clamp(l-15,8,92)]);
+      raw.push([h,Math.max(8,s-20),clamp(l+18,8,95)]);
+      raw.push([c1,Math.max(8,s-10),l]);
+      raw.push([c2,Math.max(8,s-10),l]);
+      raw.push([h,Math.max(5,s-30),clamp(l+30,8,96)]);
+      break;
+    }
+  }
+  // apply per-swatch adjustments from locked/offsets, preserve locked
+  return raw.map((hsl,i)=>{
+    if(locked && locked[i] && locked[i].locked) return locked[i];
+    const name=nameForHsl(hsl[0],hsl[1],hsl[2]);
+    return {h:hsl[0],s:hsl[1],l:hsl[2],name,locked:false,lOffset:0,sOffset:0};
+  });
+}
+
+// ── SVG TEXTURES ──────────────────────────────────────────────────────────────
+const TEXTURES=['damask','grasscloth','stripe','herringbone','linen'];
+function makeSvg(type,hex){
+  const [r,g,b]=hexToRgb(hex);
+  const L=luminance(r,g,b);
+  const linehex=L>.5?'rgba(0,0,0,0.15)':'rgba(255,255,255,0.15)';
+  const darkhex=L>.5?'rgba(0,0,0,0.08)':'rgba(255,255,255,0.08)';
+  switch(type){
+    case 'damask': return `<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80"><rect width="80" height="80" fill="${hex}"/>
+      <g fill="none" stroke="${linehex}" stroke-width="0.7">
+        <ellipse cx="40" cy="40" rx="14" ry="20"/>
+        <ellipse cx="40" cy="40" rx="8" ry="12"/>
+        <path d="M26,40 Q33,30 40,28 Q47,30 54,40 Q47,50 40,52 Q33,50 26,40z"/>
+        <circle cx="40" cy="40" r="3"/>
+        <path d="M40,20 Q45,24 40,28 Q35,24 40,20"/>
+        <path d="M40,52 Q45,56 40,60 Q35,56 40,52"/>
+        <path d="M20,40 Q24,35 26,40 Q24,45 20,40"/>
+        <path d="M54,40 Q56,35 60,40 Q56,45 54,40"/>
+        <ellipse cx="14" cy="14" rx="8" ry="12" transform="rotate(-45,14,14)"/>
+        <ellipse cx="66" cy="14" rx="8" ry="12" transform="rotate(45,66,14)"/>
+        <ellipse cx="14" cy="66" rx="8" ry="12" transform="rotate(45,14,66)"/>
+        <ellipse cx="66" cy="66" rx="8" ry="12" transform="rotate(-45,66,66)"/>
+      </g></svg>`;
+    case 'grasscloth': return `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="60"><rect width="40" height="60" fill="${hex}"/>
+      <g stroke="${linehex}" stroke-width="0.6">
+        ${Array.from({length:8},(_,i)=>`<line x1="${i*5}" y1="0" x2="${i*5+2}" y2="60"/>`).join('')}
+        ${Array.from({length:8},(_,i)=>`<line x1="${i*5+3}" y1="0" x2="${i*5+1}" y2="60" opacity=".6"/>`).join('')}
+      </g>
+      <g stroke="${darkhex}" stroke-width="0.4">
+        ${Array.from({length:6},(_,i)=>`<line x1="0" y1="${i*10+5}" x2="40" y2="${i*10+7}"/>`).join('')}
+      </g></svg>`;
+    case 'stripe': return `<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><rect width="60" height="60" fill="${hex}"/>
+      <g fill="${linehex}">
+        <rect x="0" y="0" width="12" height="60"/>
+        <rect x="24" y="0" width="5" height="60"/>
+        <rect x="43" y="0" width="12" height="60"/>
+      </g>
+      <g fill="${darkhex}">
+        <rect x="12" y="0" width="1.5" height="60"/>
+        <rect x="36" y="0" width="1.5" height="60"/>
+      </g></svg>`;
+    case 'herringbone': return `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect width="40" height="40" fill="${hex}"/>
+      <g fill="${linehex}">
+        <polygon points="0,0 8,0 8,20 0,20" transform="skewX(-20)"/>
+        <polygon points="10,0 18,0 18,20 10,20" transform="skewX(-20)"/>
+        <polygon points="20,0 28,0 28,20 20,20" transform="skewX(-20)"/>
+        <polygon points="30,0 38,0 38,20 30,20" transform="skewX(-20)"/>
+        <polygon points="0,20 8,20 8,40 0,40" transform="skewX(20) translate(-8,0)"/>
+        <polygon points="10,20 18,20 18,40 10,40" transform="skewX(20) translate(-8,0)"/>
+        <polygon points="20,20 28,20 28,40 20,40" transform="skewX(20) translate(-8,0)"/>
+        <polygon points="30,20 38,20 38,40 30,40" transform="skewX(20) translate(-8,0)"/>
+      </g></svg>`;
+    case 'linen': return `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"><rect width="20" height="20" fill="${hex}"/>
+      <g stroke="${linehex}" stroke-width="0.5">
+        ${Array.from({length:5},(_,i)=>`<line x1="0" y1="${i*4+2}" x2="20" y2="${i*4+2}"/>`).join('')}
+        ${Array.from({length:5},(_,i)=>`<line x1="${i*4+2}" y1="0" x2="${i*4+2}" y2="20"/>`).join('')}
+      </g></svg>`;
+    default: return `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect width="40" height="40" fill="${hex}"/></svg>`;
+  }
+}
+
+// ── STATE ─────────────────────────────────────────────────────────────────────
+let state={
+  h:38,s:55,l:58, // brass-ish default
+  mode:'monochrome',
+  palette:[], // [{h,s,l,name,locked,lOffset,sOffset}]
+};
+
+// ── PICKER LOGIC ──────────────────────────────────────────────────────────────
+const field=document.getElementById('colorField');
+const fieldThumb=document.getElementById('fieldThumb');
+const hueRail=document.getElementById('hueRail');
+const hueThumb=document.getElementById('hueThumb');
+const hexInput=document.getElementById('hexInput');
+const hexSwatch=document.getElementById('hexSwatch');
+
+function drawField(){
+  const ctx=field.getContext('2d');
+  const w=field.width,h=field.height;
+  // white→hue gradient (left→right)
+  const grad=ctx.createLinearGradient(0,0,w,0);
+  grad.addColorStop(0,'hsl(0,0%,100%)');
+  grad.addColorStop(1,`hsl(${state.h},100%,50%)`);
+  ctx.fillStyle=grad;ctx.fillRect(0,0,w,h);
+  // transparent→black gradient (top→bottom)
+  const grad2=ctx.createLinearGradient(0,0,0,h);
+  grad2.addColorStop(0,'rgba(0,0,0,0)');
+  grad2.addColorStop(1,'rgba(0,0,0,1)');
+  ctx.fillStyle=grad2;ctx.fillRect(0,0,w,h);
+}
+
+function posFromSL(s,l){
+  // x = saturation (0..100 → 0..width), y = inverse lightness (100..0 → 0..height)
+  const x=clamp((s/100)*(field.offsetWidth||280),0,(field.offsetWidth||280));
+  const y=clamp(((100-l)/100)*(field.offsetHeight||180),0,(field.offsetHeight||180));
+  return {x,y};
+}
+
+function slFromPos(x,y){
+  const w=field.offsetWidth||280,h=field.offsetHeight||180;
+  const px=clamp(x/w,0,1),py=clamp(y/h,0,1);
+  const s=Math.round(px*100);
+  const l=Math.round((1-py)*100);
+  return {s:clamp(s,0,100),l:clamp(l,2,98)};
+}
+
+function updateThumbPos(){
+  const w=field.offsetWidth||280,h=field.offsetHeight||180;
+  const pos=posFromSL(state.s,state.l);
+  // thumb is inside the relative wrapper (field.parentNode)
+  fieldThumb.style.left=clamp(pos.x,0,w)+'px';
+  fieldThumb.style.top=clamp(pos.y,0,h)+'px';
+  const hx=(state.h/360)*hueRail.offsetWidth;
+  hueThumb.style.left=hx+'px';
+  hueThumb.style.background=`hsl(${state.h},100%,50%)`;
+}
+
+function updateHexDisplay(){
+  const hex=hslToHex(state.h,state.s,state.l);
+  hexInput.value=hex;
+  hexSwatch.style.background=hex;
+}
+
+function fullUpdate(){
+  drawField();
+  updateThumbPos();
+  updateHexDisplay();
+  buildPalette();
+}
+
+// Field drag
+let draggingField=false;
+function handleFieldMove(e){
+  const rect=field.getBoundingClientRect();
+  const touch=e.touches?e.touches[0]:e;
+  const x=touch.clientX-rect.left,y=touch.clientY-rect.top;
+  const sl=slFromPos(x,y);
+  state.s=sl.s;state.l=sl.l;
+  updateThumbPos();updateHexDisplay();buildPalette();
+}
+field.addEventListener('mousedown',e=>{draggingField=true;handleFieldMove(e)});
+field.addEventListener('touchstart',e=>{draggingField=true;handleFieldMove(e)},{passive:true});
+document.addEventListener('mousemove',e=>{if(draggingField)handleFieldMove(e)});
+document.addEventListener('touchmove',e=>{if(draggingField)handleFieldMove(e)},{passive:true});
+document.addEventListener('mouseup',()=>{draggingField=false});
+document.addEventListener('touchend',()=>{draggingField=false});
+
+// Hue drag
+let draggingHue=false;
+function handleHueMove(e){
+  const rect=hueRail.getBoundingClientRect();
+  const touch=e.touches?e.touches[0]:e;
+  const x=clamp(touch.clientX-rect.left,0,rect.width);
+  state.h=Math.round((x/rect.width)*360);
+  drawField();updateThumbPos();updateHexDisplay();buildPalette();
+}
+hueRail.addEventListener('mousedown',e=>{draggingHue=true;handleHueMove(e)});
+hueRail.addEventListener('touchstart',e=>{draggingHue=true;handleHueMove(e)},{passive:true});
+document.addEventListener('mousemove',e=>{if(draggingHue)handleHueMove(e)});
+document.addEventListener('touchmove',e=>{if(draggingHue)handleHueMove(e)},{passive:true});
+document.addEventListener('mouseup',()=>{draggingHue=false});
+document.addEventListener('touchend',()=>{draggingHue=false});
+
+// Hex input
+hexInput.addEventListener('change',()=>{
+  const v=hexInput.value.trim();
+  if(/^#?[0-9a-fA-F]{6}$/.test(v)){
+    const hex=v.startsWith('#')?v:'#'+v;
+    const hsl=hexToHsl(hex);
+    state.h=hsl[0];state.s=hsl[1];state.l=hsl[2];
+    fullUpdate();
+  }
+});
+hexInput.addEventListener('keydown',e=>{if(e.key==='Enter')hexInput.blur()});
+
+// ── MODE PILLS ────────────────────────────────────────────────────────────────
+document.getElementById('modeRow').addEventListener('click',e=>{
+  const pill=e.target.closest('.pill');
+  if(!pill)return;
+  document.querySelectorAll('.pill').forEach(p=>p.classList.remove('active'));
+  pill.classList.add('active');
+  state.mode=pill.dataset.mode;
+  buildPalette();
+});
+
+// ── PALETTE BUILD ─────────────────────────────────────────────────────────────
+let regenAnim=null;
+function buildPalette(animate=false){
+  state.palette=generatePalette(state.h,state.s,state.l,state.mode,state.palette);
+  renderPalette(animate);
+  renderPreview();
+  updateHarmony();
+}
+
+function renderPalette(animate){
+  const strip=document.getElementById('paletteStrip');
+  const existing=strip.querySelectorAll('.swatch-card');
+  state.palette.forEach((sw,i)=>{
+    const hex=hslToHex(sw.h,clamp(sw.s+sw.sOffset,0,100),clamp(sw.l+sw.lOffset,5,95));
+    const texture=TEXTURES[i%TEXTURES.length];
+    if(existing[i]){
+      // update existing
+      const card=existing[i];
+      card.querySelector('.swatch-texture').innerHTML=makeSvg(texture,hex);
+      card.querySelector('.swatch-name').textContent=sw.name;
+      card.querySelector('.swatch-hex').textContent=hex.toUpperCase();
+      const lockBtn=card.querySelector('[data-action="lock"]');
+      lockBtn.textContent=sw.locked?'Locked':'Lock';
+      lockBtn.classList.toggle('locked',sw.locked);
+      const sl=card.querySelector('.swatch-slider');
+      sl.value=50+sw.lOffset;
+      if(animate){
+        card.style.opacity='0';card.style.transform='translateY(8px)';
+        requestAnimationFrame(()=>{
+          card.style.transition=`opacity .3s ease ${i*.06}s,transform .3s ease ${i*.06}s`;
+          card.style.opacity='1';card.style.transform='translateY(0)';
+        });
+      }
+    } else {
+      const card=document.createElement('div');
+      card.className='swatch-card';
+      card.setAttribute('data-idx',i);
+      card.innerHTML=`
+        <div class="swatch-texture">${makeSvg(texture,hex)}</div>
+        <div class="swatch-info">
+          <div class="swatch-name">${sw.name}</div>
+          <div class="swatch-hex">${hex.toUpperCase()}</div>
+          <div class="swatch-actions">
+            <button class="swatch-btn" data-action="copy">Copy</button>
+            <button class="swatch-btn ${sw.locked?'locked':''}" data-action="lock">${sw.locked?'Locked':'Lock'}</button>
+          </div>
+          <input class="swatch-slider" type="range" min="0" max="100" value="${50+sw.lOffset}"
+            aria-label="Lightness adjust for ${sw.name}" title="Shift lightness">
+        </div>`;
+      strip.appendChild(card);
+      // slider
+      card.querySelector('.swatch-slider').addEventListener('input',function(){
+        const idx=parseInt(card.dataset.idx);
+        state.palette[idx].lOffset=parseInt(this.value)-50;
+        updateSwatchColor(card,idx);
+      });
+      // actions
+      card.querySelector('.swatch-info').addEventListener('click',e=>{
+        const btn=e.target.closest('[data-action]');
+        if(!btn)return;
+        const idx=parseInt(card.dataset.idx);
+        if(btn.dataset.action==='copy'){
+          copyToClipboard(hslToHex(
+            state.palette[idx].h,
+            clamp(state.palette[idx].s+state.palette[idx].sOffset,0,100),
+            clamp(state.palette[idx].l+state.palette[idx].lOffset,5,95)
+          ));
+          btn.classList.add('copied');btn.textContent='Copied!';
+          setTimeout(()=>{btn.classList.remove('copied');btn.textContent='Copy'},1400);
+        } else if(btn.dataset.action==='lock'){
+          state.palette[idx].locked=!state.palette[idx].locked;
+          btn.classList.toggle('locked');
+          btn.textContent=state.palette[idx].locked?'Locked':'Lock';
+        }
+      });
+    }
+  });
+  // remove extras
+  Array.from(existing).slice(state.palette.length).forEach(c=>c.remove());
+}
+
+function updateSwatchColor(card,idx){
+  const sw=state.palette[idx];
+  const hex=hslToHex(sw.h,clamp(sw.s+sw.sOffset,0,100),clamp(sw.l+sw.lOffset,5,95));
+  const texture=TEXTURES[idx%TEXTURES.length];
+  card.querySelector('.swatch-texture').innerHTML=makeSvg(texture,hex);
+  card.querySelector('.swatch-hex').textContent=hex.toUpperCase();
+  renderPreview();
+  updateHarmony();
+}
+
+function renderPreview(){
+  const strip=document.getElementById('previewStrip');
+  strip.innerHTML='';
+  state.palette.forEach((sw,i)=>{
+    const hex=hslToHex(sw.h,clamp(sw.s+sw.sOffset,0,100),clamp(sw.l+sw.lOffset,5,95));
+    const d=document.createElement('div');
+    d.className='preview-stripe';
+    d.style.background=hex;
+    d.title=sw.name+' — '+hex;
+    strip.appendChild(d);
+  });
+}
+
+function updateHarmony(){
+  const hexes=state.palette.map(sw=>hslToHex(sw.h,clamp(sw.s+sw.sOffset,0,100),clamp(sw.l+sw.lOffset,5,95)));
+  // harmony: avg pairwise contrast
+  let totalContrast=0,pairs=0;
+  for(let i=0;i<hexes.length;i++)for(let j=i+1;j<hexes.length;j++){
+    totalContrast+=contrastRatio(hexes[i],hexes[j]);pairs++;
+  }
+  const avg=pairs?totalContrast/pairs:1;
+  // score: high contrast = more dynamic, moderate = more harmonious
+  // ideal harmony score = contrast 1.5-3.5
+  const score=Math.round(clamp((3.5-Math.abs(avg-2.5))/2.5*10,0,10));
+  const label=score>=8?'Exceptional':score>=6?'Harmonious':score>=4?'Balanced':'High Contrast';
+  document.getElementById('harmonyScore').innerHTML=`Harmony: <b>${score}/10 — ${label}</b>`;
+  const spread=clamp(avg/10*100,5,100);
+  document.getElementById('contrastFill').style.width=spread+'%';
+}
+
+// ── REGEN ─────────────────────────────────────────────────────────────────────
+document.getElementById('regenBtn').addEventListener('click',()=>{
+  // shuffle non-locked by nudging hue slightly
+  state.palette=state.palette.map(sw=>{
+    if(sw.locked)return sw;
+    return {...sw,locked:false};
+  });
+  // re-generate with small random hue variation
+  const nudge=(Math.random()-.5)*24;
+  const nh=((state.h+nudge)+360)%360;
+  const generated=generatePalette(nh,state.s,state.l,state.mode,state.palette);
+  state.palette=state.palette.map((sw,i)=>sw.locked?sw:generated[i]);
+  renderPalette(true);
+  renderPreview();
+  updateHarmony();
+});
+
+// ── RESET ─────────────────────────────────────────────────────────────────────
+document.getElementById('resetBtn').addEventListener('click',()=>{
+  state.palette=state.palette.map(sw=>({...sw,locked:false,lOffset:0,sOffset:0}));
+  buildPalette(true);
+});
+
+// ── COPY ALL ──────────────────────────────────────────────────────────────────
+document.getElementById('copyAllBtn').addEventListener('click',()=>{
+  const hexes=state.palette.map(sw=>{
+    const h=hslToHex(sw.h,clamp(sw.s+sw.sOffset,0,100),clamp(sw.l+sw.lOffset,5,95));
+    return `${sw.name}: ${h.toUpperCase()}`;
+  }).join('\n');
+  copyToClipboard(hexes);
+});
+
+// ── CLIPBOARD ─────────────────────────────────────────────────────────────────
+function copyToClipboard(text){
+  navigator.clipboard&&navigator.clipboard.writeText(text).then(()=>showToast('Copied to clipboard')).catch(()=>fallbackCopy(text));
+  if(!navigator.clipboard)fallbackCopy(text);
+}
+function fallbackCopy(text){
+  const ta=document.createElement('textarea');ta.value=text;
+  ta.style.cssText='position:fixed;opacity:0;top:0;left:0';
+  document.body.appendChild(ta);ta.select();
+  document.execCommand('copy');document.body.removeChild(ta);
+  showToast('Copied to clipboard');
+}
+
+// ── TOAST ─────────────────────────────────────────────────────────────────────
+let toastTimer=null;
+function showToast(msg){
+  const t=document.getElementById('toast');
+  t.textContent=msg;t.classList.add('show');
+  clearTimeout(toastTimer);
+  toastTimer=setTimeout(()=>t.classList.remove('show'),2200);
+}
+
+// ── EXPORT CARD ───────────────────────────────────────────────────────────────
+document.getElementById('exportBtn').addEventListener('click',()=>{
+  const W=900,H=420,PAD=40,SW=140,SH=220;
+  const c=document.getElementById('export-canvas');
+  c.width=W;c.height=H;c.style.display='none';
+  const ctx=c.getContext('2d');
+
+  // bg
+  ctx.fillStyle='#f4f1ea';ctx.fillRect(0,0,W,H);
+
+  // title
+  ctx.fillStyle='#1c1a17';
+  ctx.font='bold 14px Georgia,serif';ctx.letterSpacing='2px';
+  ctx.textAlign='left';
+  ctx.fillText('DESIGNER WALLCOVERINGS — PALETTE MIXER',PAD,PAD+14);
+
+  // mode
+  ctx.font='11px sans-serif';ctx.fillStyle='#7a7368';
+  ctx.fillText(state.mode.toUpperCase()+' PALETTE',PAD,PAD+30);
+
+  // swatches
+  state.palette.forEach((sw,i)=>{
+    const hex=hslToHex(sw.h,clamp(sw.s+sw.sOffset,0,100),clamp(sw.l+sw.lOffset,5,95));
+    const x=PAD+i*(SW+12),y=PAD+50;
+    ctx.fillStyle=hex;
+    ctx.beginPath();ctx.roundRect(x,y,SW,SH,6);ctx.fill();
+    // name
+    ctx.fillStyle='#1c1a17';ctx.font='bold 12px Georgia,serif';ctx.letterSpacing='1px';
+    ctx.textAlign='center';
+    ctx.fillText(sw.name.toUpperCase(),x+SW/2,y+SH+22);
+    ctx.font='10px monospace';ctx.letterSpacing='0';ctx.fillStyle='#7a7368';
+    ctx.fillText(hex.toUpperCase(),x+SW/2,y+SH+38);
+  });
+
+  // border
+  ctx.strokeStyle='#ddd8ce';ctx.lineWidth=1;
+  ctx.strokeRect(1,1,W-2,H-2);
+
+  // footer
+  ctx.fillStyle='#c9a961';ctx.font='italic 11px Georgia,serif';ctx.letterSpacing='1px';
+  ctx.textAlign='right';
+  ctx.fillText('designerwallcoverings.com',W-PAD,H-PAD+8);
+
+  // export
+  const url=c.toDataURL('image/png');
+  const a=document.createElement('a');
+  a.href=url;a.download='dw-palette.png';a.click();
+  showToast('Palette card exported');
+});
+
+// ── KEYBOARD ──────────────────────────────────────────────────────────────────
+document.addEventListener('keydown',e=>{
+  if(e.target.tagName==='INPUT')return;
+  if(e.key==='ArrowRight')state.h=(state.h+5)%360;
+  else if(e.key==='ArrowLeft')state.h=((state.h-5)+360)%360;
+  else if(e.key==='ArrowUp')state.l=clamp(state.l+3,2,98);
+  else if(e.key==='ArrowDown')state.l=clamp(state.l-3,2,98);
+  else return;
+  e.preventDefault();
+  fullUpdate();
+});
+
+// ── INIT ──────────────────────────────────────────────────────────────────────
+// Set initial state from default brass color
+(function init(){
+  // position thumb correctly after layout
+  requestAnimationFrame(()=>{
+    fullUpdate();
+    // position hue thumb
+    const hx=(state.h/360)*(hueRail.offsetWidth);
+    hueThumb.style.left=hx+'px';
+    hueThumb.style.background=`hsl(${state.h},100%,50%)`;
+  });
+})();
+</script>
+</body>
+</html>
diff --git a/public/games/dw-this-or-that.html b/public/games/dw-this-or-that.html
new file mode 100644
index 0000000..ded0acf
--- /dev/null
+++ b/public/games/dw-this-or-that.html
@@ -0,0 +1,922 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>This or That — Designer Wallcoverings</title>
+<style>
+:root{
+  --bg:#0d0b09;--bg2:#1a1714;--bg3:#231f1b;
+  --ink:#f4f1ea;--ink2:#c4bfb6;--ink3:#8a857d;
+  --accent:#c9a961;--accent2:#a8863d;--accent3:#e8c97a;
+  --serif:'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif;
+  --sans:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
+  --ease:cubic-bezier(.4,0,.2,1);
+  --ease-spring:cubic-bezier(.34,1.56,.64,1);
+  --r:8px;
+}
+*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
+html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--ink);font-family:var(--sans)}
+
+/* HEADER */
+header{
+  position:fixed;top:0;left:0;right:0;z-index:100;
+  display:flex;align-items:center;justify-content:space-between;
+  padding:14px 24px;background:rgba(13,11,9,.92);
+  backdrop-filter:blur(12px);border-bottom:1px solid rgba(201,169,97,.18);
+}
+.wordmark{
+  font-family:var(--serif);font-size:clamp(18px,2.2vw,26px);
+  font-weight:400;letter-spacing:.18em;text-transform:uppercase;
+  color:var(--ink);line-height:1;
+}
+.wordmark span{color:var(--accent)}
+#mute-btn{
+  background:none;border:1px solid rgba(201,169,97,.35);
+  color:var(--accent);padding:6px 10px;border-radius:4px;
+  cursor:pointer;font-size:16px;line-height:1;transition:all .2s;
+}
+#mute-btn:hover{background:rgba(201,169,97,.12)}
+
+/* PROGRESS BAR */
+#progress-wrap{
+  position:fixed;top:57px;left:0;right:0;z-index:99;
+  height:3px;background:rgba(255,255,255,.06);
+}
+#progress-bar{height:100%;background:var(--accent);width:0%;transition:width .4s var(--ease)}
+
+/* ROUND COUNTER */
+#round-counter{
+  position:fixed;top:68px;left:50%;transform:translateX(-50%);z-index:98;
+  font-family:var(--serif);font-size:13px;letter-spacing:.12em;text-transform:uppercase;
+  color:var(--ink3);padding:4px 14px;background:rgba(13,11,9,.7);border-radius:20px;
+}
+
+/* ARENA */
+#arena{
+  position:fixed;top:57px;left:0;right:0;bottom:0;
+  display:flex;flex-direction:row;overflow:hidden;
+}
+
+/* PANELS */
+.panel{
+  flex:1;position:relative;cursor:pointer;overflow:hidden;
+  display:flex;align-items:center;justify-content:center;
+  transition:transform .15s var(--ease);
+}
+.panel:hover{transform:scale(1.015)}
+.panel:hover .panel-label{transform:translateY(-4px);opacity:1}
+.panel.left{border-right:2px solid rgba(201,169,97,.3)}
+.panel.right{border-left:2px solid rgba(201,169,97,.3)}
+
+.panel-svg{width:100%;height:100%;position:absolute;inset:0;transition:opacity .5s var(--ease)}
+.panel-overlay{
+  position:absolute;inset:0;
+  background:linear-gradient(to top,rgba(13,11,9,.85) 0%,rgba(13,11,9,.1) 55%,rgba(13,11,9,.0) 100%);
+}
+.panel-label{
+  position:absolute;bottom:0;left:0;right:0;
+  padding:28px 24px 36px;transition:transform .25s var(--ease),opacity .25s;
+}
+.panel-name{
+  font-family:var(--serif);font-size:clamp(22px,3vw,36px);
+  font-weight:400;color:var(--ink);letter-spacing:.06em;line-height:1.1;
+  margin-bottom:6px;
+}
+.panel-colorway{
+  font-size:12px;letter-spacing:.14em;text-transform:uppercase;
+  color:var(--accent);
+}
+.panel-hint{
+  margin-top:12px;display:inline-flex;align-items:center;gap:6px;
+  font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink2);
+  border:1px solid rgba(201,169,97,.3);padding:5px 12px;border-radius:20px;
+  background:rgba(13,11,9,.5);
+}
+
+/* OR divider */
+#or-divider{
+  position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
+  z-index:101;width:52px;height:52px;border-radius:50%;
+  background:var(--bg);border:2px solid var(--accent);
+  display:flex;align-items:center;justify-content:center;
+  font-family:var(--serif);font-size:18px;color:var(--accent);
+  letter-spacing:.06em;pointer-events:none;
+  box-shadow:0 0 0 8px rgba(13,11,9,.9);
+}
+
+/* CHOSEN animation sweep */
+.panel.chosen{animation:sweep-in .45s var(--ease-spring) forwards}
+.panel.loser{animation:fade-out .4s var(--ease) forwards}
+.panel.chosen .panel-overlay{background:linear-gradient(to top,rgba(13,11,9,.7) 0%,rgba(201,169,97,.08) 100%)}
+@keyframes sweep-in{
+  0%{transform:scale(1)}
+  50%{transform:scale(1.05)}
+  100%{transform:scale(1.02)}
+}
+@keyframes fade-out{
+  0%{opacity:1}
+  100%{opacity:.18}
+}
+
+/* SLIDE transitions */
+#arena.slide-out{animation:slide-out .28s var(--ease) forwards}
+#arena.slide-in{animation:slide-in .32s var(--ease) forwards}
+@keyframes slide-out{
+  0%{opacity:1;transform:translateY(0)}
+  100%{opacity:0;transform:translateY(-40px)}
+}
+@keyframes slide-in{
+  0%{opacity:0;transform:translateY(40px)}
+  100%{opacity:1;transform:translateY(0)}
+}
+
+/* RESULT SCREEN */
+#result-screen{
+  position:fixed;inset:0;z-index:200;
+  background:var(--bg);display:flex;flex-direction:column;
+  align-items:center;overflow-y:auto;
+  visibility:hidden;opacity:0;transition:opacity .5s var(--ease),visibility .5s;
+}
+#result-screen.visible{visibility:visible;opacity:1}
+.result-inner{
+  max-width:680px;width:100%;padding:80px 32px 60px;
+  text-align:center;
+}
+.result-eyebrow{
+  font-size:12px;letter-spacing:.18em;text-transform:uppercase;
+  color:var(--accent);margin-bottom:16px;
+}
+.result-title{
+  font-family:var(--serif);font-size:clamp(32px,5vw,56px);
+  font-weight:400;color:var(--ink);line-height:1.1;margin-bottom:8px;
+}
+.result-subtitle{
+  font-size:14px;color:var(--ink3);letter-spacing:.06em;margin-bottom:40px;
+}
+.result-palette{
+  display:flex;gap:10px;justify-content:center;margin-bottom:40px;
+}
+.swatch{width:52px;height:52px;border-radius:50%;border:2px solid rgba(255,255,255,.12)}
+.result-styles{
+  display:flex;flex-wrap:wrap;gap:10px;justify-content:center;margin-bottom:44px;
+}
+.style-chip{
+  padding:7px 18px;border:1px solid rgba(201,169,97,.4);border-radius:20px;
+  font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--accent);
+  background:rgba(201,169,97,.06);
+}
+.result-recs-title{
+  font-family:var(--serif);font-size:22px;letter-spacing:.08em;
+  color:var(--ink);margin-bottom:24px;
+}
+.rec-grid{display:flex;gap:20px;justify-content:center;flex-wrap:wrap;margin-bottom:48px}
+.rec-card{
+  width:160px;border:1px solid rgba(201,169,97,.2);border-radius:var(--r);
+  overflow:hidden;background:var(--bg2);
+}
+.rec-card svg{width:100%;height:110px;display:block}
+.rec-card-name{
+  font-family:var(--serif);font-size:14px;color:var(--ink2);
+  padding:10px 12px;letter-spacing:.04em;
+}
+.result-btns{display:flex;gap:14px;justify-content:center;flex-wrap:wrap}
+.btn{
+  padding:13px 30px;border-radius:4px;font-size:13px;letter-spacing:.12em;
+  text-transform:uppercase;cursor:pointer;transition:all .2s var(--ease);
+  font-family:var(--sans);
+}
+.btn-primary{background:var(--accent);color:#0d0b09;border:none;font-weight:600}
+.btn-primary:hover{background:var(--accent3);transform:translateY(-1px)}
+.btn-secondary{background:none;color:var(--ink2);border:1px solid rgba(201,169,97,.35)}
+.btn-secondary:hover{border-color:var(--accent);color:var(--ink)}
+
+/* mobile */
+@media(max-width:640px){
+  #arena{flex-direction:column}
+  .panel.left{border-right:none;border-bottom:2px solid rgba(201,169,97,.3)}
+  .panel.right{border-left:none;border-top:2px solid rgba(201,169,97,.3)}
+  #or-divider{top:calc(57px + 50% - 26px)}
+  .panel:hover{transform:none}
+}
+</style>
+</head>
+<body>
+
+<header>
+  <div class="wordmark">Designer <span>Wallcoverings</span></div>
+  <button id="mute-btn" title="Toggle sound">♪</button>
+</header>
+
+<div id="progress-wrap"><div id="progress-bar"></div></div>
+<div id="round-counter">Round 1 of 15</div>
+<div id="or-divider">or</div>
+
+<div id="arena">
+  <div class="panel left" id="panel-a" tabindex="0" role="button" aria-label="Choose left pattern">
+    <svg class="panel-svg" id="svg-a" viewBox="0 0 400 600" preserveAspectRatio="xMidYMid slice"></svg>
+    <div class="panel-overlay"></div>
+    <div class="panel-label">
+      <div class="panel-name" id="name-a"></div>
+      <div class="panel-colorway" id="colorway-a"></div>
+      <div class="panel-hint">&#8592; Left Arrow · Click to Choose</div>
+    </div>
+  </div>
+  <div class="panel right" id="panel-b" tabindex="0" role="button" aria-label="Choose right pattern">
+    <svg class="panel-svg" id="svg-b" viewBox="0 0 400 600" preserveAspectRatio="xMidYMid slice"></svg>
+    <div class="panel-overlay"></div>
+    <div class="panel-label">
+      <div class="panel-name" id="name-b"></div>
+      <div class="panel-colorway" id="colorway-b"></div>
+      <div class="panel-hint">Click to Choose · Right Arrow &#8594;</div>
+    </div>
+  </div>
+</div>
+
+<div id="result-screen" role="dialog" aria-modal="true" aria-label="Your taste profile results">
+  <div class="result-inner" id="result-inner"></div>
+</div>
+
+<script>
+/* ── PATTERNS LIBRARY ─────────────────────────────────────── */
+const PATTERNS = [
+  {
+    id:'damask-ivory',name:'Sorrento Damask',colorway:'Ivory & Gold',
+    styles:['Traditional','Formal','Warm'],
+    colors:['#f4f0e4','#c9a961','#8a6d3b','#e8dfc8'],
+    draw(svg){
+      svg.innerHTML=`
+      <defs>
+        <pattern id="pd1" x="0" y="0" width="100" height="120" patternUnits="userSpaceOnUse">
+          <rect width="100" height="120" fill="#f4f0e4"/>
+          <ellipse cx="50" cy="60" rx="18" ry="28" fill="none" stroke="#c9a961" stroke-width="1.5"/>
+          <ellipse cx="50" cy="60" rx="10" ry="16" fill="rgba(201,169,97,.18)" stroke="#c9a961" stroke-width=".8"/>
+          <path d="M50 32 C38 40 32 50 32 60 C32 70 38 80 50 88 C62 80 68 70 68 60 C68 50 62 40 50 32Z" fill="none" stroke="#c9a961" stroke-width=".6" opacity=".5"/>
+          <circle cx="50" cy="60" r="4" fill="#c9a961" opacity=".6"/>
+          <path d="M50 28 L54 36 M50 92 L46 84 M26 60 L34 56 M74 60 L66 64" stroke="#c9a961" stroke-width=".8" opacity=".4"/>
+          <path d="M34 38 Q42 30 50 32 Q58 30 66 38" fill="none" stroke="#c9a961" stroke-width=".7" opacity=".5"/>
+          <path d="M34 82 Q42 90 50 88 Q58 90 66 82" fill="none" stroke="#c9a961" stroke-width=".7" opacity=".5"/>
+        </pattern>
+      </defs>
+      <rect width="400" height="600" fill="url(#pd1)"/>`;
+    }
+  },
+  {
+    id:'toile-blue',name:'Versailles Toile',colorway:'Cobalt & Cream',
+    styles:['Traditional','Romantic','Cool'],
+    colors:['#e8edf4','#1a3a6b','#4a6fa5','#c4cee0'],
+    draw(svg){
+      svg.innerHTML=`
+      <defs>
+        <pattern id="pt1" x="0" y="0" width="120" height="120" patternUnits="userSpaceOnUse">
+          <rect width="120" height="120" fill="#e8edf4"/>
+          <g opacity=".75" stroke="#1a3a6b" fill="none" stroke-width="1">
+            <ellipse cx="60" cy="40" rx="14" ry="20"/>
+            <path d="M46 58 Q60 70 74 58"/>
+            <path d="M52 72 Q60 85 68 72 Q74 82 80 78 Q74 90 60 95 Q46 90 40 78 Q46 82 52 72Z" fill="rgba(26,58,107,.12)"/>
+            <path d="M30 20 Q38 12 46 18 Q54 10 60 16" stroke-width=".8" opacity=".5"/>
+            <line x1="20" y1="110" x2="100" y2="110" stroke-width=".5" opacity=".3"/>
+            <circle cx="30" cy="70" r="6" opacity=".3"/>
+            <circle cx="90" cy="50" r="5" opacity=".3"/>
+            <path d="M20 30 Q25 25 30 30 Q35 25 40 30" stroke-width=".6" opacity=".4"/>
+            <path d="M80 90 Q85 82 92 86 Q88 95 80 90Z" fill="rgba(26,58,107,.15)"/>
+          </g>
+        </pattern>
+      </defs>
+      <rect width="400" height="600" fill="url(#pt1)"/>`;
+    }
+  },
+  {
+    id:'ikat-terracotta',name:'Oaxaca Ikat',colorway:'Terracotta & Saffron',
+    styles:['Bohemian','Bold','Warm'],
+    colors:['#c4623a','#e8a84c','#8b2e12','#f4d08a'],
+    draw(svg){
+      svg.innerHTML=`
+      <defs>
+        <pattern id="pik1" x="0" y="0" width="80" height="80" patternUnits="userSpaceOnUse">
+          <rect width="80" height="80" fill="#8b2e12"/>
+          <rect x="10" y="10" width="60" height="60" fill="#c4623a"/>
+          <rect x="20" y="20" width="40" height="40" fill="#e8a84c" opacity=".7"/>
+          <path d="M40 10 L50 20 L40 30 L30 20Z" fill="#f4d08a" opacity=".6"/>
+          <path d="M40 50 L50 60 L40 70 L30 60Z" fill="#f4d08a" opacity=".6"/>
+          <path d="M10 40 L20 30 L30 40 L20 50Z" fill="#f4d08a" opacity=".6"/>
+          <path d="M50 40 L60 30 L70 40 L60 50Z" fill="#f4d08a" opacity=".6"/>
+          <rect x="34" y="34" width="12" height="12" fill="#8b2e12" transform="rotate(45 40 40)"/>
+          <line x1="0" y1="0" x2="10" y2="10" stroke="#c4623a" stroke-width="2"/>
+          <line x1="70" y1="0" x2="80" y2="10" stroke="#c4623a" stroke-width="2"/>
+        </pattern>
+      </defs>
+      <rect width="400" height="600" fill="url(#pik1)"/>`;
+    }
+  },
+  {
+    id:'chinoiserie-sage',name:'Canton Garden',colorway:'Sage & Blush',
+    styles:['Chinoiserie','Romantic','Serene'],
+    colors:['#d4e4d8','#5a8a6a','#e8c4c0','#3a6a4a'],
+    draw(svg){
+      svg.innerHTML=`
+      <defs>
+        <pattern id="pch1" x="0" y="0" width="160" height="200" patternUnits="userSpaceOnUse">
+          <rect width="160" height="200" fill="#d4e4d8"/>
+          <g stroke="#3a6a4a" fill="none" stroke-width="1">
+            <path d="M80 20 L80 140"/>
+            <path d="M60 140 Q80 160 100 140"/>
+            <ellipse cx="80" cy="80" rx="18" ry="22"/>
+            <path d="M62 80 Q70 68 80 70 Q90 68 98 80"/>
+            <path d="M68 55 Q74 48 80 50 Q86 48 92 55"/>
+            <path d="M50 30 Q60 20 80 22 Q100 20 110 30" stroke-width=".8" opacity=".6"/>
+          </g>
+          <g opacity=".45" stroke="#5a8a6a" fill="rgba(90,138,106,.12)" stroke-width=".8">
+            <ellipse cx="80" cy="80" rx="8" ry="10"/>
+          </g>
+          <circle cx="80" cy="100" r="3" fill="#e8c4c0" opacity=".8"/>
+          <circle cx="66" cy="88" r="2.5" fill="#e8c4c0" opacity=".7"/>
+          <circle cx="94" cy="88" r="2.5" fill="#e8c4c0" opacity=".7"/>
+          <g transform="translate(20,160)" stroke="#3a6a4a" fill="none" stroke-width=".7" opacity=".4">
+            <path d="M0 10 Q10 0 20 8 Q30 0 40 10"/>
+          </g>
+          <g transform="translate(100,40)" stroke="#3a6a4a" fill="none" stroke-width=".7" opacity=".4">
+            <path d="M0 10 Q10 0 20 8 Q30 0 40 10"/>
+          </g>
+        </pattern>
+      </defs>
+      <rect width="400" height="600" fill="url(#pch1)"/>`;
+    }
+  },
+  {
+    id:'geo-navy',name:'Meridian Lattice',colorway:'Navy & Brass',
+    styles:['Geometric','Bold','Classic'],
+    colors:['#1a2a4a','#c9a961','#2a3d6a','#e8d4a0'],
+    draw(svg){
+      svg.innerHTML=`
+      <defs>
+        <pattern id="pgeo1" x="0" y="0" width="60" height="60" patternUnits="userSpaceOnUse">
+          <rect width="60" height="60" fill="#1a2a4a"/>
+          <rect x="5" y="5" width="50" height="50" fill="none" stroke="#c9a961" stroke-width="1.2"/>
+          <rect x="15" y="15" width="30" height="30" fill="none" stroke="#c9a961" stroke-width=".8" opacity=".6"/>
+          <rect x="25" y="25" width="10" height="10" fill="#c9a961" opacity=".5"/>
+          <line x1="5" y1="5" x2="15" y2="15" stroke="#c9a961" stroke-width=".6" opacity=".5"/>
+          <line x1="55" y1="5" x2="45" y2="15" stroke="#c9a961" stroke-width=".6" opacity=".5"/>
+          <line x1="5" y1="55" x2="15" y2="45" stroke="#c9a961" stroke-width=".6" opacity=".5"/>
+          <line x1="55" y1="55" x2="45" y2="45" stroke="#c9a961" stroke-width=".6" opacity=".5"/>
+        </pattern>
+      </defs>
+      <rect width="400" height="600" fill="url(#pgeo1)"/>`;
+    }
+  },
+  {
+    id:'grasscloth-linen',name:'Kyoto Weave',colorway:'Natural Linen',
+    styles:['Textural','Serene','Natural'],
+    colors:['#d8c9a8','#a89068','#c4b48c','#e8dcc0'],
+    draw(svg){
+      svg.innerHTML=`
+      <defs>
+        <filter id="fgrain"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="4" result="noise"/><feColorMatrix type="saturate" values="0" in="noise" result="grey"/><feBlend in="SourceGraphic" in2="grey" mode="multiply" result="blend"/><feComposite in="blend" in2="SourceGraphic" operator="in"/></filter>
+        <pattern id="pgc1" x="0" y="0" width="12" height="6" patternUnits="userSpaceOnUse">
+          <rect width="12" height="6" fill="#d8c9a8"/>
+          <line x1="0" y1="3" x2="12" y2="3" stroke="#a89068" stroke-width=".8" opacity=".4"/>
+          <line x1="6" y1="0" x2="6" y2="6" stroke="#c4b48c" stroke-width=".5" opacity=".3"/>
+        </pattern>
+        <pattern id="pgc2" x="0" y="0" width="24" height="12" patternUnits="userSpaceOnUse">
+          <rect width="24" height="12" fill="url(#pgc1)"/>
+          <rect x="0" y="0" width="24" height="12" fill="none" stroke="#a89068" stroke-width=".3" opacity=".2"/>
+        </pattern>
+      </defs>
+      <rect width="400" height="600" fill="url(#pgc2)"/>
+      <rect width="400" height="600" fill="rgba(168,144,104,.04)" filter="url(#fgrain)"/>`;
+    }
+  },
+  {
+    id:'botanical-forest',name:'Ardmore Botanical',colorway:'Forest & Gold',
+    styles:['Botanical','Dramatic','Natural'],
+    colors:['#1a2e1a','#4a7a4a','#c9a961','#8ab88a'],
+    draw(svg){
+      svg.innerHTML=`
+      <defs>
+        <pattern id="pbot1" x="0" y="0" width="200" height="240" patternUnits="userSpaceOnUse">
+          <rect width="200" height="240" fill="#1a2e1a"/>
+          <g stroke="#4a7a4a" fill="none" stroke-width="1.2">
+            <path d="M100 240 L100 60"/>
+            <path d="M100 120 Q70 95 50 105 Q65 88 100 100"/>
+            <path d="M100 120 Q130 95 150 105 Q135 88 100 100"/>
+            <path d="M100 160 Q65 138 40 148 Q58 128 100 140"/>
+            <path d="M100 160 Q135 138 160 148 Q142 128 100 140"/>
+            <path d="M100 90 Q82 72 66 78 Q78 62 100 72"/>
+            <path d="M100 90 Q118 72 134 78 Q122 62 100 72"/>
+          </g>
+          <g fill="#c9a961" opacity=".7">
+            <circle cx="100" cy="120" r="4"/>
+            <circle cx="52" cy="107" r="3"/>
+            <circle cx="148" cy="107" r="3"/>
+          </g>
+          <g fill="rgba(74,122,74,.18)">
+            <ellipse cx="55" cy="100" rx="20" ry="10" transform="rotate(-30 55 100)"/>
+            <ellipse cx="145" cy="100" rx="20" ry="10" transform="rotate(30 145 100)"/>
+            <ellipse cx="42" cy="142" rx="24" ry="11" transform="rotate(-25 42 142)"/>
+            <ellipse cx="158" cy="142" rx="24" ry="11" transform="rotate(25 158 142)"/>
+          </g>
+        </pattern>
+      </defs>
+      <rect width="400" height="600" fill="url(#pbot1)"/>`;
+    }
+  },
+  {
+    id:'artdeco-blush',name:'Gatsby Rose',colorway:'Blush & Champagne',
+    styles:['Art Deco','Glamorous','Warm'],
+    colors:['#e8d4d4','#c9a0a0','#d4aa80','#8a6060'],
+    draw(svg){
+      svg.innerHTML=`
+      <defs>
+        <pattern id="pad1" x="0" y="0" width="100" height="100" patternUnits="userSpaceOnUse">
+          <rect width="100" height="100" fill="#e8d4d4"/>
+          <g stroke="#8a6060" fill="none">
+            <polygon points="50,10 62,38 92,38 68,56 78,86 50,68 22,86 32,56 8,38 38,38" stroke-width="1" fill="rgba(201,160,160,.15)"/>
+            <polygon points="50,20 59,44 84,44 64,59 72,82 50,66 28,82 36,59 16,44 41,44" stroke-width=".6" opacity=".5"/>
+          </g>
+          <circle cx="50" cy="50" r="6" fill="#d4aa80" opacity=".8"/>
+          <g stroke="#8a6060" stroke-width=".5" opacity=".3">
+            <line x1="0" y1="0" x2="100" y2="100"/>
+            <line x1="100" y1="0" x2="0" y2="100"/>
+          </g>
+        </pattern>
+      </defs>
+      <rect width="400" height="600" fill="url(#pad1)"/>`;
+    }
+  },
+  {
+    id:'stripe-emerald',name:'Mayfair Stripe',colorway:'Emerald & Cream',
+    styles:['Classic','Bold','Graphic'],
+    colors:['#1a4a2a','#f4f1ea','#2a6a3a','#c9d8c4'],
+    draw(svg){
+      svg.innerHTML=`
+      <defs>
+        <pattern id="pst1" x="0" y="0" width="80" height="10" patternUnits="userSpaceOnUse">
+          <rect width="80" height="10" fill="#f4f1ea"/>
+          <rect x="0" y="0" width="20" height="10" fill="#1a4a2a"/>
+          <rect x="30" y="0" width="4" height="10" fill="#2a6a3a" opacity=".5"/>
+          <rect x="40" y="0" width="20" height="10" fill="#1a4a2a"/>
+          <rect x="70" y="0" width="4" height="10" fill="#2a6a3a" opacity=".5"/>
+        </pattern>
+      </defs>
+      <rect width="400" height="600" fill="url(#pst1)"/>`;
+    }
+  },
+  {
+    id:'maximalist-jewel',name:'Bosphorus Iznik',colorway:'Jewel & Crimson',
+    styles:['Maximalist','Bold','Exotic'],
+    colors:['#6a1a2a','#c9a961','#1a3a6a','#d4602a'],
+    draw(svg){
+      svg.innerHTML=`
+      <defs>
+        <pattern id="pmx1" x="0" y="0" width="120" height="120" patternUnits="userSpaceOnUse">
+          <rect width="120" height="120" fill="#6a1a2a"/>
+          <g fill="none" stroke="#c9a961" stroke-width="1.2">
+            <circle cx="60" cy="60" r="40"/>
+            <circle cx="60" cy="60" r="28"/>
+            <circle cx="60" cy="60" r="12"/>
+          </g>
+          <g fill="none" stroke="#c9a961" stroke-width=".8" opacity=".6">
+            <path d="M60 20 L60 100 M20 60 L100 60"/>
+            <path d="M31.7 31.7 L88.3 88.3 M88.3 31.7 L31.7 88.3"/>
+          </g>
+          <g fill="#c9a961" opacity=".7">
+            <circle cx="60" cy="20" r="3"/><circle cx="60" cy="100" r="3"/>
+            <circle cx="20" cy="60" r="3"/><circle cx="100" cy="60" r="3"/>
+            <circle cx="60" cy="60" r="5"/>
+          </g>
+          <g fill="#1a3a6a" opacity=".5">
+            <path d="M45 45 L60 35 L75 45 L75 60 L60 52 L45 60Z"/>
+          </g>
+          <g fill="#d4602a" opacity=".4">
+            <path d="M45 60 L60 52 L75 60 L75 75 L60 85 L45 75Z"/>
+          </g>
+        </pattern>
+      </defs>
+      <rect width="400" height="600" fill="url(#pmx1)"/>`;
+    }
+  },
+  {
+    id:'chevron-charcoal',name:'Westminster Chevron',colorway:'Charcoal & Silver',
+    styles:['Geometric','Modern','Cool'],
+    colors:['#2a2a2a','#8a8a8a','#444444','#c4c4c4'],
+    draw(svg){
+      svg.innerHTML=`
+      <defs>
+        <pattern id="pchev" x="0" y="0" width="60" height="40" patternUnits="userSpaceOnUse">
+          <rect width="60" height="40" fill="#2a2a2a"/>
+          <polyline points="0,20 15,5 30,20 45,5 60,20" fill="none" stroke="#8a8a8a" stroke-width="3"/>
+          <polyline points="0,40 15,25 30,40 45,25 60,40" fill="none" stroke="#8a8a8a" stroke-width="3"/>
+          <polyline points="0,20 15,5 30,20 45,5 60,20" fill="none" stroke="#c4c4c4" stroke-width=".8" opacity=".3"/>
+        </pattern>
+      </defs>
+      <rect width="400" height="600" fill="url(#pchev)"/>`;
+    }
+  },
+  {
+    id:'floral-blush-lg',name:'Kensington Bloom',colorway:'Blush & Sage',
+    styles:['Floral','Romantic','Serene'],
+    colors:['#f4e8e8','#e8a0a8','#88aa88','#d4c0b0'],
+    draw(svg){
+      svg.innerHTML=`
+      <defs>
+        <pattern id="pfl1" x="0" y="0" width="140" height="140" patternUnits="userSpaceOnUse">
+          <rect width="140" height="140" fill="#f4e8e8"/>
+          <g fill="none">
+            <g transform="translate(70,70)">
+              <ellipse cx="0" cy="-24" rx="10" ry="16" fill="#e8a0a8" opacity=".75" transform="rotate(0)"/>
+              <ellipse cx="0" cy="-24" rx="10" ry="16" fill="#e8a0a8" opacity=".75" transform="rotate(60)"/>
+              <ellipse cx="0" cy="-24" rx="10" ry="16" fill="#e8b0b8" opacity=".75" transform="rotate(120)"/>
+              <ellipse cx="0" cy="-24" rx="10" ry="16" fill="#e8a0a8" opacity=".75" transform="rotate(180)"/>
+              <ellipse cx="0" cy="-24" rx="10" ry="16" fill="#e8b0b8" opacity=".75" transform="rotate(240)"/>
+              <ellipse cx="0" cy="-24" rx="10" ry="16" fill="#e8a0a8" opacity=".75" transform="rotate(300)"/>
+              <circle cx="0" cy="0" r="8" fill="#f4d0a0" opacity=".9"/>
+              <circle cx="0" cy="0" r="4" fill="#c9a961" opacity=".7"/>
+            </g>
+            <g stroke="#88aa88" stroke-width="1" opacity=".5">
+              <path d="M20 120 Q50 90 70 70"/>
+              <path d="M120 20 Q95 50 70 70"/>
+              <path d="M70 70 Q80 100 70 120" fill="rgba(136,170,136,.1)"/>
+            </g>
+            <g transform="translate(20,20)" opacity=".4">
+              <ellipse cx="0" cy="-14" rx="6" ry="10" fill="#e8a0a8" transform="rotate(0)"/>
+              <ellipse cx="0" cy="-14" rx="6" ry="10" fill="#e8a0a8" transform="rotate(72)"/>
+              <ellipse cx="0" cy="-14" rx="6" ry="10" fill="#e8a0a8" transform="rotate(144)"/>
+              <ellipse cx="0" cy="-14" rx="6" ry="10" fill="#e8a0a8" transform="rotate(216)"/>
+              <ellipse cx="0" cy="-14" rx="6" ry="10" fill="#e8a0a8" transform="rotate(288)"/>
+              <circle cx="0" cy="0" r="4" fill="#f4d0a0"/>
+            </g>
+          </g>
+        </pattern>
+      </defs>
+      <rect width="400" height="600" fill="url(#pfl1)"/>`;
+    }
+  },
+  {
+    id:'marbled-midnight',name:'Venetian Marble',colorway:'Midnight & Gilt',
+    styles:['Maximalist','Dramatic','Classic'],
+    colors:['#0a0a1a','#1a1a3a','#c9a961','#4a4a8a'],
+    draw(svg){
+      svg.innerHTML=`
+      <defs>
+        <filter id="fmrb">
+          <feTurbulence type="turbulence" baseFrequency="0.015 0.008" numOctaves="6" seed="3" result="turb"/>
+          <feColorMatrix type="matrix" values="0 0 0 0 0.06  0 0 0 0 0.06  0 0 0 0 0.15  0 0 0 15 -7" in="turb" result="c1"/>
+          <feBlend in="SourceGraphic" in2="c1" mode="overlay"/>
+        </filter>
+        <linearGradient id="gmrb" x1="0%" y1="0%" x2="100%" y2="100%">
+          <stop offset="0%" stop-color="#0a0a1a"/>
+          <stop offset="40%" stop-color="#1a1a3a"/>
+          <stop offset="100%" stop-color="#0d0d28"/>
+        </linearGradient>
+      </defs>
+      <rect width="400" height="600" fill="url(#gmrb)"/>
+      <rect width="400" height="600" fill="rgba(10,10,26,.5)" filter="url(#fmrb)"/>
+      <g stroke="#c9a961" stroke-width=".8" opacity=".35" fill="none">
+        <path d="M0 150 Q100 130 200 180 Q300 230 400 160"/>
+        <path d="M0 280 Q120 250 200 310 Q280 360 400 290"/>
+        <path d="M0 420 Q80 400 200 450 Q320 490 400 420"/>
+        <path d="M100 0 Q130 150 90 300 Q60 450 120 600"/>
+        <path d="M280 0 Q260 150 300 300 Q330 450 270 600"/>
+      </g>
+      <rect width="400" height="600" fill="rgba(74,74,138,.08)"/>`;
+    }
+  },
+  {
+    id:'trellis-coral',name:'Alhambra Trellis',colorway:'Coral & Ivory',
+    styles:['Geometric','Bohemian','Warm'],
+    colors:['#e8b098','#d4602a','#f4e8e0','#a84820'],
+    draw(svg){
+      svg.innerHTML=`
+      <defs>
+        <pattern id="ptr1" x="0" y="0" width="80" height="80" patternUnits="userSpaceOnUse">
+          <rect width="80" height="80" fill="#f4e8e0"/>
+          <g stroke="#d4602a" fill="none" stroke-width="1.4">
+            <circle cx="0" cy="0" r="20"/>
+            <circle cx="80" cy="0" r="20"/>
+            <circle cx="0" cy="80" r="20"/>
+            <circle cx="80" cy="80" r="20"/>
+            <circle cx="40" cy="40" r="20"/>
+          </g>
+          <g stroke="#e8b098" fill="none" stroke-width=".7" opacity=".6">
+            <circle cx="0" cy="0" r="28"/>
+            <circle cx="80" cy="0" r="28"/>
+            <circle cx="0" cy="80" r="28"/>
+            <circle cx="80" cy="80" r="28"/>
+            <circle cx="40" cy="40" r="28"/>
+          </g>
+          <circle cx="40" cy="40" r="5" fill="#a84820" opacity=".6"/>
+          <circle cx="0" cy="0" r="4" fill="#a84820" opacity=".4"/>
+        </pattern>
+      </defs>
+      <rect width="400" height="600" fill="url(#ptr1)"/>`;
+    }
+  },
+  {
+    id:'linen-white',name:'Zurich Linen',colorway:'Blanc & Natural',
+    styles:['Minimalist','Serene','Textural'],
+    colors:['#f4f1ea','#e0d8c8','#c8bfa8','#a8a090'],
+    draw(svg){
+      svg.innerHTML=`
+      <defs>
+        <pattern id="pln1" x="0" y="0" width="16" height="8" patternUnits="userSpaceOnUse">
+          <rect width="16" height="8" fill="#f4f1ea"/>
+          <line x1="0" y1="4" x2="16" y2="4" stroke="#c8bfa8" stroke-width="1.2" opacity=".35"/>
+          <line x1="0" y1="4" x2="16" y2="4" stroke="#a8a090" stroke-width=".4" opacity=".2"/>
+        </pattern>
+        <pattern id="pln2" x="0" y="0" width="8" height="16" patternUnits="userSpaceOnUse">
+          <rect width="8" height="16" fill="url(#pln1)"/>
+          <line x1="4" y1="0" x2="4" y2="16" stroke="#e0d8c8" stroke-width=".6" opacity=".2"/>
+        </pattern>
+      </defs>
+      <rect width="400" height="600" fill="url(#pln2)"/>`;
+    }
+  },
+];
+
+/* ── PAIRS (hand-curated contrasts) ──────────────────────── */
+const PAIRS = [
+  [0,1],[2,3],[4,5],[6,7],[8,9],[10,11],[12,13],[14,1],
+  [0,7],[3,4],[6,13],[9,14],[2,11],[5,12],[8,0],
+];
+const TOTAL_ROUNDS = 15;
+
+/* ── TASTE PROFILE ───────────────────────────────────────── */
+const PROFILES = [
+  {id:'TW',label:'Traditional & Warm',desc:'You gravitate toward time-honored elegance with rich, enveloping tones.',styles:['Traditional','Formal','Warm','Classic'],recs:[0,3,8]},
+  {id:'BG',label:'Bold & Graphic',desc:'Strong contrasts and geometric precision define your signature style.',styles:['Geometric','Bold','Graphic','Modern'],recs:[4,8,10]},
+  {id:'SR',label:'Serene & Textural',desc:'You seek calm, natural beauty where touch and material speak quietly.',styles:['Serene','Textural','Natural','Minimalist'],recs:[5,14,3]},
+  {id:'RO',label:'Romantic & Botanical',desc:'Lush, storied interiors with flora, fauna, and a hint of nostalgia.',styles:['Romantic','Floral','Botanical','Chinoiserie'],recs:[6,11,3]},
+  {id:'DR',label:'Dramatic & Maximalist',desc:'You curate rooms that command attention — jewel-toned, layered, unforgettable.',styles:['Maximalist','Dramatic','Glamorous','Exotic'],recs:[12,9,7]},
+  {id:'BH',label:'Bohemian & Global',desc:'Pattern-mixing adventurer, drawn to artisan textiles and worldly motifs.',styles:['Bohemian','Exotic','Bold','Ikat'],recs:[2,13,9]},
+];
+
+/* ── STATE ────────────────────────────────────────────────── */
+let round=0, picks=[], muted=false;
+let styleScore={};
+
+/* ── AUDIO ───────────────────────────────────────────────── */
+let ctx;
+function getAudio(){if(!ctx)ctx=new(window.AudioContext||window.webkitAudioContext)();return ctx}
+function beep(freq,dur,type='sine',gain=.18){
+  if(muted)return;
+  try{
+    const a=getAudio(),o=a.createOscillator(),g=a.createGain();
+    o.connect(g);g.connect(a.destination);
+    o.type=type;o.frequency.value=freq;
+    g.gain.setValueAtTime(0,a.currentTime);
+    g.gain.linearRampToValueAtTime(gain,a.currentTime+.01);
+    g.gain.exponentialRampToValueAtTime(.001,a.currentTime+dur);
+    o.start(a.currentTime);o.stop(a.currentTime+dur+.05);
+  }catch(e){}
+}
+function sfxPick(){beep(440,.08,'sine',.15);setTimeout(()=>beep(660,.12,'sine',.12),90)}
+function sfxWhoosh(){
+  if(muted)return;
+  try{
+    const a=getAudio(),o=a.createOscillator(),g=a.createGain();
+    o.connect(g);g.connect(a.destination);
+    o.type='sawtooth';
+    o.frequency.setValueAtTime(200,a.currentTime);
+    o.frequency.exponentialRampToValueAtTime(60,a.currentTime+.3);
+    g.gain.setValueAtTime(.1,a.currentTime);
+    g.gain.exponentialRampToValueAtTime(.001,a.currentTime+.3);
+    o.start(a.currentTime);o.stop(a.currentTime+.35);
+  }catch(e){}
+}
+function sfxResult(){
+  if(muted)return;
+  [523,659,784,1046].forEach((f,i)=>setTimeout(()=>beep(f,.3,'sine',.14),i*120));
+}
+
+/* ── RENDER ──────────────────────────────────────────────── */
+function renderPanel(svgEl,nameEl,colorwayEl,pat){
+  pat.draw(svgEl);
+  nameEl.textContent=pat.name;
+  colorwayEl.textContent=pat.colorway;
+}
+
+function loadRound(animated){
+  if(round>=TOTAL_ROUNDS){showResult();return}
+  const [ai,bi]=PAIRS[round%PAIRS.length];
+  const a=PATTERNS[ai],b=PATTERNS[bi];
+  const arena=document.getElementById('arena');
+
+  function doRender(){
+    // remove animation classes
+    arena.classList.remove('slide-out','slide-in');
+    document.getElementById('panel-a').classList.remove('chosen','loser');
+    document.getElementById('panel-b').classList.remove('chosen','loser');
+    // restore hover
+    document.getElementById('panel-a').style.pointerEvents='';
+    document.getElementById('panel-b').style.pointerEvents='';
+    document.getElementById('svg-a').style.opacity='';
+    document.getElementById('svg-b').style.opacity='';
+
+    renderPanel(
+      document.getElementById('svg-a'),
+      document.getElementById('name-a'),
+      document.getElementById('colorway-a'),a
+    );
+    renderPanel(
+      document.getElementById('svg-b'),
+      document.getElementById('name-b'),
+      document.getElementById('colorway-b'),b
+    );
+    // progress
+    const pct=(round/TOTAL_ROUNDS)*100;
+    document.getElementById('progress-bar').style.width=pct+'%';
+    document.getElementById('round-counter').textContent=`Round ${round+1} of ${TOTAL_ROUNDS}`;
+
+    if(animated){
+      void arena.offsetWidth;
+      arena.classList.add('slide-in');
+    }
+  }
+
+  if(animated){
+    arena.classList.add('slide-out');
+    arena.addEventListener('animationend',function once(){
+      arena.removeEventListener('animationend',once);
+      doRender();
+    },{once:true});
+  } else {
+    doRender();
+  }
+}
+
+function pick(side){
+  if(document.getElementById('result-screen').classList.contains('visible'))return;
+  const [ai,bi]=PAIRS[round%PAIRS.length];
+  const chosen=side==='a'?PATTERNS[ai]:PATTERNS[bi];
+  const loserPanel=side==='a'?'panel-b':'panel-a';
+  const winnerPanel=side==='a'?'panel-a':'panel-b';
+
+  // disable clicks
+  document.getElementById('panel-a').style.pointerEvents='none';
+  document.getElementById('panel-b').style.pointerEvents='none';
+
+  document.getElementById(winnerPanel).classList.add('chosen');
+  document.getElementById(loserPanel).classList.add('loser');
+
+  // score
+  chosen.styles.forEach(s=>{styleScore[s]=(styleScore[s]||0)+1});
+  picks.push(chosen);
+
+  sfxPick();
+  setTimeout(()=>sfxWhoosh(),180);
+
+  round++;
+  setTimeout(()=>loadRound(true),680);
+}
+
+/* ── RESULT ──────────────────────────────────────────────── */
+function computeProfile(){
+  // find best-matching profile
+  let best=PROFILES[0],bestScore=0;
+  PROFILES.forEach(p=>{
+    let s=0;
+    p.styles.forEach(st=>{s+=(styleScore[st]||0)*2});
+    if(s>bestScore){bestScore=s;best=p}
+  });
+  return best;
+}
+
+function showResult(){
+  sfxResult();
+  const profile=computeProfile();
+  const topStyles=Object.entries(styleScore)
+    .sort((a,b)=>b[1]-a[1]).slice(0,4).map(e=>e[0]);
+
+  // palette from picks
+  const palette=[];
+  picks.slice(0,5).forEach(p=>{
+    p.colors.slice(0,2).forEach(c=>{if(!palette.includes(c)&&palette.length<6)palette.push(c)});
+  });
+
+  const recCards=profile.recs.map(i=>{
+    const p=PATTERNS[i];
+    const tmpSvg=document.createElementNS('http://www.w3.org/2000/svg','svg');
+    tmpSvg.setAttribute('viewBox','0 0 400 600');
+    tmpSvg.setAttribute('preserveAspectRatio','xMidYMid slice');
+    p.draw(tmpSvg);
+    return `<div class="rec-card">
+      ${tmpSvg.outerHTML}
+      <div class="rec-card-name">${p.name}</div>
+    </div>`;
+  }).join('');
+
+  const swatches=palette.map(c=>`<div class="swatch" style="background:${c}"></div>`).join('');
+  const chips=topStyles.map(s=>`<div class="style-chip">${s}</div>`).join('');
+
+  document.getElementById('result-inner').innerHTML=`
+    <div class="result-eyebrow">Your Taste Profile</div>
+    <div class="result-title">${profile.label}</div>
+    <div class="result-subtitle">${profile.desc}</div>
+    <div class="result-palette">${swatches}</div>
+    <div class="result-styles">${chips}</div>
+    <div class="result-recs-title">Your Signature Patterns</div>
+    <div class="rec-grid">${recCards}</div>
+    <div class="result-btns">
+      <button class="btn btn-primary" onclick="saveCard()">Save Result Card</button>
+      <button class="btn btn-secondary" onclick="playAgain()">Play Again</button>
+    </div>
+  `;
+
+  const rs=document.getElementById('result-screen');
+  rs.style.visibility='visible';
+  rs.style.opacity='0';
+  rs.classList.add('visible');
+  document.getElementById('or-divider').style.display='none';
+
+  // stagger reveal
+  setTimeout(()=>{
+    rs.querySelectorAll('.result-eyebrow,.result-title,.result-subtitle,.result-palette,.result-styles,.result-recs-title,.rec-grid,.result-btns').forEach((el,i)=>{
+      el.style.opacity='0';el.style.transform='translateY(20px)';el.style.transition=`opacity .5s ${i*.08}s var(--ease),transform .5s ${i*.08}s var(--ease)`;
+      requestAnimationFrame(()=>{el.style.opacity='1';el.style.transform='translateY(0)'});
+    });
+  },100);
+}
+
+function playAgain(){
+  round=0;picks=[];styleScore={};
+  document.getElementById('result-screen').classList.remove('visible');
+  document.getElementById('result-screen').style.visibility='hidden';
+  document.getElementById('or-divider').style.display='';
+  document.getElementById('progress-bar').style.width='0%';
+  loadRound(false);
+}
+
+/* ── SHARE / SAVE ────────────────────────────────────────── */
+function saveCard(){
+  const profile=computeProfile();
+  const c=document.createElement('canvas');
+  c.width=800;c.height=480;
+  const ctx2=c.getContext('2d');
+  // bg
+  ctx2.fillStyle='#0d0b09';ctx2.fillRect(0,0,800,480);
+  // border
+  ctx2.strokeStyle='#c9a961';ctx2.lineWidth=2;
+  ctx2.strokeRect(20,20,760,440);
+  ctx2.strokeStyle='rgba(201,169,97,.35)';ctx2.lineWidth=1;
+  ctx2.strokeRect(28,28,744,424);
+  // text
+  ctx2.fillStyle='#c9a961';ctx2.font='12px sans-serif';
+  ctx2.letterSpacing='3px';
+  ctx2.fillText('DESIGNER WALLCOVERINGS',40,60);
+  ctx2.fillStyle='#f4f1ea';ctx2.font='bold 42px serif';
+  ctx2.fillText(profile.label,40,130);
+  ctx2.fillStyle='#8a857d';ctx2.font='15px sans-serif';
+  const words=profile.desc.split(' ');let line='',y=175;
+  words.forEach(w=>{
+    const t=line?line+' '+w:w;
+    if(ctx2.measureText(t).width>680){ctx2.fillText(line,40,y);line=w;y+=22}else line=t;
+  });
+  ctx2.fillText(line,40,y);
+  // palette swatches
+  const palette=[];
+  picks.slice(0,5).forEach(p=>{p.colors.slice(0,2).forEach(col=>{if(!palette.includes(col)&&palette.length<6)palette.push(col)})});
+  palette.forEach((col,i)=>{
+    ctx2.beginPath();ctx2.arc(60+i*60,280,22,0,Math.PI*2);
+    ctx2.fillStyle=col;ctx2.fill();
+    ctx2.strokeStyle='rgba(255,255,255,.15)';ctx2.lineWidth=1.5;ctx2.stroke();
+  });
+  ctx2.fillStyle='rgba(201,169,97,.5)';ctx2.fillRect(40,330,720,1);
+  ctx2.fillStyle='#8a857d';ctx2.font='11px sans-serif';
+  ctx2.fillText('designerwallcoverings.com  ·  Play at model-arena',40,360);
+
+  const a=document.createElement('a');
+  a.download='dw-taste-profile.png';
+  a.href=c.toDataURL('image/png');
+  a.click();
+}
+
+/* ── EVENTS ──────────────────────────────────────────────── */
+document.getElementById('panel-a').addEventListener('click',()=>pick('a'));
+document.getElementById('panel-b').addEventListener('click',()=>pick('b'));
+document.addEventListener('keydown',e=>{
+  if(document.getElementById('result-screen').classList.contains('visible')){
+    if(e.key==='Enter'||e.key===' ')playAgain();
+    return;
+  }
+  if(e.key==='ArrowLeft')pick('a');
+  if(e.key==='ArrowRight')pick('b');
+});
+document.getElementById('mute-btn').addEventListener('click',()=>{
+  muted=!muted;
+  document.getElementById('mute-btn').textContent=muted?'♪̶':'♪';
+  document.getElementById('mute-btn').setAttribute('aria-label',muted?'Unmute':'Mute');
+});
+
+// touch swipe
+let tx=0;
+document.getElementById('arena').addEventListener('touchstart',e=>{tx=e.touches[0].clientX},{passive:true});
+document.getElementById('arena').addEventListener('touchend',e=>{
+  const dx=e.changedTouches[0].clientX-tx;
+  if(Math.abs(dx)>40)pick(dx>0?'a':'b');
+},{passive:true});
+
+// init
+loadRound(false);
+</script>
+</body>
+</html>

← f4f86dc Agent-driven game: DW Pattern Runner (frontend-developer) —  ·  back to Model Arena  ·  Agent-driven tool: DW Palette Mixer (frontend-developer) — H e89161d →