[object Object]

← back to Model Arena

Agent-driven art toy: DW Motif Spinner (frontend-developer) — 3-reel slot → generated swatch + collection name, rarity jackpots. Registered.

a0778d81a3f0caa49e630ed5c65998ae06754bce · 2026-07-25 12:55:42 -0700 · Steve Abrams

Files touched

Diff

commit a0778d81a3f0caa49e630ed5c65998ae06754bce
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Jul 25 12:55:42 2026 -0700

    Agent-driven art toy: DW Motif Spinner (frontend-developer) — 3-reel slot → generated swatch + collection name, rarity jackpots. Registered.
---
 data/arcade-games.json             |   7 +
 public/games/dw-motif-spinner.html | 831 +++++++++++++++++++++++++++++++++++++
 2 files changed, 838 insertions(+)

diff --git a/data/arcade-games.json b/data/arcade-games.json
index d5b72bb..01802c7 100644
--- a/data/arcade-games.json
+++ b/data/arcade-games.json
@@ -174,6 +174,13 @@
       "credit": "frontend-developer",
       "category": "Tools",
       "note": "Agent-built tool \u2014 one pattern across 10 named colorways, live recolor + compare grid + room vignette + favorites"
+    },
+    {
+      "slug": "dw-motif-spinner",
+      "title": "DW Motif Spinner",
+      "credit": "frontend-developer",
+      "category": "Art Toys",
+      "note": "Agent-built slot toy \u2014 spin Motif \u00d7 Colorway \u00d7 Finish reels into a generated wallcovering swatch + collection name, rarity jackpots"
     }
   ]
 }
\ No newline at end of file
diff --git a/public/games/dw-motif-spinner.html b/public/games/dw-motif-spinner.html
new file mode 100644
index 0000000..8fba5e0
--- /dev/null
+++ b/public/games/dw-motif-spinner.html
@@ -0,0 +1,831 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Motif Spinner — Designer Wallcoverings</title>
+<style>
+:root{
+  --bg:#0d0b09;--surface:#1a1712;--surface2:#252018;--ink:#f4f1ea;--ink2:#c4bfb2;
+  --accent:#c9a961;--accent2:#e8c97a;--accent-dim:#8a6e3a;
+  --serif:'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif;
+  --sans:system-ui,-apple-system,sans-serif;
+  --ease:cubic-bezier(.22,.68,0,1.2);
+  --reel-w:160px;--reel-h:420px;--cell-h:84px;
+  --radius:6px;--gutter:18px;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+html,body{min-height:100vh;background:var(--bg);color:var(--ink);font-family:var(--sans);overscroll-behavior:none}
+
+/* ── HEADER ── */
+header{
+  display:flex;align-items:center;justify-content:space-between;
+  padding:14px 24px 12px;
+  border-bottom:1px solid rgba(201,169,97,.2);
+  background:var(--bg);position:sticky;top:0;z-index:50;
+}
+.wordmark{
+  font-family:var(--serif);font-size:clamp(22px,3vw,40px);
+  text-transform:uppercase;letter-spacing:.18em;color:var(--ink);
+  font-weight:400;line-height:1;
+}
+.wordmark span{color:var(--accent);font-size:.55em;display:block;letter-spacing:.32em;margin-top:3px}
+#mute-btn{
+  background:none;border:1px solid var(--accent-dim);color:var(--accent);
+  font-size:18px;width:36px;height:36px;border-radius:50%;cursor:pointer;
+  transition:all .2s;display:flex;align-items:center;justify-content:center;
+}
+#mute-btn:hover{background:var(--accent-dim);color:var(--bg)}
+
+/* ── MAIN ── */
+main{max-width:900px;margin:0 auto;padding:24px var(--gutter) 40px}
+
+.tagline{
+  text-align:center;font-family:var(--serif);font-size:clamp(13px,1.6vw,17px);
+  color:var(--ink2);letter-spacing:.12em;text-transform:uppercase;margin-bottom:28px;
+}
+
+/* ── MACHINE FRAME ── */
+.machine{
+  background:var(--surface);
+  border:1px solid rgba(201,169,97,.25);
+  border-radius:12px;
+  padding:24px 20px 28px;
+  position:relative;
+  box-shadow:0 0 60px rgba(0,0,0,.6),inset 0 1px 0 rgba(201,169,97,.15);
+}
+.machine::before{
+  content:'';position:absolute;inset:0;border-radius:12px;pointer-events:none;
+  background:radial-gradient(ellipse at 50% 0%,rgba(201,169,97,.06) 0%,transparent 60%);
+}
+
+/* ── REEL ROW ── */
+.reels-wrap{
+  display:flex;gap:12px;justify-content:center;align-items:flex-start;
+  flex-wrap:nowrap;overflow-x:auto;padding-bottom:4px;
+}
+.reel-col{display:flex;flex-direction:column;align-items:center;gap:8px;min-width:var(--reel-w)}
+
+.reel-label{
+  font-family:var(--serif);font-size:11px;text-transform:uppercase;
+  letter-spacing:.22em;color:var(--accent);font-weight:600;
+}
+
+/* ── REEL WINDOW ── */
+.reel-window{
+  width:var(--reel-w);height:var(--reel-h);
+  border:1.5px solid rgba(201,169,97,.35);border-radius:var(--radius);
+  overflow:hidden;position:relative;
+  background:var(--surface2);
+  box-shadow:inset 0 0 24px rgba(0,0,0,.5);
+}
+.reel-window::before,.reel-window::after{
+  content:'';position:absolute;left:0;right:0;height:70px;z-index:4;pointer-events:none;
+}
+.reel-window::before{top:0;background:linear-gradient(to bottom,var(--surface2) 0%,transparent 100%)}
+.reel-window::after{bottom:0;background:linear-gradient(to top,var(--surface2) 0%,transparent 100%)}
+
+/* selection band */
+.reel-window .band{
+  position:absolute;top:50%;transform:translateY(-50%);left:0;right:0;
+  height:var(--cell-h);z-index:3;pointer-events:none;
+  border-top:1.5px solid var(--accent);border-bottom:1.5px solid var(--accent);
+  background:rgba(201,169,97,.04);
+}
+
+.reel-track{
+  position:absolute;top:0;left:0;right:0;
+  will-change:transform;
+}
+.reel-cell{
+  width:var(--reel-w);height:var(--cell-h);
+  display:flex;flex-direction:column;align-items:center;justify-content:center;
+  gap:5px;padding:6px 4px;
+  font-family:var(--serif);font-size:12.5px;color:var(--ink2);
+  text-align:center;letter-spacing:.04em;
+  transition:color .15s;
+}
+.reel-cell.active-cell{color:var(--ink);font-weight:600}
+.reel-cell svg{flex-shrink:0}
+
+/* ── LOCK BUTTON ── */
+.lock-btn{
+  background:var(--surface);border:1px solid var(--accent-dim);
+  color:var(--accent-dim);font-size:10px;letter-spacing:.15em;
+  text-transform:uppercase;font-family:var(--serif);
+  padding:5px 12px;border-radius:20px;cursor:pointer;
+  transition:all .2s;white-space:nowrap;
+}
+.lock-btn.locked{
+  background:var(--accent);color:var(--bg);border-color:var(--accent);
+  box-shadow:0 0 10px rgba(201,169,97,.4);
+}
+.lock-btn:hover:not(.locked){border-color:var(--accent);color:var(--accent)}
+
+/* ── SPIN LEVER ── */
+.spin-area{display:flex;flex-direction:column;align-items:center;gap:14px;margin-top:22px}
+#spin-btn{
+  font-family:var(--serif);font-size:clamp(16px,2.5vw,22px);
+  text-transform:uppercase;letter-spacing:.22em;
+  background:linear-gradient(135deg,var(--accent) 0%,var(--accent2) 50%,var(--accent) 100%);
+  background-size:200% 200%;
+  color:var(--bg);border:none;
+  padding:14px 52px;border-radius:40px;cursor:pointer;
+  box-shadow:0 4px 20px rgba(201,169,97,.35),0 1px 0 rgba(255,255,255,.15) inset;
+  transition:transform .12s var(--ease),box-shadow .12s;
+  position:relative;overflow:hidden;
+}
+#spin-btn::after{
+  content:'';position:absolute;inset:0;
+  background:linear-gradient(135deg,transparent 0%,rgba(255,255,255,.12) 50%,transparent 100%);
+}
+#spin-btn:hover{transform:scale(1.04);box-shadow:0 6px 28px rgba(201,169,97,.5)}
+#spin-btn:active{transform:scale(.97)}
+#spin-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}
+.spin-hint{font-size:11px;color:var(--ink2);letter-spacing:.1em;opacity:.6}
+
+/* ── RESULT PANEL ── */
+#result-panel{
+  margin-top:26px;
+  background:var(--surface2);border:1px solid rgba(201,169,97,.2);border-radius:8px;
+  overflow:hidden;
+  display:none;
+}
+#result-panel.visible{display:block;animation:slideUp .4s var(--ease)}
+@keyframes slideUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
+
+.result-inner{display:grid;grid-template-columns:200px 1fr;gap:0}
+@media(max-width:600px){.result-inner{grid-template-columns:1fr}}
+
+#swatch-canvas-wrap{
+  width:200px;height:200px;background:var(--surface);
+  display:flex;align-items:center;justify-content:center;overflow:hidden;
+  flex-shrink:0;
+}
+@media(max-width:600px){#swatch-canvas-wrap{width:100%;height:160px}}
+
+#swatch-svg{width:100%;height:100%;display:block}
+
+.result-meta{padding:20px 22px;display:flex;flex-direction:column;justify-content:center;gap:10px}
+.combo-name{
+  font-family:var(--serif);font-size:clamp(18px,2.5vw,26px);
+  color:var(--ink);letter-spacing:.04em;line-height:1.2;
+}
+.combo-tags{display:flex;gap:7px;flex-wrap:wrap}
+.tag{
+  font-size:10px;letter-spacing:.14em;text-transform:uppercase;
+  background:rgba(201,169,97,.1);color:var(--accent);
+  border:1px solid rgba(201,169,97,.25);border-radius:20px;
+  padding:3px 10px;
+}
+.rarity{font-family:var(--serif);font-size:13px;color:var(--accent2);letter-spacing:.08em;font-style:italic}
+
+.result-actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:4px}
+.act-btn{
+  font-family:var(--serif);font-size:11px;text-transform:uppercase;letter-spacing:.14em;
+  padding:7px 16px;border-radius:20px;cursor:pointer;transition:all .18s;
+  border:1px solid var(--accent-dim);background:transparent;color:var(--accent);
+}
+.act-btn:hover{background:var(--accent);color:var(--bg);border-color:var(--accent)}
+.act-btn.primary{background:var(--accent);color:var(--bg);border-color:var(--accent)}
+.act-btn.primary:hover{background:var(--accent2)}
+
+/* ── LOOKBOOK ── */
+#lookbook-section{margin-top:30px}
+.lb-header{
+  display:flex;align-items:center;justify-content:space-between;
+  margin-bottom:14px;
+}
+.lb-title{font-family:var(--serif);font-size:15px;text-transform:uppercase;letter-spacing:.18em;color:var(--ink2)}
+#lb-strip{display:flex;gap:10px;flex-wrap:wrap}
+.lb-card{
+  width:90px;background:var(--surface);border:1px solid rgba(201,169,97,.2);
+  border-radius:5px;overflow:hidden;cursor:pointer;
+  transition:transform .2s,border-color .2s;flex-shrink:0;
+}
+.lb-card:hover{transform:scale(1.06);border-color:var(--accent)}
+.lb-card-swatch{width:90px;height:72px;display:block}
+.lb-card-label{
+  font-family:var(--serif);font-size:8px;letter-spacing:.08em;
+  text-transform:uppercase;color:var(--ink2);padding:4px 5px;
+  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:center;
+}
+.lb-empty{font-size:12px;color:var(--ink2);opacity:.4;font-style:italic;font-family:var(--serif)}
+
+/* ── TOAST ── */
+#toast{
+  position:fixed;bottom:24px;left:50%;transform:translateX(-50%) translateY(20px);
+  background:var(--accent);color:var(--bg);font-family:var(--serif);
+  font-size:13px;letter-spacing:.1em;padding:10px 22px;border-radius:30px;
+  opacity:0;transition:opacity .25s,transform .25s;pointer-events:none;z-index:200;
+  white-space:nowrap;
+}
+#toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
+
+/* ── JACKPOT OVERLAY ── */
+#jackpot-overlay{
+  visibility:hidden;position:fixed;inset:0;z-index:100;
+  display:flex;align-items:center;justify-content:center;
+  background:rgba(0,0,0,.75);backdrop-filter:blur(4px);
+}
+#jackpot-overlay.active{visibility:visible;animation:fadeIn .3s ease}
+@keyframes fadeIn{from{opacity:0}to{opacity:1}}
+.jackpot-box{
+  background:var(--surface);border:2px solid var(--accent);border-radius:12px;
+  padding:40px 48px;text-align:center;max-width:380px;
+  box-shadow:0 0 60px rgba(201,169,97,.3);
+}
+.jackpot-box h2{
+  font-family:var(--serif);font-size:32px;color:var(--accent);
+  letter-spacing:.12em;text-transform:uppercase;margin-bottom:10px;
+}
+.jackpot-box p{font-family:var(--serif);font-size:16px;color:var(--ink2);margin-bottom:22px;letter-spacing:.06em}
+</style>
+</head>
+<body>
+<header>
+  <div class="wordmark">
+    Designer Wallcoverings
+    <span>Motif Spinner</span>
+  </div>
+  <button id="mute-btn" title="Toggle sound" aria-label="Toggle sound">♪</button>
+</header>
+
+<main>
+  <p class="tagline">Spin the reels &mdash; discover your next statement wall</p>
+
+  <div class="machine" role="region" aria-label="Motif Spinner Machine">
+    <div class="reels-wrap" id="reels-wrap">
+      <!-- Reel 1: Motif -->
+      <div class="reel-col">
+        <div class="reel-label">Motif</div>
+        <div class="reel-window" id="reel-0"><div class="band"></div><div class="reel-track" id="track-0"></div></div>
+        <button class="lock-btn" id="lock-0" aria-pressed="false">Lock</button>
+      </div>
+      <!-- Reel 2: Colorway -->
+      <div class="reel-col">
+        <div class="reel-label">Colorway</div>
+        <div class="reel-window" id="reel-1"><div class="band"></div><div class="reel-track" id="track-1"></div></div>
+        <button class="lock-btn" id="lock-1" aria-pressed="false">Lock</button>
+      </div>
+      <!-- Reel 3: Scale / Finish -->
+      <div class="reel-col">
+        <div class="reel-label">Scale &amp; Finish</div>
+        <div class="reel-window" id="reel-2"><div class="band"></div><div class="reel-track" id="track-2"></div></div>
+        <button class="lock-btn" id="lock-2" aria-pressed="false">Lock</button>
+      </div>
+    </div>
+
+    <div class="spin-area">
+      <button id="spin-btn" aria-label="Spin the reels">✦ Spin ✦</button>
+      <span class="spin-hint">Space bar to spin &bull; Lock reels to hold</span>
+    </div>
+  </div>
+
+  <!-- Result -->
+  <div id="result-panel" aria-live="polite">
+    <div class="result-inner">
+      <div id="swatch-canvas-wrap"></div>
+      <div class="result-meta">
+        <div class="combo-name" id="combo-name"></div>
+        <div class="combo-tags" id="combo-tags"></div>
+        <div class="rarity" id="rarity-text"></div>
+        <div class="result-actions">
+          <button class="act-btn primary" id="save-btn">★ Save to Lookbook</button>
+          <button class="act-btn" id="dl-btn">Download Swatch</button>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <!-- Lookbook -->
+  <div id="lookbook-section">
+    <div class="lb-header">
+      <span class="lb-title">✦ Lookbook</span>
+      <button class="act-btn" id="clear-lb-btn" style="font-size:10px;padding:4px 12px">Clear</button>
+    </div>
+    <div id="lb-strip"><span class="lb-empty">Your saved combinations will appear here</span></div>
+  </div>
+</main>
+
+<div id="toast"></div>
+
+<div id="jackpot-overlay" role="dialog" aria-modal="true" aria-label="Rare Combination">
+  <div class="jackpot-box">
+    <h2>Rare Find ✦</h2>
+    <p id="jackpot-msg"></p>
+    <button class="act-btn primary" id="jackpot-close">Wonderful</button>
+  </div>
+</div>
+
+<script>
+// ── DATA ──────────────────────────────────────────────────────────────────────
+const MOTIFS = [
+  { name:'Damask', svg: (c1,c2,s) => damaskSVG(c1,c2,s) },
+  { name:'Toile',  svg: (c1,c2,s) => toileSVG(c1,c2,s) },
+  { name:'Trellis',svg: (c1,c2,s) => trellisSVG(c1,c2,s) },
+  { name:'Chinoiserie',svg:(c1,c2,s)=>chinoiserieSVG(c1,c2,s) },
+  { name:'Ikat',   svg: (c1,c2,s) => ikatSVG(c1,c2,s) },
+  { name:'Geometric',svg:(c1,c2,s)=>geometricSVG(c1,c2,s) },
+  { name:'Botanical',svg:(c1,c2,s)=>botanicalSVG(c1,c2,s) },
+];
+const COLORWAYS = [
+  { name:'Alabaster', bg:'#f5f0e8', fg:'#c8b89a', acc:'#a89070' },
+  { name:'Celadon',   bg:'#d4e8d8', fg:'#7aaa88', acc:'#3d7a54' },
+  { name:'Blush',     bg:'#f0dcd8', fg:'#c8887a', acc:'#9a5048' },
+  { name:'Ink',       bg:'#1c2030', fg:'#4a5878', acc:'#8090b8' },
+  { name:'Brass',     bg:'#2a2010', fg:'#c9a961', acc:'#e8c97a' },
+  { name:'Aubergine', bg:'#2c1a2e', fg:'#7a4a90', acc:'#b87ac8' },
+  { name:'Slate',     bg:'#e0e4e8', fg:'#8090a0', acc:'#506070' },
+  { name:'Terracotta',bg:'#f0dbc8', fg:'#c87040', acc:'#8a4020' },
+];
+const FINISHES = [
+  { name:'Petite',        scale:.55 },
+  { name:'Grand',         scale:1.4 },
+  { name:'Flocked',       scale:.9 },
+  { name:'Metallic',      scale:.9 },
+  { name:'Grasscloth',    scale:1.0 },
+];
+
+// ── SVG MOTIF GENERATORS ─────────────────────────────────────────────────────
+function damaskSVG(c1,c2,s){
+  const r=50*s;
+  return `<pattern id="p" x="0" y="0" width="${100*s}" height="${100*s}" patternUnits="userSpaceOnUse">
+    <rect width="${100*s}" height="${100*s}" fill="${c1}"/>
+    <g fill="${c2}" opacity=".85">
+      <ellipse cx="${50*s}" cy="${50*s}" rx="${18*s}" ry="${30*s}"/>
+      <ellipse cx="${50*s}" cy="${50*s}" rx="${30*s}" ry="${18*s}"/>
+      <ellipse cx="${50*s}" cy="${50*s}" rx="${9*s}" ry="${15*s}" fill="${c1}"/>
+      <ellipse cx="${50*s}" cy="${50*s}" rx="${15*s}" ry="${9*s}" fill="${c1}"/>
+      <circle cx="${50*s}" cy="${50*s}" r="${5*s}"/>
+      <ellipse cx="${50*s}" cy="${20*s}" rx="${6*s}" ry="${10*s}"/>
+      <ellipse cx="${50*s}" cy="${80*s}" rx="${6*s}" ry="${10*s}"/>
+      <ellipse cx="${20*s}" cy="${50*s}" rx="${10*s}" ry="${6*s}"/>
+      <ellipse cx="${80*s}" cy="${50*s}" rx="${10*s}" ry="${6*s}"/>
+    </g>
+  </pattern>`;
+}
+function toileSVG(c1,c2,s){
+  return `<pattern id="p" x="0" y="0" width="${120*s}" height="${120*s}" patternUnits="userSpaceOnUse">
+    <rect width="${120*s}" height="${120*s}" fill="${c1}"/>
+    <g stroke="${c2}" fill="none" stroke-width="${1.2*s}">
+      <path d="M${20*s},${60*s} C${20*s},${30*s} ${60*s},${20*s} ${80*s},${40*s} S${100*s},${80*s} ${80*s},${90*s} S${40*s},${100*s} ${20*s},${60*s}Z"/>
+      <path d="M${55*s},${35*s} C${60*s},${20*s} ${75*s},${15*s} ${80*s},${25*s}"/>
+      <path d="M${80*s},${50*s} L${95*s},${40*s} M${90*s},${60*s} L${105*s},${55*s}"/>
+      <circle cx="${50*s}" cy="${58*s}" r="${8*s}" fill="${c2}" opacity=".18"/>
+      <path d="M${30*s},${70*s} Q${45*s},${90*s} ${65*s},${80*s}"/>
+    </g>
+  </pattern>`;
+}
+function trellisSVG(c1,c2,s){
+  const g=40*s;
+  return `<pattern id="p" x="0" y="0" width="${g}" height="${g}" patternUnits="userSpaceOnUse">
+    <rect width="${g}" height="${g}" fill="${c1}"/>
+    <path d="M0,${g/2} L${g/2},0 L${g},${g/2} L${g/2},${g}Z" fill="none" stroke="${c2}" stroke-width="${2*s}"/>
+    <circle cx="${g/2}" cy="${g/2}" r="${3.5*s}" fill="${c2}" opacity=".7"/>
+    <circle cx="0" cy="0" r="${3.5*s}" fill="${c2}" opacity=".7"/>
+    <circle cx="${g}" cy="0" r="${3.5*s}" fill="${c2}" opacity=".7"/>
+    <circle cx="0" cy="${g}" r="${3.5*s}" fill="${c2}" opacity=".7"/>
+    <circle cx="${g}" cy="${g}" r="${3.5*s}" fill="${c2}" opacity=".7"/>
+  </pattern>`;
+}
+function chinoiserieSVG(c1,c2,s){
+  return `<pattern id="p" x="0" y="0" width="${150*s}" height="${150*s}" patternUnits="userSpaceOnUse">
+    <rect width="${150*s}" height="${150*s}" fill="${c1}"/>
+    <g stroke="${c2}" fill="none" stroke-width="${1.2*s}">
+      <path d="M${40*s},${120*s} C${40*s},${80*s} ${60*s},${60*s} ${75*s},${40*s} C${90*s},${20*s} ${100*s},${30*s} ${95*s},${50*s} C${90*s},${70*s} ${70*s},${60*s} ${75*s},${80*s} S${100*s},${110*s} ${80*s},${130*s}"/>
+      <circle cx="${75*s}" cy="${40*s}" r="${7*s}" fill="${c2}" opacity=".3"/>
+      <circle cx="${80*s}" cy="${60*s}" r="${4*s}" fill="${c2}" opacity=".3"/>
+      <path d="M${50*s},${90*s} Q${55*s},${80*s} ${65*s},${85*s}"/>
+      <path d="M${88*s},${80*s} Q${100*s},${75*s} ${105*s},${85*s}"/>
+      <path d="M${20*s},${50*s} C${25*s},${35*s} ${40*s},${30*s} ${45*s},${45*s}"/>
+    </g>
+    <g fill="${c2}" opacity=".45">
+      <ellipse cx="${30*s}" cy="${30*s}" rx="${4*s}" ry="${8*s}"/>
+      <ellipse cx="${120*s}" cy="${110*s}" rx="${4*s}" ry="${8*s}"/>
+    </g>
+  </pattern>`;
+}
+function ikatSVG(c1,c2,s){
+  const w=60*s,h=70*s;
+  return `<pattern id="p" x="0" y="0" width="${w}" height="${h}" patternUnits="userSpaceOnUse">
+    <rect width="${w}" height="${h}" fill="${c1}"/>
+    <path d="M${w/2},${5*s} C${w*0.8},${h*0.2} ${w*0.9},${h*0.4} ${w/2},${h/2} C${w*0.1},${h*0.6} ${w*0.2},${h*0.8} ${w/2},${h-5*s} C${w*0.8},${h*0.8} ${w*0.9},${h*0.6} ${w/2},${h/2} C${w*0.1},${h*0.4} ${w*0.2},${h*0.2} ${w/2},${5*s}Z"
+      fill="${c2}" opacity=".75"/>
+    <path d="M${w/2},${15*s} C${w*0.7},${h*0.25} ${w*0.8},${h*0.42} ${w/2},${h/2} C${w*0.2},${h*0.58} ${w*0.3},${h*0.75} ${w/2},${h-15*s} C${w*0.7},${h*0.75} ${w*0.8},${h*0.58} ${w/2},${h/2} C${w*0.2},${h*0.42} ${w*0.3},${h*0.25} ${w/2},${15*s}Z"
+      fill="${c1}"/>
+    <ellipse cx="${w/2}" cy="${h/2}" rx="${5*s}" ry="${6*s}" fill="${c2}" opacity=".6"/>
+  </pattern>`;
+}
+function geometricSVG(c1,c2,s){
+  const g=50*s;
+  return `<pattern id="p" x="0" y="0" width="${g}" height="${g}" patternUnits="userSpaceOnUse">
+    <rect width="${g}" height="${g}" fill="${c1}"/>
+    <polygon points="${g/2},${4*s} ${g-4*s},${g/2} ${g/2},${g-4*s} ${4*s},${g/2}" fill="none" stroke="${c2}" stroke-width="${2*s}"/>
+    <polygon points="${g/2},${12*s} ${g-12*s},${g/2} ${g/2},${g-12*s} ${12*s},${g/2}" fill="${c2}" opacity=".18"/>
+    <line x1="${g/2}" y1="0" x2="${g/2}" y2="${g}" stroke="${c2}" stroke-width="${.7*s}" opacity=".25"/>
+    <line x1="0" y1="${g/2}" x2="${g}" y2="${g/2}" stroke="${c2}" stroke-width="${.7*s}" opacity=".25"/>
+  </pattern>`;
+}
+function botanicalSVG(c1,c2,s){
+  return `<pattern id="p" x="0" y="0" width="${130*s}" height="${140*s}" patternUnits="userSpaceOnUse">
+    <rect width="${130*s}" height="${140*s}" fill="${c1}"/>
+    <g stroke="${c2}" fill="none" stroke-width="${1.2*s}">
+      <path d="M${65*s},${130*s} C${65*s},${90*s} ${65*s},${60*s} ${65*s},${20*s}"/>
+      <path d="M${65*s},${80*s} C${40*s},${60*s} ${25*s},${50*s} ${20*s},${35*s}"/>
+      <path d="M${65*s},${65*s} C${90*s},${45*s} ${105*s},${40*s} ${110*s},${25*s}"/>
+      <path d="M${65*s},${100*s} C${45*s},${90*s} ${35*s},${95*s} ${28*s},${105*s}"/>
+      <path d="M${65*s},${90*s} C${85*s},${80*s} ${98*s},${88*s} ${105*s},${100*s}"/>
+    </g>
+    <g fill="${c2}" opacity=".7">
+      <ellipse cx="${20*s}" cy="${30*s}" rx="${12*s}" ry="${7*s}" transform="rotate(-30,${20*s},${30*s})"/>
+      <ellipse cx="${110*s}" cy="${22*s}" rx="${12*s}" ry="${7*s}" transform="rotate(30,${110*s},${22*s})"/>
+      <ellipse cx="${28*s}" cy="${108*s}" rx="${10*s}" ry="${6*s}" transform="rotate(-45,${28*s},${108*s})"/>
+      <ellipse cx="${105*s}" cy="${103*s}" rx="${10*s}" ry="${6*s}" transform="rotate(45,${105*s},${103*s})"/>
+      <circle cx="${65*s}" cy="${18*s}" r="${6*s}"/>
+    </g>
+  </pattern>`;
+}
+
+// ── FINISH OVERLAYS ───────────────────────────────────────────────────────────
+function finishOverlay(finish, accent){
+  if(finish.name==='Flocked') return `<filter id="flock"><feTurbulence type="fractalNoise" baseFrequency=".9" numOctaves="4" result="n"/><feColorMatrix in="n" type="saturate" values="0" result="gray"/><feBlend in="SourceGraphic" in2="gray" mode="multiply"/></filter><rect width="200" height="200" filter="url(#flock)" opacity=".18" fill="${accent}"/>`;
+  if(finish.name==='Metallic') return `<defs><linearGradient id="metal" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="${accent}" stop-opacity=".55"/><stop offset="50%" stop-color="#fff" stop-opacity=".12"/><stop offset="100%" stop-color="${accent}" stop-opacity=".35"/></linearGradient></defs><rect width="200" height="200" fill="url(#metal)" style="mix-blend-mode:overlay"/>`;
+  if(finish.name==='Grasscloth') return `<filter id="grass"><feTurbulence type="turbulence" baseFrequency=".04 .6" numOctaves="3" result="n"/><feDisplacementMap in="SourceGraphic" in2="n" scale="3" xChannelSelector="R" yChannelSelector="G"/></filter><rect width="200" height="200" filter="url(#grass)" fill="transparent"/>`;
+  return '';
+}
+
+// ── PREVIEW SVG ASSEMBLY ──────────────────────────────────────────────────────
+function buildSwatchSVG(motif, colorway, finish){
+  const cw = colorway, s = finish.scale;
+  const pat = motif.svg(cw.bg, cw.fg, s);
+  const over = finishOverlay(finish, cw.acc);
+  return `<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" width="200" height="200">
+    <defs>${pat}</defs>
+    <rect width="200" height="200" fill="url(#p)"/>
+    ${over}
+  </svg>`;
+}
+
+// ── RARITY ────────────────────────────────────────────────────────────────────
+const RARE_COMBOS = [
+  {test:(m,c,f)=>m.name==='Damask'&&c.name==='Brass'&&f.name==='Metallic', msg:'Grand Metallic Damask in Brass — an exceptionally opulent pairing, rarely assembled.'},
+  {test:(m,c,f)=>m.name==='Chinoiserie'&&c.name==='Ink', msg:'Ink Chinoiserie — the classic Blue-and-White tradition, elevated.'},
+  {test:(m,c,f)=>m.name==='Botanical'&&c.name==='Celadon'&&f.name==='Grasscloth', msg:'Celadon Botanical on Grasscloth — a sanctuary combination of the highest distinction.'},
+  {test:(m,c,f)=>m.name==='Toile'&&c.name==='Blush', msg:'Blush Toile — a timeless pastoral in the softest romantic palette.'},
+  {test:(m,c,f)=>f.name==='Flocked'&&c.name==='Aubergine', msg:'Flocked Aubergine — velvet-finish depth beloved in the grandest interiors.'},
+];
+function getRarity(m,c,f){
+  const r = RARE_COMBOS.find(r=>r.test(m,c,f));
+  if(r) return {tier:'Collector\'s Edition ✦✦✦', msg:r.msg};
+  const idx = (MOTIFS.indexOf(m)+COLORWAYS.indexOf(c)+FINISHES.indexOf(f));
+  if(idx%7===0) return {tier:'Sought-After ✦✦', msg:'A pairing that appears in the finest rooms.'};
+  return null;
+}
+
+// ── AUDIO ─────────────────────────────────────────────────────────────────────
+let audioCtx, muted=false;
+function ensureAudio(){if(!audioCtx)audioCtx=new(window.AudioContext||window.webkitAudioContext)();}
+function tick(){
+  if(muted||!audioCtx) return;
+  try{
+    const o=audioCtx.createOscillator(),g=audioCtx.createGain();
+    o.connect(g);g.connect(audioCtx.destination);
+    o.frequency.setValueAtTime(220+Math.random()*120,audioCtx.currentTime);
+    o.type='triangle';
+    g.gain.setValueAtTime(.06,audioCtx.currentTime);
+    g.gain.exponentialRampToValueAtTime(.001,audioCtx.currentTime+.04);
+    o.start();o.stop(audioCtx.currentTime+.05);
+  }catch(e){}
+}
+function settle(){
+  if(muted||!audioCtx) return;
+  try{
+    [0,.08,.14].forEach((t,i)=>{
+      const o=audioCtx.createOscillator(),g=audioCtx.createGain();
+      o.connect(g);g.connect(audioCtx.destination);
+      o.frequency.value=330-i*40;o.type='sine';
+      g.gain.setValueAtTime(.09,audioCtx.currentTime+t);
+      g.gain.exponentialRampToValueAtTime(.001,audioCtx.currentTime+t+.18);
+      o.start(audioCtx.currentTime+t);o.stop(audioCtx.currentTime+t+.2);
+    });
+  }catch(e){}
+}
+function jackpotChime(){
+  if(muted||!audioCtx) return;
+  try{
+    [523,659,784,1047].forEach((f,i)=>{
+      const o=audioCtx.createOscillator(),g=audioCtx.createGain();
+      o.connect(g);g.connect(audioCtx.destination);
+      o.frequency.value=f;o.type='sine';
+      const t=audioCtx.currentTime+i*.12;
+      g.gain.setValueAtTime(.12,t);
+      g.gain.exponentialRampToValueAtTime(.001,t+.45);
+      o.start(t);o.stop(t+.5);
+    });
+  }catch(e){}
+}
+
+// ── REEL ENGINE ───────────────────────────────────────────────────────────────
+const VISIBLE = 5; // cells visible
+const PAD = 3; // padding above/below center
+const TOTAL_CELLS = VISIBLE + PAD*2; // track height
+const CELL_H = 84; // px, must match --cell-h
+
+const reelData = [MOTIFS, COLORWAYS, FINISHES];
+const locked = [false, false, false];
+const currentIdx = [0, 0, 0];
+const tracks = [null,null,null];
+let spinning = false;
+
+// Build cell HTML for a single item
+function cellHTML(item, i, reelIdx){
+  const label = item.name;
+  let icon = '';
+  if(reelIdx===0) icon = motifIcon(item.name);
+  else if(reelIdx===1) icon = colorIcon(item);
+  else icon = finishIcon(item.name);
+  return `<div class="reel-cell" data-i="${i}">${icon}<span>${label}</span></div>`;
+}
+
+function motifIcon(name){
+  const icons={
+    Damask:`<svg width="40" height="40" viewBox="0 0 40 40"><ellipse cx="20" cy="20" rx="7" ry="12" fill="#c9a961" opacity=".7"/><ellipse cx="20" cy="20" rx="12" ry="7" fill="#c9a961" opacity=".5"/><circle cx="20" cy="20" r="3" fill="#f4f1ea"/></svg>`,
+    Toile:`<svg width="40" height="40" viewBox="0 0 40 40"><path d="M8,20 C8,10 18,6 24,14 S34,28 24,32 S10,30 8,20Z" fill="none" stroke="#c9a961" stroke-width="1.5"/></svg>`,
+    Trellis:`<svg width="40" height="40" viewBox="0 0 40 40"><polygon points="20,4 36,20 20,36 4,20" fill="none" stroke="#c9a961" stroke-width="2"/><circle cx="20" cy="20" r="3" fill="#c9a961"/></svg>`,
+    Chinoiserie:`<svg width="40" height="40" viewBox="0 0 40 40"><path d="M20,34 C20,20 20,15 20,6" stroke="#c9a961" stroke-width="1.2" fill="none"/><path d="M20,22 C12,16 8,14 7,8" stroke="#c9a961" stroke-width="1.2" fill="none"/><circle cx="20" cy="6" r="4" fill="#c9a961" opacity=".6"/></svg>`,
+    Ikat:`<svg width="40" height="40" viewBox="0 0 40 40"><path d="M20,4 C28,10 30,16 20,20 C10,24 12,30 20,36 C28,30 30,24 20,20 C10,16 12,10 20,4Z" fill="#c9a961" opacity=".6"/></svg>`,
+    Geometric:`<svg width="40" height="40" viewBox="0 0 40 40"><polygon points="20,5 35,20 20,35 5,20" fill="none" stroke="#c9a961" stroke-width="1.8"/><polygon points="20,12 28,20 20,28 12,20" fill="#c9a961" opacity=".3"/></svg>`,
+    Botanical:`<svg width="40" height="40" viewBox="0 0 40 40"><path d="M20,36 L20,10" stroke="#c9a961" stroke-width="1.2" fill="none"/><ellipse cx="12" cy="18" rx="8" ry="5" fill="#c9a961" opacity=".55" transform="rotate(-25,12,18)"/><ellipse cx="28" cy="14" rx="8" ry="5" fill="#c9a961" opacity=".55" transform="rotate(25,28,14)"/></svg>`,
+  };
+  return icons[name]||'';
+}
+function colorIcon(cw){
+  return `<svg width="28" height="28" viewBox="0 0 28 28"><circle cx="14" cy="14" r="12" fill="${cw.bg}" stroke="${cw.fg}" stroke-width="2"/><circle cx="14" cy="14" r="5" fill="${cw.acc}"/></svg>`;
+}
+function finishIcon(name){
+  const icons={
+    Petite:`<svg width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="4" fill="#c9a961" opacity=".8"/><circle cx="8" cy="8" r="2" fill="#c9a961" opacity=".4"/><circle cx="24" cy="24" r="2" fill="#c9a961" opacity=".4"/></svg>`,
+    Grand:`<svg width="32" height="32" viewBox="0 0 32 32"><ellipse cx="16" cy="16" rx="12" ry="8" fill="none" stroke="#c9a961" stroke-width="2"/><ellipse cx="16" cy="16" rx="5" ry="3" fill="#c9a961" opacity=".5"/></svg>`,
+    Flocked:`<svg width="32" height="32" viewBox="0 0 32 32"><circle cx="10" cy="10" r="3" fill="#c9a961" opacity=".5"/><circle cx="22" cy="10" r="3" fill="#c9a961" opacity=".6"/><circle cx="16" cy="20" r="3" fill="#c9a961" opacity=".7"/><circle cx="8" cy="22" r="2" fill="#c9a961" opacity=".4"/><circle cx="24" cy="22" r="2" fill="#c9a961" opacity=".4"/></svg>`,
+    Metallic:`<svg width="32" height="32" viewBox="0 0 32 32"><polygon points="16,4 20,12 28,14 22,20 24,28 16,24 8,28 10,20 4,14 12,12" fill="#c9a961" opacity=".7"/></svg>`,
+    Grasscloth:`<svg width="32" height="32" viewBox="0 0 32 32"><line x1="4" y1="4" x2="4" y2="28" stroke="#c9a961" stroke-width="1.2" opacity=".6"/><line x1="10" y1="4" x2="10" y2="28" stroke="#c9a961" stroke-width="1.2" opacity=".5"/><line x1="16" y1="4" x2="16" y2="28" stroke="#c9a961" stroke-width="1.2" opacity=".65"/><line x1="22" y1="4" x2="22" y2="28" stroke="#c9a961" stroke-width="1.2" opacity=".5"/><line x1="28" y1="4" x2="28" y2="28" stroke="#c9a961" stroke-width="1.2" opacity=".6"/></svg>`,
+  };
+  return icons[name]||'';
+}
+
+function buildTrack(reelIdx){
+  const data = reelData[reelIdx];
+  const track = document.getElementById(`track-${reelIdx}`);
+  track.innerHTML = '';
+  // Build a looping strip: repeat items enough for smooth animation
+  const reps = 4;
+  for(let r=0;r<reps;r++) data.forEach((item,i)=>{ track.innerHTML += cellHTML(item,r*data.length+i,reelIdx); });
+  tracks[reelIdx] = track;
+  // Position so center cell shows currentIdx
+  positionReel(reelIdx, currentIdx[reelIdx], false);
+}
+
+function positionReel(reelIdx, targetIdx, animate){
+  const data = reelData[reelIdx];
+  const track = tracks[reelIdx];
+  const n = data.length;
+  // We want the cell at targetIdx to be centered
+  // Center of window = CELL_H * PAD offset from track top
+  const centerOffset = CELL_H * PAD;
+  // Target cell offset within one rep: targetIdx * CELL_H
+  const baseY = -(targetIdx * CELL_H) + centerOffset;
+  track.style.transition = animate ? `transform .6s cubic-bezier(.22,.68,0,1.2)` : 'none';
+  track.style.transform = `translateY(${baseY}px)`;
+  updateActiveCells(reelIdx, targetIdx);
+}
+
+function updateActiveCells(reelIdx, idx){
+  const data = reelData[reelIdx];
+  const n = data.length;
+  tracks[reelIdx].querySelectorAll('.reel-cell').forEach(el=>{
+    const i = parseInt(el.dataset.i);
+    el.classList.toggle('active-cell', i % n === idx % n);
+  });
+}
+
+// ── SPIN LOGIC ────────────────────────────────────────────────────────────────
+function spinReels(){
+  if(spinning) return;
+  ensureAudio();
+  spinning = true;
+  const btn = document.getElementById('spin-btn');
+  btn.disabled = true;
+
+  const promises = reelData.map((_,ri)=>{
+    if(locked[ri]) return Promise.resolve(currentIdx[ri]);
+    return spinOneReel(ri);
+  });
+
+  Promise.all(promises).then(results=>{
+    results.forEach((idx,ri)=>{ currentIdx[ri]=idx; });
+    settle();
+    setTimeout(()=>{
+      showResult();
+      spinning=false;
+      btn.disabled=false;
+    },200);
+  });
+}
+
+function spinOneReel(ri){
+  return new Promise(resolve=>{
+    const data = reelData[ri];
+    const n = data.length;
+    const track = tracks[ri];
+    const targetIdx = Math.floor(Math.random()*n);
+    const fullReps = 3 + Math.floor(Math.random()*2); // 3-4 full spins
+    const totalCells = fullReps * n + targetIdx;
+    const duration = 1200 + ri*250 + Math.random()*300;
+    const startTime = performance.now();
+    const startOffset = currentIdx[ri];
+    let lastTickCell = -1;
+
+    function step(now){
+      const elapsed = now - startTime;
+      const t = Math.min(elapsed/duration,1);
+      // Ease: fast in, slow out with bounce
+      const ease = easeOutBounce(t);
+      const cellOffset = ease * totalCells;
+      const px = -((startOffset + cellOffset) % (n*4)) * CELL_H + CELL_H*PAD;
+
+      // Apply blur during fast phase
+      const speed = t < .8 ? 1-t/0.8 : 0;
+      track.style.filter = speed>0.1 ? `blur(${speed*3.5}px)` : '';
+      track.style.transform = `translateY(${px}px)`;
+
+      // Tick sound every new cell
+      const cellNow = Math.floor(startOffset+cellOffset);
+      if(cellNow!==lastTickCell){tick();lastTickCell=cellNow;}
+
+      if(t<1){ requestAnimationFrame(step); }
+      else{
+        track.style.filter='';
+        track.style.transform=`translateY(${-(targetIdx*CELL_H)+CELL_H*PAD}px)`;
+        updateActiveCells(ri,targetIdx);
+        resolve(targetIdx);
+      }
+    }
+    requestAnimationFrame(step);
+  });
+}
+
+function easeOutBounce(t){
+  if(t<1/2.75) return 7.5625*t*t;
+  else if(t<2/2.75){t-=1.5/2.75;return 7.5625*t*t+.75;}
+  else if(t<2.5/2.75){t-=2.25/2.75;return 7.5625*t*t+.9375;}
+  else{t-=2.625/2.75;return 7.5625*t*t+.984375;}
+}
+
+// ── RESULT ────────────────────────────────────────────────────────────────────
+let currentCombo = null;
+
+function showResult(){
+  const m = MOTIFS[currentIdx[0]];
+  const c = COLORWAYS[currentIdx[1]];
+  const f = FINISHES[currentIdx[2]];
+  currentCombo = {m,c,f};
+
+  const svgStr = buildSwatchSVG(m,c,f);
+  document.getElementById('swatch-canvas-wrap').innerHTML = svgStr;
+
+  const name = `${f.name} ${c.name} ${m.name}`;
+  document.getElementById('combo-name').textContent = name;
+  document.getElementById('combo-tags').innerHTML =
+    [m.name,c.name,f.name].map(t=>`<span class="tag">${t}</span>`).join('');
+
+  const rarity = getRarity(m,c,f);
+  const rarityEl = document.getElementById('rarity-text');
+  if(rarity){
+    rarityEl.textContent = rarity.tier;
+    if(RARE_COMBOS.some(r=>r.test(m,c,f))){
+      jackpotChime();
+      setTimeout(()=>showJackpot(name, rarity.msg), 600);
+    }
+  } else { rarityEl.textContent=''; }
+
+  const panel = document.getElementById('result-panel');
+  panel.classList.remove('visible');
+  void panel.offsetWidth;
+  panel.style.display='block';
+  panel.classList.add('visible');
+}
+
+// ── JACKPOT ───────────────────────────────────────────────────────────────────
+function showJackpot(name, msg){
+  document.getElementById('jackpot-msg').innerHTML = `<strong>${name}</strong><br><br>${msg}`;
+  document.getElementById('jackpot-overlay').classList.add('active');
+}
+document.getElementById('jackpot-close').addEventListener('click',()=>{
+  document.getElementById('jackpot-overlay').classList.remove('active');
+});
+document.getElementById('jackpot-overlay').addEventListener('click',e=>{
+  if(e.target===e.currentTarget) e.currentTarget.classList.remove('active');
+});
+
+// ── LOOKBOOK ─────────────────────────────────────────────────────────────────
+const LB_KEY = 'dw_motif_lookbook';
+function loadLookbook(){ return JSON.parse(localStorage.getItem(LB_KEY)||'[]'); }
+function saveLookbook(lb){ localStorage.setItem(LB_KEY, JSON.stringify(lb)); }
+
+function renderLookbook(){
+  const lb = loadLookbook();
+  const strip = document.getElementById('lb-strip');
+  if(!lb.length){
+    strip.innerHTML='<span class="lb-empty">Your saved combinations will appear here</span>';
+    return;
+  }
+  strip.innerHTML = lb.map((entry,i)=>{
+    const m=MOTIFS.find(x=>x.name===entry.m)||MOTIFS[0];
+    const c=COLORWAYS.find(x=>x.name===entry.c)||COLORWAYS[0];
+    const f=FINISHES.find(x=>x.name===entry.f)||FINISHES[0];
+    const inner=buildSwatchSVG(m,c,f);
+    // Replace opening <svg ...> tag attrs for thumbnail sizing
+    const thumb=inner.replace(/<svg([^>]*)>/,
+      '<svg viewBox="0 0 200 200" width="90" height="72" xmlns="http://www.w3.org/2000/svg" class="lb-card-swatch">');
+    return `<div class="lb-card" title="${entry.name}" data-i="${i}">${thumb}<div class="lb-card-label">${entry.name}</div></div>`;
+  }).join('');
+}
+
+document.getElementById('save-btn').addEventListener('click',()=>{
+  if(!currentCombo) return;
+  const {m,c,f}=currentCombo;
+  const name=`${f.name} ${c.name} ${m.name}`;
+  const lb=loadLookbook();
+  if(lb.length>=20){toast('Lookbook full (20 max) — clear some first');return;}
+  if(lb.find(e=>e.name===name)){toast('Already in lookbook');return;}
+  lb.push({m:m.name,c:c.name,f:f.name,name});
+  saveLookbook(lb);
+  renderLookbook();
+  toast('✦ Saved to Lookbook');
+  const btn=document.getElementById('save-btn');
+  btn.textContent='✦ Saved!';
+  setTimeout(()=>btn.textContent='★ Save to Lookbook',1800);
+});
+
+document.getElementById('clear-lb-btn').addEventListener('click',()=>{
+  saveLookbook([]);renderLookbook();toast('Lookbook cleared');
+});
+
+// ── DOWNLOAD ──────────────────────────────────────────────────────────────────
+document.getElementById('dl-btn').addEventListener('click',()=>{
+  if(!currentCombo) return;
+  const {m,c,f}=currentCombo;
+  const svgStr=buildSwatchSVG(m,c,f);
+  const blob=new Blob([svgStr],{type:'image/svg+xml'});
+  const url=URL.createObjectURL(blob);
+  const a=document.createElement('a');
+  a.href=url;a.download=`DW-${f.name}-${c.name}-${m.name}.svg`.replace(/\s+/g,'-');
+  a.click();
+  URL.revokeObjectURL(url);
+  toast('Swatch downloaded');
+});
+
+// ── TOAST ─────────────────────────────────────────────────────────────────────
+let toastTimer;
+function toast(msg){
+  const el=document.getElementById('toast');
+  el.textContent=msg;el.classList.add('show');
+  clearTimeout(toastTimer);
+  toastTimer=setTimeout(()=>el.classList.remove('show'),2400);
+}
+
+// ── MUTE ─────────────────────────────────────────────────────────────────────
+document.getElementById('mute-btn').addEventListener('click',()=>{
+  muted=!muted;
+  document.getElementById('mute-btn').textContent=muted?'♩':'♪';
+  toast(muted?'Sound off':'Sound on');
+});
+
+// ── LOCK BUTTONS ─────────────────────────────────────────────────────────────
+[0,1,2].forEach(ri=>{
+  document.getElementById(`lock-${ri}`).addEventListener('click',()=>{
+    locked[ri]=!locked[ri];
+    const btn=document.getElementById(`lock-${ri}`);
+    btn.classList.toggle('locked',locked[ri]);
+    btn.setAttribute('aria-pressed',locked[ri]);
+    btn.textContent=locked[ri]?'Locked':'Lock';
+  });
+});
+
+// ── SPIN BUTTON + KEYBOARD ────────────────────────────────────────────────────
+document.getElementById('spin-btn').addEventListener('click',spinReels);
+document.addEventListener('keydown',e=>{
+  if(e.code==='Space'&&e.target===document.body){e.preventDefault();spinReels();}
+  if(e.code==='Escape') document.getElementById('jackpot-overlay').classList.remove('active');
+});
+
+// ── INIT ──────────────────────────────────────────────────────────────────────
+[0,1,2].forEach(ri=>{
+  currentIdx[ri]=Math.floor(Math.random()*reelData[ri].length);
+  buildTrack(ri);
+});
+renderLookbook();
+</script>
+</body>
+</html>

← 6caed3a Agent-driven tool: DW Colorway Studio (frontend-developer) —  ·  back to Model Arena  ·  Agent-driven tool: DW Pairing Studio (frontend-developer) — cc77e59 →