← 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
M data/arcade-games.jsonA public/games/dw-palette-mixer.htmlA public/games/dw-this-or-that.html
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">← 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 →</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 →