[object Object]

← back to Model Arena

Agent-driven game: DW Swatch Memory (frontend-developer) — 32 inline-SVG textures, 3 difficulties, 3D flips, inert win overlay. Registered.

dd560003ce5a77ae8ca3a7db08673df1f3703e67 · 2026-07-25 11:32:17 -0700 · Steve Abrams

Files touched

Diff

commit dd560003ce5a77ae8ca3a7db08673df1f3703e67
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Jul 25 11:32:17 2026 -0700

    Agent-driven game: DW Swatch Memory (frontend-developer) — 32 inline-SVG textures, 3 difficulties, 3D flips, inert win overlay. Registered.
---
 data/arcade-games.json             |    6 +
 public/games/dw-shop-by-color.html |  774 ++++++++++++++++++++++++++
 public/games/dw-swatch-memory.html | 1066 ++++++++++++++++++++++++++++++++++++
 3 files changed, 1846 insertions(+)

diff --git a/data/arcade-games.json b/data/arcade-games.json
index f6867c4..f620ff7 100644
--- a/data/arcade-games.json
+++ b/data/arcade-games.json
@@ -35,6 +35,12 @@
       "title": "DW Find Your Style",
       "credit": "frontend-developer",
       "note": "Agent-built \u2014 interactive interior-style quiz (6 visual questions \u2192 5 archetypes \u2192 shareable result card)"
+    },
+    {
+      "slug": "dw-swatch-memory",
+      "title": "DW Swatch Memory",
+      "credit": "frontend-developer",
+      "note": "Agent-built game \u2014 memory pairs with 32 luxury wallcovering textures, 3 difficulties, 3D flips"
     }
   ]
 }
\ No newline at end of file
diff --git a/public/games/dw-shop-by-color.html b/public/games/dw-shop-by-color.html
new file mode 100644
index 0000000..715a83d
--- /dev/null
+++ b/public/games/dw-shop-by-color.html
@@ -0,0 +1,774 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Shop by Color — Designer Wallcoverings</title>
+<style>
+:root{
+  --bg:#f4f1ea;
+  --bg2:#edeae1;
+  --ink:#1c1a17;
+  --ink2:#3d3a34;
+  --ink3:#7a7570;
+  --accent:#c9a961;
+  --accent2:#b8933f;
+  --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:12px;
+  --panel-w:420px;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+html,body{height:100%;background:var(--bg);color:var(--ink);font-family:var(--sans);overflow-x:hidden}
+
+/* ── HEADER ── */
+header{
+  position:fixed;top:0;left:0;right:0;z-index:100;
+  display:flex;align-items:center;justify-content:space-between;
+  padding:18px 36px 14px;
+  background:var(--bg);
+  border-bottom:1px solid rgba(28,26,23,.12);
+}
+.wordmark{
+  font-family:var(--serif);
+  font-size:clamp(22px,2.8vw,42px);
+  font-weight:400;
+  letter-spacing:.18em;
+  text-transform:uppercase;
+  color:var(--ink);
+  line-height:1;
+}
+.wordmark span{color:var(--accent);font-weight:600}
+.tagline{
+  font-family:var(--serif);
+  font-size:13px;letter-spacing:.12em;
+  text-transform:uppercase;color:var(--ink3);
+}
+
+/* ── LAYOUT ── */
+main{
+  display:flex;flex-direction:column;align-items:center;
+  padding:90px 20px 60px;
+  min-height:100vh;
+}
+.page-title{
+  font-family:var(--serif);font-size:clamp(28px,4vw,54px);
+  font-weight:300;letter-spacing:.06em;color:var(--ink);
+  text-align:center;margin-bottom:6px;margin-top:10px;
+}
+.page-sub{
+  font-size:13px;letter-spacing:.1em;text-transform:uppercase;
+  color:var(--ink3);text-align:center;margin-bottom:36px;
+}
+
+/* ── TONE TOGGLE ── */
+.tone-row{
+  display:flex;gap:8px;margin-bottom:40px;
+  background:var(--bg2);border-radius:100px;padding:4px;
+}
+.tone-btn{
+  padding:7px 24px;border-radius:100px;border:none;background:transparent;
+  font-family:var(--serif);font-size:15px;letter-spacing:.08em;
+  color:var(--ink2);cursor:pointer;transition:all .25s var(--ease);
+}
+.tone-btn.active{background:var(--accent);color:#fff;}
+.tone-btn:hover:not(.active){background:rgba(201,169,97,.15);}
+
+/* ── WHEEL SECTION ── */
+.wheel-section{
+  display:flex;flex-direction:column;align-items:center;
+  position:relative;width:100%;max-width:700px;
+}
+#wheel-canvas{
+  cursor:pointer;filter:drop-shadow(0 8px 32px rgba(28,26,23,.18));
+  transition:filter .3s var(--ease);
+  touch-action:none;
+}
+#wheel-canvas:hover{filter:drop-shadow(0 12px 48px rgba(28,26,23,.28));}
+
+.hue-label{
+  margin-top:28px;text-align:center;
+  font-family:var(--serif);font-size:clamp(26px,3vw,42px);
+  font-weight:300;letter-spacing:.1em;color:var(--ink);
+  min-height:52px;
+  transition:opacity .3s var(--ease);
+}
+.hue-label small{
+  display:block;font-size:13px;letter-spacing:.14em;
+  text-transform:uppercase;color:var(--ink3);margin-top:4px;
+}
+
+/* ── VALUE SLIDER ── */
+.value-row{
+  display:flex;align-items:center;gap:16px;margin:32px 0 40px;
+  width:100%;max-width:420px;
+}
+.value-label{font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink3);white-space:nowrap;}
+input[type=range]{
+  flex:1;appearance:none;height:3px;background:linear-gradient(to right,#e8e2d6,var(--ink2));
+  border-radius:2px;outline:none;cursor:pointer;
+}
+input[type=range]::-webkit-slider-thumb{
+  appearance:none;width:18px;height:18px;border-radius:50%;
+  background:var(--accent);border:2px solid var(--bg);
+  box-shadow:0 2px 8px rgba(0,0,0,.2);cursor:grab;
+}
+input[type=range]::-webkit-slider-thumb:active{cursor:grabbing;}
+
+/* ── SWATCH GRID ── */
+.swatch-section{
+  width:100%;max-width:740px;
+}
+.swatch-grid{
+  display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));
+  gap:20px;
+  transition:opacity .35s var(--ease),transform .35s var(--ease);
+}
+.swatch-grid.hidden{opacity:0;transform:translateY(12px);pointer-events:none;}
+
+.swatch-card{
+  background:#fff;border-radius:var(--r);
+  overflow:hidden;cursor:pointer;
+  box-shadow:0 2px 12px rgba(28,26,23,.08);
+  transition:transform .25s var(--ease),box-shadow .25s var(--ease);
+}
+.swatch-card:hover{transform:translateY(-5px);box-shadow:0 12px 40px rgba(28,26,23,.18);}
+.swatch-card:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
+
+.swatch-art{width:100%;aspect-ratio:4/3;display:block;}
+.swatch-info{padding:14px 16px 16px;}
+.swatch-pattern{
+  font-family:var(--serif);font-size:17px;font-weight:500;
+  color:var(--ink);letter-spacing:.03em;line-height:1.2;
+}
+.swatch-colorway{
+  font-size:11px;letter-spacing:.12em;text-transform:uppercase;
+  color:var(--ink3);margin-top:4px;
+}
+
+/* ── DETAIL PANEL ── */
+#detail-panel{
+  position:fixed;top:0;right:0;bottom:0;width:var(--panel-w);
+  background:var(--bg);border-left:1px solid rgba(28,26,23,.1);
+  z-index:200;display:flex;flex-direction:column;
+  transform:translateX(100%);transition:transform .4s var(--ease);
+  visibility:hidden;
+  overflow-y:auto;
+}
+#detail-panel.open{transform:translateX(0);visibility:visible;}
+.panel-close{
+  position:absolute;top:20px;right:20px;
+  background:transparent;border:1px solid rgba(28,26,23,.2);
+  border-radius:50%;width:36px;height:36px;cursor:pointer;
+  font-size:18px;color:var(--ink2);display:flex;align-items:center;justify-content:center;
+  transition:background .2s;
+}
+.panel-close:hover{background:rgba(28,26,23,.08);}
+.panel-close:focus-visible{outline:2px solid var(--accent);}
+.panel-art{width:100%;aspect-ratio:4/3;display:block;}
+.panel-body{padding:28px 28px 40px;}
+.panel-colorway{
+  font-size:11px;letter-spacing:.16em;text-transform:uppercase;
+  color:var(--accent);margin-bottom:8px;
+}
+.panel-name{
+  font-family:var(--serif);font-size:32px;font-weight:400;
+  color:var(--ink);line-height:1.1;margin-bottom:12px;
+}
+.panel-evocative{
+  font-family:var(--serif);font-size:16px;font-style:italic;
+  color:var(--ink2);line-height:1.6;margin-bottom:24px;
+  border-left:2px solid var(--accent);padding-left:14px;
+}
+.panel-section-title{
+  font-size:10px;letter-spacing:.18em;text-transform:uppercase;
+  color:var(--ink3);margin-bottom:12px;
+}
+.pairs-row{display:flex;gap:12px;}
+.pair-chip{
+  display:flex;align-items:center;gap:8px;
+  background:var(--bg2);border-radius:8px;padding:8px 12px;flex:1;
+}
+.pair-dot{width:24px;height:24px;border-radius:50%;flex-shrink:0;}
+.pair-name{font-size:12px;color:var(--ink2);letter-spacing:.04em;}
+
+.panel-overlay{
+  position:fixed;inset:0;background:rgba(28,26,23,.35);z-index:190;
+  opacity:0;pointer-events:none;transition:opacity .4s var(--ease);
+  visibility:hidden;
+}
+.panel-overlay.open{opacity:1;pointer-events:auto;visibility:visible;}
+
+/* ── RESPONSIVE ── */
+@media(max-width:600px){
+  header{padding:14px 18px 12px;}
+  .tagline{display:none;}
+  :root{--panel-w:100vw;}
+  #detail-panel{top:auto;bottom:0;height:85vh;transform:translateY(100%);border-left:none;border-top:1px solid rgba(28,26,23,.1);}
+  #detail-panel.open{transform:translateY(0);}
+}
+@media(max-width:480px){
+  .swatch-grid{grid-template-columns:repeat(2,1fr);gap:12px;}
+}
+</style>
+</head>
+<body>
+
+<header>
+  <div class="wordmark">Designer <span>W</span>allcoverings</div>
+  <div class="tagline">Shop by Color</div>
+</header>
+
+<main>
+  <h1 class="page-title">Discover by Color</h1>
+  <p class="page-sub">Turn the wheel · Select a hue · Find your wallcovering</p>
+
+  <div class="tone-row" role="group" aria-label="Color tone filter">
+    <button class="tone-btn active" data-tone="warm" aria-pressed="true">Warm</button>
+    <button class="tone-btn" data-tone="cool" aria-pressed="false">Cool</button>
+    <button class="tone-btn" data-tone="neutral" aria-pressed="false">Neutral</button>
+  </div>
+
+  <div class="wheel-section">
+    <canvas id="wheel-canvas" width="440" height="440"
+            role="img" aria-label="Color wheel — click or drag to select a hue family"
+            tabindex="0"></canvas>
+    <div class="hue-label" id="hue-label" aria-live="polite">
+      <span id="hue-name">Terracotta</span>
+      <small id="hue-desc">Warm earthen clay</small>
+    </div>
+  </div>
+
+  <div class="value-row" role="group" aria-label="Color depth">
+    <span class="value-label">Light</span>
+    <input type="range" id="value-slider" min="0" max="100" value="50"
+           aria-label="Color depth from light to deep">
+    <span class="value-label">Deep</span>
+  </div>
+
+  <div class="swatch-section">
+    <div class="swatch-grid" id="swatch-grid" aria-live="polite"></div>
+  </div>
+</main>
+
+<div class="panel-overlay" id="panel-overlay" role="presentation"
+     aria-hidden="true" tabindex="-1"></div>
+
+<aside id="detail-panel" role="dialog" aria-modal="true"
+       aria-label="Swatch detail" aria-hidden="true">
+  <button class="panel-close" id="panel-close" aria-label="Close detail panel">✕</button>
+  <svg class="panel-art" id="panel-art" viewBox="0 0 420 315" xmlns="http://www.w3.org/2000/svg"></svg>
+  <div class="panel-body">
+    <div class="panel-colorway" id="panel-colorway"></div>
+    <div class="panel-name" id="panel-name"></div>
+    <div class="panel-evocative" id="panel-evocative"></div>
+    <div class="panel-section-title">Pairs beautifully with</div>
+    <div class="pairs-row" id="pairs-row"></div>
+  </div>
+</aside>
+
+<script>
+// ── DATA ──────────────────────────────────────────────────────────────────────
+const HUE_FAMILIES = {
+  warm:[
+    {id:'terracotta',name:'Terracotta',desc:'Warm earthen clay',h:18,s:.7,
+     swatches:['Baked Clay','Tuscan Sun','Adobe Dusk','Canyon Rose','Copper Earth','Sienna Bloom']},
+    {id:'blush',name:'Blush',desc:'Romantic rose petal',h:345,s:.45,
+     swatches:['Petal Drift','Rosewater Mist','Cameo Flush','Boudoir Pink','Blush Silk','Seashell Tint']},
+    {id:'brass',name:'Brass',desc:'Gilded warmth',h:42,s:.65,
+     swatches:['Gilded Age','Honey Lacquer','Antique Gold','Warm Ember','Ochre Field','Topaz Weave']},
+    {id:'terracina',name:'Ochre',desc:'Sun-drenched saffron',h:35,s:.75,
+     swatches:['Saffron Haze','Golden Hour','Tuscan Yellow','Amber Damask','Sun Stripe','Lemon Grove']},
+    {id:'sand',name:'Sand',desc:'Coastal dune warmth',h:38,s:.38,
+     swatches:['Dune Linen','Desert Oatmeal','Warm Parchment','Sahara Silk','Sandstone Weave','Ivory Drift']},
+  ],
+  cool:[
+    {id:'celadon',name:'Celadon',desc:'Ancient jade serenity',h:160,s:.38,
+     swatches:['Jade Mist','Pale Celadon','Sea Foam Silk','Verdant Haze','Mint Damask','Celadon Cloud']},
+    {id:'ink',name:'Ink',desc:'Midnight depth',h:230,s:.55,
+     swatches:['Midnight Linen','Navy Grasscloth','Indigo Damask','Prussian Silk','Deep Denim','Ink Stripe']},
+    {id:'slate',name:'Slate',desc:'Storm-cloud grey',h:210,s:.2,
+     swatches:['Pewter Chinoiserie','Ash Linen','Dove Geometric','Steel Damask','Fog Silk','Storm Weave']},
+    {id:'aqua',name:'Aqua',desc:'Mediterranean clarity',h:185,s:.5,
+     swatches:['Lagoon Silk','Teal Grasscloth','Aqua Damask','Sea Glass','Turquoise Weave','Coastal Blue']},
+    {id:'aubergine',name:'Aubergine',desc:'Deep violet luxury',h:280,s:.4,
+     swatches:['Plum Velvet','Iris Damask','Violet Linen','Mulberry Silk','Grape Chinoiserie','Dusk Weave']},
+  ],
+  neutral:[
+    {id:'oatmeal',name:'Oatmeal',desc:'Timeless quiet warmth',h:36,s:.18,
+     swatches:['Raw Linen','Cream Grasscloth','Parchment Damask','Bisque Silk','Flax Weave','Stone Chinoiserie']},
+    {id:'mushroom',name:'Mushroom',desc:'Grounded earthy tone',h:25,s:.18,
+     swatches:['Truffle Linen','Mocha Grasscloth','Taupe Damask','Cocoa Silk','Clay Weave','Fawn Chinoiserie']},
+    {id:'sage',name:'Sage',desc:'Muted botanical calm',h:100,s:.22,
+     swatches:['Herb Garden','Sage Linen','Eucalyptus Silk','Lichen Damask','Grey-Green Weave','Muted Fern']},
+    {id:'charcoal',name:'Charcoal',desc:'Refined graphite depth',h:220,s:.08,
+     swatches:['Graphite Linen','Iron Grasscloth','Carbon Silk','Slate Damask','Obsidian Weave','Smoke Chinoiserie']},
+    {id:'cream',name:'Ivory',desc:'Pure luminous white',h:45,s:.12,
+     swatches:['Alabaster Silk','Snow Linen','Pearl Damask','Milk Grasscloth','Champagne Weave','Egret Chinoiserie']},
+  ],
+};
+
+// Complementary pairs lookup
+const PAIR_MAP = {
+  terracotta:[{name:'Sage',h:100,s:.22,l:.55},{name:'Ivory',h:45,s:.12,l:.88}],
+  blush:[{name:'Celadon',h:160,s:.38,l:.65},{name:'Charcoal',h:220,s:.08,l:.35}],
+  brass:[{name:'Ink',h:230,s:.55,l:.3},{name:'Oatmeal',h:36,s:.18,l:.82}],
+  terracina:[{name:'Slate',h:210,s:.2,l:.5},{name:'Ivory',h:45,s:.12,l:.9}],
+  sand:[{name:'Aubergine',h:280,s:.4,l:.4},{name:'Sage',h:100,s:.22,l:.55}],
+  celadon:[{name:'Blush',h:345,s:.45,l:.75},{name:'Brass',h:42,s:.65,l:.55}],
+  ink:[{name:'Brass',h:42,s:.65,l:.6},{name:'Oatmeal',h:36,s:.18,l:.84}],
+  slate:[{name:'Blush',h:345,s:.45,l:.8},{name:'Sage',h:100,s:.22,l:.58}],
+  aqua:[{name:'Terracotta',h:18,s:.7,l:.55},{name:'Sand',h:38,s:.38,l:.78}],
+  aubergine:[{name:'Brass',h:42,s:.65,l:.6},{name:'Celadon',h:160,s:.38,l:.65}],
+  oatmeal:[{name:'Ink',h:230,s:.55,l:.3},{name:'Sage',h:100,s:.22,l:.55}],
+  mushroom:[{name:'Brass',h:42,s:.65,l:.6},{name:'Celadon',h:160,s:.38,l:.65}],
+  sage:[{name:'Blush',h:345,s:.45,l:.8},{name:'Ivory',h:45,s:.12,l:.9}],
+  charcoal:[{name:'Blush',h:345,s:.45,l:.78},{name:'Brass',h:42,s:.65,l:.6}],
+  cream:[{name:'Ink',h:230,s:.55,l:.28},{name:'Sage',h:100,s:.22,l:.52}],
+};
+
+const EVOCATIVE = {
+  terracotta:"A sun-baked memory of Tuscan afternoons — warmth you can almost feel underfoot.",
+  blush:"Softer than candlelight, this hue wraps a room in the quiet romance of a garden at dusk.",
+  brass:"The gleam of antique objects, of well-traveled luggage, of rooms that know their own worth.",
+  terracina:"Where Moroccan souks meet Florentine piazzas — a saffron warmth that awakens any interior.",
+  sand:"The unhurried calm of a dune at low tide, luminous without demanding attention.",
+  celadon:"Ancient kilns and scholar's gardens distilled into a single, irreplaceable tone.",
+  ink:"A room dressed in midnight: sophisticated, commanding, inviting the stars indoors.",
+  slate:"Neither grey nor blue, it is the color of a winter sea viewed from warm shelter.",
+  aqua:"The Mediterranean in August — clarity, depth, and effortless luxury in equal measure.",
+  aubergine:"The most complex of purples, where wine meets shadow and sophistication has no need to shout.",
+  oatmeal:"The architecture of quiet: rooms that breathe, textiles that invite touch, light that lingers.",
+  mushroom:"Earth's own neutral — the color of fallen leaves, of damp forest paths, of patient grace.",
+  sage:"The herb garden's whisper: muted, botanical, a green that feels as timeless as stone.",
+  charcoal:"Not merely dark but precise — the confident line of a draughtsman's pen on vellum.",
+  cream:"Light made material. The tone that amplifies every other colour it accompanies.",
+};
+
+// ── TEXTURE RENDERERS (inline SVG) ───────────────────────────────────────────
+function hsl(h,s,l){return `hsl(${h},${Math.round(s*100)}%,${Math.round(l*100)}%)`}
+
+const TEXTURE_NAMES = ['grasscloth','damask','silk-stripe','chinoiserie','geometric','linen'];
+
+function renderGrasscloth(h,s,l,w,ht){
+  const c1=hsl(h,s,l), c2=hsl(h,s*.9,l*.9), c3=hsl(h,s*.7,l*1.08);
+  let lines='';
+  for(let i=0;i<ht;i+=4){
+    const y=i+Math.sin(i*.3)*.8;
+    lines+=`<line x1="0" y1="${y}" x2="${w}" y2="${y+Math.sin(i*.2)*.5}"
+              stroke="${i%8<4?c2:c3}" stroke-width="${i%12<6?1.2:.7}" opacity="${.5+Math.sin(i*.5)*.2}"/>`;
+  }
+  let weave='';
+  for(let x=0;x<w;x+=8){
+    weave+=`<line x1="${x}" y1="0" x2="${x+Math.sin(x*.15)*3}" y2="${ht}"
+              stroke="${c2}" stroke-width=".5" opacity=".25"/>`;
+  }
+  return `<rect width="${w}" height="${ht}" fill="${c1}"/>${weave}${lines}`;
+}
+
+function renderDamask(h,s,l,w,ht){
+  const c1=hsl(h,s,l), c2=hsl(h,s*.8,l*.82), c3=hsl(h,s*.6,l*1.1);
+  const cx=w/2,cy=ht/2,r=Math.min(w,ht)*.3;
+  return `<rect width="${w}" height="${ht}" fill="${c1}"/>
+    <g opacity=".18" fill="${c2}">
+      ${[0,w/2,w].map(ox=>[0,ht/2,ht].map(oy=>
+        `<circle cx="${ox}" cy="${oy}" r="${r*.55}"/>`).join('')).join('')}
+    </g>
+    <g fill="none" stroke="${c2}" stroke-width="1" opacity=".45">
+      <path d="M${cx} ${cy-r} C${cx+r*.6} ${cy-r*.6} ${cx+r} ${cy} ${cx+r*.6} ${cy+r*.6} S${cx} ${cy+r} ${cx-r*.6} ${cy+r*.6} ${cx-r} ${cy} ${cx-r*.6} ${cy-r*.6} Z"/>
+      <path d="M${cx} ${cy-r*.55} C${cx+r*.3} ${cy-r*.3} ${cx+r*.55} ${cy} ${cx} ${cy+r*.55} ${cx-r*.55} ${cy} Z" opacity=".7"/>
+      <ellipse cx="${cx}" cy="${cy}" rx="${r*.18}" ry="${r*.25}"/>
+      <ellipse cx="${cx}" cy="${cy}" rx="${r*.32}" ry="${r*.1}" transform="rotate(90 ${cx} ${cy})"/>
+    </g>
+    <g fill="${c3}" opacity=".22">
+      ${[[cx-r*.55,cy],[cx+r*.55,cy],[cx,cy-r*.55],[cx,cy+r*.55]].map(([x,y])=>
+        `<ellipse cx="${x}" cy="${y}" rx="${r*.1}" ry="${r*.17}"/>`).join('')}
+    </g>`;
+}
+
+function renderSilkStripe(h,s,l,w,ht){
+  const bands=[0,.14,.28,.36,.46,.6,.72,.82,1];
+  let strips='';
+  bands.forEach((b,i)=>{
+    if(i===bands.length-1)return;
+    const x=b*w, ww=(bands[i+1]-b)*w;
+    const ll=i%2===0?l:l*.88;
+    const ss=i%3===0?s:s*.7;
+    strips+=`<rect x="${x}" y="0" width="${ww}" height="${ht}" fill="${hsl(h,ss,ll)}"/>`;
+  });
+  let shine='';
+  for(let x=0;x<w;x+=24){
+    shine+=`<line x1="${x+2}" y1="0" x2="${x+2}" y2="${ht}" stroke="white" stroke-width="1" opacity=".12"/>`;
+  }
+  return `${strips}${shine}`;
+}
+
+function renderChinoiserie(h,s,l,w,ht){
+  const bg=hsl(h,s*.5,l*1.08), ink=hsl(h,s,l*.62);
+  const cx=w/2,cy=ht/2;
+  const branch=`M${cx-60} ${cy+50} Q${cx-30} ${cy} ${cx} ${cy-20} T${cx+60} ${cy-60}`;
+  const leaves=[[cx-20,cy+10],[cx+10,cy-5],[cx+40,cy-35],[cx-5,cy-18]].map(([x,y])=>
+    `<ellipse cx="${x}" cy="${y}" rx="10" ry="6" transform="rotate(${-30+x*.2} ${x} ${y})" fill="${ink}" opacity=".55"/>`).join('');
+  const blossoms=[[cx+5,cy-22],[cx+42,cy-50],[cx-22,cy-5]].map(([x,y])=>
+    `<circle cx="${x}" cy="${y}" r="5" fill="${hsl(h,s*.6,l*1.12)}" opacity=".7"/>
+     <circle cx="${x}" cy="${y}" r="2" fill="${ink}" opacity=".5"/>`).join('');
+  const bird=`<path d="M${cx+30} ${cy+30} Q${cx+50} ${cy+20} ${cx+65} ${cy+28} Q${cx+52} ${cy+32} ${cx+60} ${cy+42} Q${cx+42} ${cy+36} ${cx+30} ${cy+30}Z"
+    fill="${ink}" opacity=".5"/>
+    <circle cx="${cx+62}" cy="${cy+28}" r="3" fill="${ink}" opacity=".55"/>`;
+  return `<rect width="${w}" height="${ht}" fill="${bg}"/>
+    <path d="${branch}" stroke="${ink}" stroke-width="2.5" fill="none" opacity=".6" stroke-linecap="round"/>
+    ${leaves}${blossoms}${bird}`;
+}
+
+function renderGeometric(h,s,l,w,ht){
+  const c1=hsl(h,s,l), c2=hsl(h,s*.75,l*.84), c3=hsl(h,s*.5,l*1.1);
+  const sz=28;
+  let tiles='';
+  for(let row=0;row*sz<ht+sz;row++){
+    for(let col=0;col*sz<w+sz;col++){
+      const x=col*sz+(row%2)*sz*.5, y=row*sz*.87;
+      const type=(row+col)%3;
+      if(type===0){
+        tiles+=`<polygon points="${x},${y+sz*.87} ${x+sz*.5},${y} ${x+sz},${y+sz*.87}"
+          fill="${c2}" stroke="${c3}" stroke-width=".5"/>`;
+      } else if(type===1){
+        tiles+=`<rect x="${x+2}" y="${y+2}" width="${sz-4}" height="${sz*.75-4}" rx="2"
+          fill="${c1}" stroke="${c2}" stroke-width=".8"/>`;
+      } else {
+        tiles+=`<polygon points="${x},${y} ${x+sz},${y} ${x+sz*.75},${y+sz*.87} ${x+sz*.25},${y+sz*.87}"
+          fill="${c3}" stroke="${c1}" stroke-width=".5" opacity=".85"/>`;
+      }
+    }
+  }
+  return `<rect width="${w}" height="${ht}" fill="${c1}"/>${tiles}`;
+}
+
+function renderLinen(h,s,l,w,ht){
+  const c1=hsl(h,s*.6,l*1.04), c2=hsl(h,s*.4,l*.92);
+  let warp='',weft='';
+  for(let y=0;y<ht;y+=3){
+    weft+=`<line x1="0" y1="${y+.5}" x2="${w}" y2="${y+Math.sin(y*.8)*.4}"
+      stroke="${c2}" stroke-width="${y%6<3?1.1:.6}" opacity="${.3+Math.sin(y*.3)*.15}"/>`;
+  }
+  for(let x=0;x<w;x+=4){
+    warp+=`<line x1="${x+.5}" y1="0" x2="${x+Math.sin(x*.5)*.3}" y2="${ht}"
+      stroke="${c2}" stroke-width=".4" opacity=".2"/>`;
+  }
+  return `<rect width="${w}" height="${ht}" fill="${c1}"/>${warp}${weft}`;
+}
+
+const RENDERERS=[renderGrasscloth,renderDamask,renderSilkStripe,renderChinoiserie,renderGeometric,renderLinen];
+
+function makeSVGContent(texIdx,h,s,l,w,ht){
+  return RENDERERS[texIdx%RENDERERS.length](h,s,l,w,ht);
+}
+
+// ── STATE ─────────────────────────────────────────────────────────────────────
+let tone='warm';
+let activeFamily=null;
+let depth=0.5; // 0=light,1=deep
+let selectedSwatch=null;
+
+// ── WHEEL ─────────────────────────────────────────────────────────────────────
+const canvas=document.getElementById('wheel-canvas');
+const ctx=canvas.getContext('2d');
+const W=canvas.width, H=canvas.height;
+const CX=W/2, CY=H/2;
+const R_OUTER=W*.46, R_INNER=W*.22, R_INDICATOR=R_OUTER+10;
+let hoveredAngle=null, selectedAngle=null;
+
+function familiesByTone(){return HUE_FAMILIES[tone]||[];}
+
+function angleForFamily(idx,total){
+  return (idx/total)*Math.PI*2 - Math.PI/2;
+}
+
+function drawWheel(){
+  ctx.clearRect(0,0,W,H);
+  const families=familiesByTone();
+  const n=families.length;
+  const segSpan=(Math.PI*2)/n;
+
+  families.forEach((fam,i)=>{
+    const aStart=angleForFamily(i,n)-segSpan/2;
+    const aEnd=aStart+segSpan;
+    const aMid=(aStart+aEnd)/2;
+    const isActive=activeFamily&&activeFamily.id===fam.id;
+    const isHovered=hoveredAngle!==null&&segForAngle(hoveredAngle,n)===i&&!isActive;
+
+    // draw segment
+    const expandR=isActive?6:isHovered?3:0;
+    for(let layer=0;layer<3;layer++){
+      const lR_i=R_INNER+(R_OUTER-R_INNER)*(layer/3);
+      const lR_o=R_INNER+(R_OUTER-R_INNER)*((layer+1)/3)+expandR*(layer*.4);
+      const lL=.82-(layer*.12);
+      const lS=fam.s*(1+layer*.15);
+      ctx.beginPath();
+      ctx.moveTo(CX,CY);
+      ctx.arc(CX,CY,lR_o,aStart+.015,aEnd-.015);
+      ctx.arc(CX,CY,lR_i,aEnd-.015,aStart+.015,true);
+      ctx.closePath();
+      ctx.fillStyle=hsl(fam.h,lS,lL-(depth*.25));
+      ctx.fill();
+    }
+
+    // segment border
+    ctx.beginPath();
+    ctx.moveTo(CX,CY);
+    ctx.arc(CX,CY,R_OUTER+expandR,aStart,aEnd);
+    ctx.arc(CX,CY,R_INNER,aEnd,aStart,true);
+    ctx.closePath();
+    ctx.strokeStyle='rgba(244,241,234,.6)';
+    ctx.lineWidth=isActive?2:1;
+    ctx.stroke();
+
+    // active ring
+    if(isActive){
+      ctx.beginPath();
+      ctx.arc(CX,CY,R_OUTER+expandR+5,aStart+.02,aEnd-.02);
+      ctx.strokeStyle=hsl(fam.h,fam.s*.9,.45);
+      ctx.lineWidth=2.5;
+      ctx.stroke();
+    }
+
+    // hue label tick
+    const tickR=R_OUTER+expandR+20;
+    const tx=CX+Math.cos(aMid)*tickR, ty=CY+Math.sin(aMid)*tickR;
+    ctx.font=`${isActive?600:400} ${isActive?11:10}px -apple-system,sans-serif`;
+    ctx.fillStyle=isActive?hsl(fam.h,fam.s,.38):'rgba(28,26,23,.55)';
+    ctx.textAlign='center';
+    ctx.textBaseline='middle';
+    ctx.fillText(fam.name.toUpperCase(),tx,ty);
+  });
+
+  // center disc
+  const grad=ctx.createRadialGradient(CX,CY,2,CX,CY,R_INNER);
+  grad.addColorStop(0,'#f4f1ea');
+  grad.addColorStop(1,'#edeae1');
+  ctx.beginPath();
+  ctx.arc(CX,CY,R_INNER,0,Math.PI*2);
+  ctx.fillStyle=grad;
+  ctx.fill();
+  ctx.strokeStyle='rgba(28,26,23,.08)';
+  ctx.lineWidth=1;
+  ctx.stroke();
+
+  // center icon
+  if(activeFamily){
+    ctx.font=`300 ${13}px var(--serif,'Cormorant Garamond',Georgia,serif)`;
+    ctx.fillStyle='rgba(28,26,23,.55)';
+    ctx.textAlign='center';
+    ctx.textBaseline='middle';
+    const words=activeFamily.name.split(' ');
+    words.forEach((w,wi)=>{
+      ctx.fillText(w,CX,CY+(wi-(words.length-1)/2)*16);
+    });
+  } else {
+    ctx.font='12px -apple-system,sans-serif';
+    ctx.fillStyle='rgba(28,26,23,.38)';
+    ctx.textAlign='center';
+    ctx.textBaseline='middle';
+    ctx.fillText('Click a hue',CX,CY);
+  }
+}
+
+function segForAngle(a,n){
+  const segSpan=Math.PI*2/n;
+  const norm=((a%(Math.PI*2))+(Math.PI*2))%(Math.PI*2);
+  // offset by -PI/2 (top start)
+  const shifted=((norm+Math.PI/2)%(Math.PI*2));
+  return Math.floor(shifted/segSpan)%n;
+}
+
+function hitTest(ex,ey){
+  const dx=ex-CX, dy=ey-CY;
+  const dist=Math.sqrt(dx*dx+dy*dy);
+  if(dist<R_INNER||dist>R_OUTER+20)return null;
+  const angle=Math.atan2(dy,dx);
+  return angle;
+}
+
+function selectAngle(a){
+  if(a===null)return;
+  const families=familiesByTone();
+  const idx=segForAngle(a,families.length);
+  const fam=families[idx];
+  if(fam){
+    activeFamily=fam;
+    selectedAngle=a;
+    document.getElementById('hue-name').textContent=fam.name;
+    document.getElementById('hue-desc').textContent=fam.desc;
+    renderSwatches(fam);
+    drawWheel();
+  }
+}
+
+// canvas events
+canvas.addEventListener('mousemove',e=>{
+  const r=canvas.getBoundingClientRect();
+  const sx=canvas.width/r.width, sy=canvas.height/r.height;
+  const a=hitTest((e.clientX-r.left)*sx,(e.clientY-r.top)*sy);
+  hoveredAngle=a;
+  canvas.style.cursor=a!==null?'pointer':'default';
+  drawWheel();
+});
+canvas.addEventListener('mouseleave',()=>{hoveredAngle=null;drawWheel();});
+canvas.addEventListener('click',e=>{
+  const r=canvas.getBoundingClientRect();
+  const sx=canvas.width/r.width, sy=canvas.height/r.height;
+  const a=hitTest((e.clientX-r.left)*sx,(e.clientY-r.top)*sy);
+  selectAngle(a);
+});
+
+// touch
+let touching=false;
+canvas.addEventListener('touchstart',e=>{touching=true;e.preventDefault();},{passive:false});
+canvas.addEventListener('touchmove',e=>{
+  if(!touching)return;e.preventDefault();
+  const t=e.touches[0];
+  const r=canvas.getBoundingClientRect();
+  const sx=canvas.width/r.width, sy=canvas.height/r.height;
+  const a=hitTest((t.clientX-r.left)*sx,(t.clientY-r.top)*sy);
+  if(a!==null){hoveredAngle=a;selectAngle(a);}
+},{passive:false});
+canvas.addEventListener('touchend',()=>{touching=false;hoveredAngle=null;drawWheel();});
+
+// keyboard
+canvas.addEventListener('keydown',e=>{
+  const families=familiesByTone();
+  if(!families.length)return;
+  let curIdx=activeFamily?families.findIndex(f=>f.id===activeFamily.id):0;
+  if(e.key==='ArrowRight'||e.key==='ArrowDown'){
+    e.preventDefault();
+    const next=(curIdx+1)%families.length;
+    const aMid=angleForFamily(next,families.length);
+    selectAngle(aMid);
+  } else if(e.key==='ArrowLeft'||e.key==='ArrowUp'){
+    e.preventDefault();
+    const prev=(curIdx-1+families.length)%families.length;
+    const aMid=angleForFamily(prev,families.length);
+    selectAngle(aMid);
+  }
+});
+
+// ── SWATCHES ─────────────────────────────────────────────────────────────────
+function depthAdjL(baseL){
+  // depth 0=light(+.12), 0.5=base, 1=deep(-.18)
+  return Math.max(.12,Math.min(.92,baseL+((.5-depth)*.28)));
+}
+
+function renderSwatches(fam){
+  const grid=document.getElementById('swatch-grid');
+  grid.classList.add('hidden');
+  setTimeout(()=>{
+    grid.innerHTML='';
+    fam.swatches.forEach((name,i)=>{
+      const texIdx=i%RENDERERS.length;
+      const texName=TEXTURE_NAMES[texIdx];
+      const l=depthAdjL(.72-(i%3)*.06);
+      const svgInner=makeSVGContent(texIdx,fam.h,fam.s,l,240,180);
+      const colorway=texName.charAt(0).toUpperCase()+texName.slice(1)+' · '+fam.name;
+      const card=document.createElement('div');
+      card.className='swatch-card';
+      card.setAttribute('tabindex','0');
+      card.setAttribute('role','button');
+      card.setAttribute('aria-label',`${name} — ${colorway}`);
+      card.innerHTML=`
+        <svg class="swatch-art" viewBox="0 0 240 180" xmlns="http://www.w3.org/2000/svg"
+             aria-hidden="true">${svgInner}</svg>
+        <div class="swatch-info">
+          <div class="swatch-pattern">${name}</div>
+          <div class="swatch-colorway">${colorway}</div>
+        </div>`;
+      card.addEventListener('click',()=>openDetail(name,colorway,fam,texIdx,i));
+      card.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();openDetail(name,colorway,fam,texIdx,i);}});
+      grid.appendChild(card);
+    });
+    grid.classList.remove('hidden');
+  },180);
+}
+
+// ── DETAIL PANEL ─────────────────────────────────────────────────────────────
+function openDetail(name,colorway,fam,texIdx,swatchIdx){
+  const l=depthAdjL(.72-(swatchIdx%3)*.06);
+  const svgContent=makeSVGContent(texIdx,fam.h,fam.s,l,420,315);
+  document.getElementById('panel-art').innerHTML=svgContent;
+  document.getElementById('panel-colorway').textContent=colorway;
+  document.getElementById('panel-name').textContent=name;
+  document.getElementById('panel-evocative').textContent=EVOCATIVE[fam.id]||'A colour of rare distinction.';
+
+  const pairs=PAIR_MAP[fam.id]||[];
+  const pairsRow=document.getElementById('pairs-row');
+  pairsRow.innerHTML=pairs.map(p=>`
+    <div class="pair-chip">
+      <div class="pair-dot" style="background:${hsl(p.h,p.s,p.l??0.62)}"></div>
+      <span class="pair-name">${p.name}</span>
+    </div>`).join('');
+
+  const panel=document.getElementById('detail-panel');
+  const overlay=document.getElementById('panel-overlay');
+  panel.classList.add('open');
+  panel.removeAttribute('aria-hidden');
+  overlay.classList.add('open');
+  overlay.removeAttribute('aria-hidden');
+  document.getElementById('panel-close').focus();
+}
+
+function closeDetail(){
+  const panel=document.getElementById('detail-panel');
+  const overlay=document.getElementById('panel-overlay');
+  panel.classList.remove('open');
+  panel.setAttribute('aria-hidden','true');
+  overlay.classList.remove('open');
+  overlay.setAttribute('aria-hidden','true');
+}
+
+document.getElementById('panel-close').addEventListener('click',closeDetail);
+document.getElementById('panel-overlay').addEventListener('click',closeDetail);
+document.addEventListener('keydown',e=>{if(e.key==='Escape')closeDetail();});
+
+// ── TONE TOGGLE ───────────────────────────────────────────────────────────────
+document.querySelectorAll('.tone-btn').forEach(btn=>{
+  btn.addEventListener('click',()=>{
+    document.querySelectorAll('.tone-btn').forEach(b=>{b.classList.remove('active');b.setAttribute('aria-pressed','false');});
+    btn.classList.add('active');btn.setAttribute('aria-pressed','true');
+    tone=btn.dataset.tone;
+    activeFamily=null;
+    document.getElementById('hue-name').textContent='Select a hue';
+    document.getElementById('hue-desc').textContent='';
+    document.getElementById('swatch-grid').innerHTML='';
+    drawWheel();
+  });
+});
+
+// ── VALUE SLIDER ──────────────────────────────────────────────────────────────
+document.getElementById('value-slider').addEventListener('input',e=>{
+  depth=e.target.value/100;
+  if(activeFamily)renderSwatches(activeFamily);
+  drawWheel();
+});
+
+// ── RESPONSIVE CANVAS RESIZE ─────────────────────────────────────────────────
+function resizeCanvas(){
+  const maxW=Math.min(440,window.innerWidth-40);
+  canvas.style.width=maxW+'px';
+  canvas.style.height=maxW+'px';
+}
+
+// ── INIT ─────────────────────────────────────────────────────────────────────
+resizeCanvas();
+window.addEventListener('resize',resizeCanvas);
+
+// Select terracotta on load
+setTimeout(()=>{
+  const families=familiesByTone();
+  if(families.length){
+    const idx=0;
+    const aMid=angleForFamily(idx,families.length);
+    selectAngle(aMid);
+  }
+},80);
+</script>
+</body>
+</html>
diff --git a/public/games/dw-swatch-memory.html b/public/games/dw-swatch-memory.html
new file mode 100644
index 0000000..84cee0c
--- /dev/null
+++ b/public/games/dw-swatch-memory.html
@@ -0,0 +1,1066 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Swatch Memory — Designer Wallcoverings</title>
+<style>
+:root {
+  --bg: #f4f1ea;
+  --bg2: #ede9e0;
+  --ink: #1c1a17;
+  --ink2: #3a3630;
+  --accent: #c9a961;
+  --accent2: #a8874a;
+  --card-back: #0d0b09;
+  --serif: 'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif;
+  --sans: 'Gill Sans','Optima',Optima,Candara,sans-serif;
+  --ease: cubic-bezier(.4,0,.2,1);
+  --ease-flip: cubic-bezier(.45,0,.55,1);
+  --r: 6px;
+  --shadow: 0 2px 12px rgba(28,26,23,.13);
+  --gold-glow: 0 0 0 3px var(--accent), 0 0 22px rgba(201,169,97,.45);
+}
+*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
+html,body{height:100%;background:var(--bg);color:var(--ink);font-family:var(--sans);-webkit-font-smoothing:antialiased}
+body{display:flex;flex-direction:column;min-height:100vh;overflow-x:hidden}
+
+/* HEADER */
+header{
+  display:flex;align-items:center;justify-content:space-between;
+  padding:.9rem 1.5rem .8rem;
+  border-bottom:1px solid rgba(201,169,97,.3);
+  background:var(--bg);
+  position:sticky;top:0;z-index:50;
+}
+.wordmark{
+  font-family:var(--serif);
+  font-size:clamp(26px,4vw,44px);
+  font-weight:400;
+  letter-spacing:.18em;
+  text-transform:uppercase;
+  color:var(--ink);
+  line-height:1;
+  user-select:none;
+}
+.wordmark span{
+  display:block;
+  font-size:.38em;
+  letter-spacing:.32em;
+  color:var(--accent2);
+  font-style:italic;
+  margin-top:.15em;
+}
+.header-controls{display:flex;align-items:center;gap:.75rem}
+.mute-btn{
+  background:none;border:1px solid rgba(201,169,97,.5);
+  color:var(--ink2);border-radius:50%;width:34px;height:34px;
+  cursor:pointer;font-size:.95rem;display:flex;align-items:center;justify-content:center;
+  transition:border-color .2s,background .2s;
+}
+.mute-btn:hover{background:rgba(201,169,97,.12);border-color:var(--accent)}
+
+/* CONTROLS BAR */
+.controls{
+  display:flex;align-items:center;justify-content:center;gap:.6rem;
+  padding:.75rem 1.2rem;
+  flex-wrap:wrap;
+  background:var(--bg2);
+  border-bottom:1px solid rgba(28,26,23,.08);
+}
+.diff-btn{
+  font-family:var(--serif);font-size:.95rem;letter-spacing:.08em;
+  padding:.38rem 1.1rem;border:1px solid rgba(201,169,97,.45);
+  background:transparent;color:var(--ink2);border-radius:var(--r);
+  cursor:pointer;transition:all .2s var(--ease);
+}
+.diff-btn:hover{background:rgba(201,169,97,.12);border-color:var(--accent)}
+.diff-btn.active{background:var(--accent);color:var(--ink);border-color:var(--accent);font-weight:600}
+.sep{color:rgba(28,26,23,.2);font-size:.8rem}
+
+/* HUD */
+.hud{
+  display:flex;align-items:center;justify-content:center;gap:2rem;
+  padding:.55rem 1.2rem;
+  font-size:.82rem;letter-spacing:.06em;color:var(--ink2);
+  text-transform:uppercase;
+  flex-wrap:wrap;
+}
+.hud-stat{display:flex;align-items:center;gap:.4rem}
+.hud-label{opacity:.6;font-size:.72rem}
+.hud-val{font-family:var(--serif);font-size:1.15rem;color:var(--ink);min-width:2.5ch;text-align:center}
+.hud-best{font-size:.72rem;color:var(--accent2);letter-spacing:.05em;margin-top:.1rem;text-align:center;opacity:.85}
+
+/* GAME AREA */
+.game-area{flex:1;display:flex;align-items:flex-start;justify-content:center;padding:1.5rem 1rem 2rem}
+.grid{
+  display:grid;
+  gap:clamp(6px,1.2vw,12px);
+  width:100%;
+  max-width:700px;
+}
+.grid.g4{grid-template-columns:repeat(4,1fr)}
+.grid.g6{grid-template-columns:repeat(6,1fr)}
+.grid.g8{grid-template-columns:repeat(8,1fr)}
+
+/* CARD */
+.card{
+  aspect-ratio:3/4;
+  perspective:700px;
+  cursor:pointer;
+  position:relative;
+}
+.card-inner{
+  width:100%;height:100%;
+  position:relative;
+  transform-style:preserve-3d;
+  transition:transform .52s var(--ease-flip);
+  border-radius:var(--r);
+}
+.card.flipped .card-inner{transform:rotateY(180deg)}
+.card.matched .card-inner{transform:rotateY(180deg)}
+.card.matched .card-front{box-shadow:var(--gold-glow)}
+.card:not(.flipped):not(.matched):hover .card-inner{transform:rotateY(15deg) translateY(-3px)}
+.card:not(.flipped):not(.matched):hover{filter:drop-shadow(0 6px 16px rgba(28,26,23,.18))}
+
+.card-face{
+  position:absolute;inset:0;
+  backface-visibility:hidden;-webkit-backface-visibility:hidden;
+  border-radius:var(--r);
+  overflow:hidden;
+}
+.card-back{
+  background:var(--card-back);
+  display:flex;align-items:center;justify-content:center;
+}
+.card-front{
+  transform:rotateY(180deg);
+  border:1.5px solid rgba(201,169,97,.35);
+}
+
+/* WIN OVERLAY */
+.win-overlay{
+  position:fixed;inset:0;
+  display:flex;align-items:center;justify-content:center;
+  background:rgba(13,11,9,.72);
+  z-index:100;
+  visibility:hidden;
+  opacity:0;
+  transition:opacity .35s var(--ease),visibility 0s .35s;
+}
+.win-overlay.show{
+  visibility:visible;opacity:1;
+  transition:opacity .35s var(--ease),visibility 0s 0s;
+}
+.win-card{
+  background:var(--bg);
+  border:1px solid rgba(201,169,97,.5);
+  border-radius:10px;
+  padding:2.5rem 2.8rem;
+  text-align:center;
+  max-width:380px;width:90%;
+  box-shadow:0 20px 60px rgba(13,11,9,.45);
+  position:relative;
+}
+.win-title{
+  font-family:var(--serif);font-size:2.4rem;
+  letter-spacing:.08em;color:var(--ink);
+  line-height:1.1;margin-bottom:.3rem;
+}
+.win-sub{font-size:.78rem;letter-spacing:.18em;text-transform:uppercase;color:var(--accent2);margin-bottom:1.6rem}
+.win-stats{
+  display:grid;grid-template-columns:1fr 1fr;gap:.75rem;
+  margin-bottom:1.6rem;
+}
+.win-stat{
+  background:var(--bg2);border-radius:var(--r);padding:.75rem .5rem;
+  border:1px solid rgba(201,169,97,.2);
+}
+.win-stat-label{font-size:.65rem;letter-spacing:.1em;text-transform:uppercase;color:var(--ink2);opacity:.7;margin-bottom:.25rem}
+.win-stat-val{font-family:var(--serif);font-size:1.6rem;color:var(--ink)}
+.win-best{font-size:.7rem;color:var(--accent);margin-top:.2rem;font-style:italic}
+.win-btn{
+  font-family:var(--serif);font-size:1rem;letter-spacing:.12em;
+  padding:.7rem 2.2rem;
+  background:var(--accent);color:var(--ink);
+  border:none;border-radius:var(--r);cursor:pointer;
+  transition:background .2s,transform .15s;
+  text-transform:uppercase;
+}
+.win-btn:hover{background:var(--accent2);transform:translateY(-1px)}
+.win-acc{
+  width:60px;height:2px;background:linear-gradient(90deg,transparent,var(--accent),transparent);
+  margin:0 auto 1.4rem;
+}
+
+/* RESPONSIVE */
+@media(max-width:480px){
+  .wordmark{font-size:22px;letter-spacing:.12em}
+  .wordmark span{font-size:.42em}
+  header{padding:.7rem 1rem}
+  .grid.g8{grid-template-columns:repeat(8,1fr)}
+}
+</style>
+</head>
+<body>
+
+<header>
+  <div class="wordmark">
+    Designer Wallcoverings
+    <span>Swatch Memory</span>
+  </div>
+  <div class="header-controls">
+    <button class="mute-btn" id="muteBtn" aria-label="Toggle sound" title="Toggle sound">♪</button>
+  </div>
+</header>
+
+<div class="controls" role="toolbar" aria-label="Difficulty">
+  <button class="diff-btn active" data-diff="4" aria-pressed="true">4 × 4</button>
+  <span class="sep">|</span>
+  <button class="diff-btn" data-diff="6" aria-pressed="false">6 × 6</button>
+  <span class="sep">|</span>
+  <button class="diff-btn" data-diff="8" aria-pressed="false">8 × 8</button>
+</div>
+
+<div class="hud" aria-live="polite" aria-atomic="true">
+  <div class="hud-stat">
+    <span class="hud-label">Moves</span>
+    <span class="hud-val" id="movesVal">0</span>
+  </div>
+  <div class="hud-stat">
+    <span class="hud-label">Time</span>
+    <span class="hud-val" id="timerVal">0:00</span>
+  </div>
+  <div class="hud-stat">
+    <span class="hud-label">Pairs</span>
+    <span class="hud-val" id="pairsVal">0</span>
+  </div>
+  <div class="hud-stat" style="flex-direction:column;align-items:center">
+    <div style="display:flex;gap:.4rem;align-items:center">
+      <span class="hud-label">Best</span>
+      <span class="hud-val hud-best" id="bestVal">—</span>
+    </div>
+  </div>
+</div>
+
+<div class="game-area">
+  <div class="grid g4" id="grid" role="grid" aria-label="Memory card grid"></div>
+</div>
+
+<div class="win-overlay" id="winOverlay" role="dialog" aria-modal="true" aria-label="You won!" aria-hidden="true">
+  <div class="win-card">
+    <div class="win-title">You Found<br>Them All</div>
+    <div class="win-acc"></div>
+    <div class="win-sub">Designer Wallcoverings</div>
+    <div class="win-stats">
+      <div class="win-stat">
+        <div class="win-stat-label">Moves</div>
+        <div class="win-stat-val" id="wMoves">—</div>
+        <div class="win-best" id="wMovesBest"></div>
+      </div>
+      <div class="win-stat">
+        <div class="win-stat-label">Time</div>
+        <div class="win-stat-val" id="wTime">—</div>
+        <div class="win-best" id="wTimeBest"></div>
+      </div>
+    </div>
+    <button class="win-btn" id="playAgainBtn">Play Again</button>
+  </div>
+</div>
+
+<script>
+/* ──────────────────────────────────────────────
+   SVG SWATCH DEFINITIONS
+   Each returns an SVG string ~ a distinct wallcovering texture.
+────────────────────────────────────────────── */
+const SWATCHES = [
+
+// 0 GRASSCLOTH — horizontal woven raffia lines
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="gc${id}" x="0" y="0" width="4" height="6" patternUnits="userSpaceOnUse">
+    <rect width="4" height="6" fill="#c8b99a"/>
+    <line x1="0" y1="1" x2="4" y2="1.2" stroke="#a89070" stroke-width=".8" opacity=".9"/>
+    <line x1="0" y1="3" x2="4" y2="2.8" stroke="#b8a080" stroke-width=".6" opacity=".7"/>
+    <line x1="0" y1="5" x2="4" y2="5.1" stroke="#987860" stroke-width=".7" opacity=".8"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#gc${id})"/>
+  <rect width="120" height="160" fill="rgba(180,160,120,.08)"/>
+</svg>`,
+
+// 1 DAMASK — symmetrical medallion repeat
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="dm${id}" x="0" y="0" width="30" height="40" patternUnits="userSpaceOnUse">
+    <rect width="30" height="40" fill="#e8dfc8"/>
+    <ellipse cx="15" cy="20" rx="9" ry="12" fill="none" stroke="#8a6a40" stroke-width="1.1"/>
+    <ellipse cx="15" cy="20" rx="5" ry="7" fill="#c9a961" opacity=".55"/>
+    <path d="M15 8 Q19 14 15 20 Q11 14 15 8Z" fill="#8a6a40" opacity=".5"/>
+    <path d="M15 32 Q19 26 15 20 Q11 26 15 32Z" fill="#8a6a40" opacity=".5"/>
+    <path d="M6 20 Q12 16 15 20 Q12 24 6 20Z" fill="#8a6a40" opacity=".5"/>
+    <path d="M24 20 Q18 16 15 20 Q18 24 24 20Z" fill="#8a6a40" opacity=".5"/>
+    <circle cx="15" cy="20" r="2.5" fill="#c9a961"/>
+    <circle cx="0" cy="0" r="1.2" fill="#8a6a40" opacity=".4"/>
+    <circle cx="30" cy="0" r="1.2" fill="#8a6a40" opacity=".4"/>
+    <circle cx="0" cy="40" r="1.2" fill="#8a6a40" opacity=".4"/>
+    <circle cx="30" cy="40" r="1.2" fill="#8a6a40" opacity=".4"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#dm${id})"/>
+</svg>`,
+
+// 2 SILK STRIPE — fine vertical alternating bands
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="ss${id}" x="0" y="0" width="16" height="4" patternUnits="userSpaceOnUse">
+    <rect width="16" height="4" fill="#d4c5b5"/>
+    <rect x="0" width="7" height="4" fill="#c0ae9c"/>
+    <rect x="7" width="1" height="4" fill="#a09080" opacity=".5"/>
+    <rect x="15" width="1" height="4" fill="#a09080" opacity=".5"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#ss${id})"/>
+  <rect width="120" height="160" fill="url(#ss${id})" opacity=".25" transform="rotate(90,60,80)"/>
+</svg>`,
+
+// 3 CHINOISERIE — pagoda + branch motifs
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="ch${id}" x="0" y="0" width="60" height="80" patternUnits="userSpaceOnUse">
+    <rect width="60" height="80" fill="#f0ece0"/>
+    <path d="M30 10 L34 18 L26 18Z" fill="#4a7a6a" opacity=".8"/>
+    <rect x="28" y="18" width="4" height="8" fill="#6a4a2a" opacity=".7"/>
+    <path d="M18 22 Q30 18 42 22" stroke="#5a8060" stroke-width="1.2" fill="none"/>
+    <circle cx="18" cy="21" r="2.5" fill="#c85050" opacity=".7"/>
+    <circle cx="42" cy="21" r="2.5" fill="#c85050" opacity=".7"/>
+    <path d="M25 30 Q30 26 35 30 Q30 34 25 30Z" fill="#4a7a6a" opacity=".6"/>
+    <path d="M12 50 Q30 44 48 50" stroke="#7a9870" stroke-width="1" fill="none" opacity=".6"/>
+    <circle cx="30" cy="60" r="4" fill="none" stroke="#4a7a6a" stroke-width="1.2"/>
+    <path d="M26 60 Q30 56 34 60 Q30 64 26 60Z" fill="#4a7a6a" opacity=".5"/>
+    <path d="M8 72 Q15 68 22 72" stroke="#c87840" stroke-width="1" fill="none"/>
+    <circle cx="8" cy="72" r="1.5" fill="#c87840"/>
+    <circle cx="22" cy="72" r="1.5" fill="#c87840"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#ch${id})"/>
+</svg>`,
+
+// 4 LATTICE GEO — interlocking diamond grid
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="lg${id}" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
+    <rect width="20" height="20" fill="#e2d8c8"/>
+    <path d="M10 0 L20 10 L10 20 L0 10Z" fill="none" stroke="#8a7050" stroke-width="1.3"/>
+    <path d="M10 4 L16 10 L10 16 L4 10Z" fill="none" stroke="#c9a961" stroke-width=".7" opacity=".7"/>
+    <circle cx="10" cy="10" r="1.8" fill="#8a7050" opacity=".6"/>
+    <circle cx="0" cy="0" r="1.2" fill="#8a7050" opacity=".5"/>
+    <circle cx="20" cy="0" r="1.2" fill="#8a7050" opacity=".5"/>
+    <circle cx="0" cy="20" r="1.2" fill="#8a7050" opacity=".5"/>
+    <circle cx="20" cy="20" r="1.2" fill="#8a7050" opacity=".5"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#lg${id})"/>
+</svg>`,
+
+// 5 LINEN — woven crosshatch texture
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs>
+    <filter id="fn${id}"><feTurbulence type="fractalNoise" baseFrequency=".65" numOctaves="2" result="n"/><feColorMatrix in="n" type="saturate" values="0" result="g"/><feBlend in="SourceGraphic" in2="g" mode="multiply"/></filter>
+    <pattern id="li${id}" x="0" y="0" width="6" height="6" patternUnits="userSpaceOnUse">
+      <rect width="6" height="6" fill="#d8ceba"/>
+      <line x1="0" y1="0" x2="6" y2="0" stroke="#bfb09c" stroke-width=".8"/>
+      <line x1="0" y1="3" x2="6" y2="3" stroke="#c8b9a5" stroke-width=".5" opacity=".7"/>
+      <line x1="0" y1="0" x2="0" y2="6" stroke="#b8a898" stroke-width=".8"/>
+      <line x1="3" y1="0" x2="3" y2="6" stroke="#c0b0a0" stroke-width=".5" opacity=".7"/>
+    </pattern>
+  </defs>
+  <rect width="120" height="160" fill="url(#li${id})"/>
+  <rect width="120" height="160" fill="#c8b89a" opacity=".08" filter="url(#fn${id})"/>
+</svg>`,
+
+// 6 MOIRE — wavy interference bands
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs>
+    <pattern id="m1${id}" x="0" y="0" width="4" height="120" patternUnits="userSpaceOnUse">
+      <rect width="4" height="120" fill="#b8c8d8"/>
+      <rect x="0" width="2" height="120" fill="#9ab0c4"/>
+    </pattern>
+    <pattern id="m2${id}" x="0" y="0" width="4" height="120" patternUnits="userSpaceOnUse">
+      <rect width="4" height="120" fill="transparent"/>
+      <rect x="0" width="2" height="120" fill="#7090aa" opacity=".4"/>
+    </pattern>
+  </defs>
+  <rect width="120" height="160" fill="url(#m1${id})"/>
+  <rect width="120" height="160" fill="url(#m2${id})" transform="rotate(5,60,80)"/>
+  <rect width="120" height="160" fill="rgba(180,210,230,.12)"/>
+</svg>`,
+
+// 7 BOTANICAL — stylized leaves and stems
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="bt${id}" x="0" y="0" width="40" height="50" patternUnits="userSpaceOnUse">
+    <rect width="40" height="50" fill="#e8eedc"/>
+    <path d="M20 8 C24 14 26 20 20 24 C14 20 16 14 20 8Z" fill="#5a7a4a" opacity=".75"/>
+    <path d="M20 8 L20 24" stroke="#6a8a5a" stroke-width=".7"/>
+    <path d="M8 20 C12 14 18 16 20 20 C18 24 12 26 8 20Z" fill="#4a6a3a" opacity=".6"/>
+    <path d="M8 20 L20 20" stroke="#5a7a4a" stroke-width=".7"/>
+    <path d="M32 18 C28 12 22 14 20 18 C22 22 28 24 32 18Z" fill="#6a8040" opacity=".65"/>
+    <path d="M32 18 L20 18" stroke="#5a7a4a" stroke-width=".7"/>
+    <path d="M20 35 C22 40 26 44 24 48" stroke="#5a7a4a" stroke-width="1.2" fill="none"/>
+    <circle cx="30" cy="38" r="3.5" fill="#c84050" opacity=".7"/>
+    <circle cx="10" cy="44" r="2.5" fill="#d06040" opacity=".65"/>
+    <path d="M20 24 L20 35" stroke="#5a7a4a" stroke-width="1" fill="none"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#bt${id})"/>
+</svg>`,
+
+// 8 TRELLIS — repeating arch/lattice
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="tr${id}" x="0" y="0" width="24" height="24" patternUnits="userSpaceOnUse">
+    <rect width="24" height="24" fill="#e4ddd0"/>
+    <path d="M0 12 Q12 0 24 12" stroke="#7a6850" stroke-width="1.4" fill="none"/>
+    <path d="M0 12 Q12 24 24 12" stroke="#7a6850" stroke-width="1.4" fill="none"/>
+    <circle cx="0" cy="12" r="2" fill="#c9a961" opacity=".7"/>
+    <circle cx="24" cy="12" r="2" fill="#c9a961" opacity=".7"/>
+    <circle cx="12" cy="0" r="2" fill="#9a8060" opacity=".6"/>
+    <circle cx="12" cy="24" r="2" fill="#9a8060" opacity=".6"/>
+    <circle cx="12" cy="12" r="1.5" fill="#c9a961" opacity=".5"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#tr${id})"/>
+</svg>`,
+
+// 9 GREEK KEY — meander border tile
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="gk${id}" x="0" y="0" width="32" height="32" patternUnits="userSpaceOnUse">
+    <rect width="32" height="32" fill="#1c2838"/>
+    <path d="M4 4 L28 4 L28 28 L4 28 L4 12 L12 12 L12 20 L20 20 L20 12 L4 12" fill="none" stroke="#c9a961" stroke-width="2.2"/>
+    <path d="M0 0 L4 0 L4 4 L0 4Z" fill="#c9a961" opacity=".3"/>
+    <path d="M28 0 L32 0 L32 4 L28 4Z" fill="#c9a961" opacity=".3"/>
+    <path d="M0 28 L4 28 L4 32 L0 32Z" fill="#c9a961" opacity=".3"/>
+    <path d="M28 28 L32 28 L32 32 L28 32Z" fill="#c9a961" opacity=".3"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#gk${id})"/>
+</svg>`,
+
+// 10 HERRINGBONE — classic chevron weave
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="hb${id}" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
+    <rect width="20" height="20" fill="#d0c4b0"/>
+    <path d="M0 10 L5 0 L10 10 L5 20Z" fill="#b8a890" opacity=".7"/>
+    <path d="M10 10 L15 0 L20 10 L15 20Z" fill="#c8b8a0" opacity=".7"/>
+    <path d="M5 0 L10 10 L15 0" stroke="#a09080" stroke-width=".8" fill="none"/>
+    <path d="M5 20 L10 10 L15 20" stroke="#a09080" stroke-width=".8" fill="none"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#hb${id})"/>
+</svg>`,
+
+// 11 OGEE — classic teardrop scale
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="og${id}" x="0" y="0" width="30" height="36" patternUnits="userSpaceOnUse">
+    <rect width="30" height="36" fill="#ede0d0"/>
+    <path d="M15 2 C26 2 28 16 15 26 C2 16 4 2 15 2Z" fill="none" stroke="#8a6050" stroke-width="1.5"/>
+    <path d="M15 6 C22 6 24 16 15 24 C6 16 8 6 15 6Z" fill="rgba(201,169,97,.18)"/>
+    <circle cx="15" cy="14" r="3" fill="#c9a961" opacity=".5"/>
+    <path d="M0 26 C4 20 11 22 15 26 C19 22 26 20 30 26" stroke="#8a6050" stroke-width=".8" fill="none"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#og${id})"/>
+</svg>`,
+
+// 12 TOILE — scenic vignette, navy + cream
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="tl${id}" x="0" y="0" width="60" height="80" patternUnits="userSpaceOnUse">
+    <rect width="60" height="80" fill="#f2eedd"/>
+    <path d="M10 60 Q14 40 20 30 Q26 20 30 18 Q34 20 38 28 Q42 36 44 50" stroke="#2a3a5a" stroke-width="1.3" fill="none"/>
+    <ellipse cx="30" cy="18" rx="6" ry="5" fill="#2a3a5a" opacity=".15"/>
+    <path d="M20 30 Q24 26 28 28" stroke="#2a3a5a" stroke-width=".9" fill="none"/>
+    <path d="M32 28 Q36 26 40 30" stroke="#2a3a5a" stroke-width=".9" fill="none"/>
+    <path d="M10 60 Q15 62 20 60 Q25 58 30 62 Q35 66 40 62 Q42 60 44 58" stroke="#2a3a5a" stroke-width="1" fill="none"/>
+    <path d="M5 20 Q8 12 12 15 Q14 18 12 22" stroke="#2a3a5a" stroke-width=".9" fill="none"/>
+    <path d="M48 40 Q52 32 56 35 Q58 38 56 42" stroke="#2a3a5a" stroke-width=".9" fill="none"/>
+    <circle cx="10" cy="72" r="3" fill="#2a3a5a" opacity=".18"/>
+    <circle cx="50" cy="68" r="2.5" fill="#2a3a5a" opacity=".18"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#tl${id})"/>
+</svg>`,
+
+// 13 BAMBOO — vertical stalks and nodes
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="bm${id}" x="0" y="0" width="18" height="40" patternUnits="userSpaceOnUse">
+    <rect width="18" height="40" fill="#d8e4c0"/>
+    <rect x="3" y="0" width="5" height="40" fill="#90a860" opacity=".7"/>
+    <rect x="4" y="0" width="3" height="40" fill="#a8c070" opacity=".5"/>
+    <rect x="3" y="13" width="5" height="2" fill="#708040" opacity=".8"/>
+    <rect x="3" y="27" width="5" height="2" fill="#708040" opacity=".8"/>
+    <rect x="11" y="0" width="4" height="40" fill="#88a058" opacity=".6"/>
+    <rect x="11" y="20" width="4" height="1.5" fill="#607030" opacity=".8"/>
+    <path d="M8 14 Q14 10 16 6" stroke="#60a040" stroke-width="1" fill="none" opacity=".7"/>
+    <path d="M8 28 Q2 24 0 20" stroke="#60a040" stroke-width="1" fill="none" opacity=".7"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#bm${id})"/>
+</svg>`,
+
+// 14 MARBLE — veined stone look
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs>
+    <filter id="mb${id}"><feTurbulence type="turbulence" baseFrequency=".018 .035" numOctaves="4" seed="9" result="t"/><feColorMatrix in="t" type="matrix" values="0 0 0 0 .85 0 0 0 0 .82 0 0 0 0 .78 0 0 0 1 0" result="stone"/><feBlend in="SourceGraphic" in2="stone" mode="multiply"/></filter>
+    <linearGradient id="mv${id}" x1="0" y1="0" x2="1" y2="1">
+      <stop offset="0%" stop-color="#e8e2d8"/>
+      <stop offset="40%" stop-color="#f0ece4"/>
+      <stop offset="100%" stop-color="#d8d0c4"/>
+    </linearGradient>
+  </defs>
+  <rect width="120" height="160" fill="url(#mv${id})"/>
+  <path d="M10 0 Q30 20 20 50 Q15 70 35 100 Q50 120 40 160" stroke="#aaa090" stroke-width="2.5" fill="none" opacity=".5"/>
+  <path d="M30 0 Q50 30 40 60 Q35 80 55 110 Q70 130 60 160" stroke="#c0b8a8" stroke-width="1.5" fill="none" opacity=".35"/>
+  <path d="M60 0 Q40 30 55 70 Q65 100 50 160" stroke="#9a9288" stroke-width="1.8" fill="none" opacity=".4"/>
+  <path d="M90 10 Q70 40 85 80 Q95 110 80 160" stroke="#b8b0a0" stroke-width="1.2" fill="none" opacity=".3"/>
+  <rect width="120" height="160" fill="#e8e2d8" opacity=".1" filter="url(#mb${id})"/>
+</svg>`,
+
+// 15 FLORAL MEDALLION — ornate flower rosette
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="fl${id}" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
+    <rect width="40" height="40" fill="#f0e8d8"/>
+    <circle cx="20" cy="20" r="10" fill="none" stroke="#9a7050" stroke-width="1"/>
+    <path d="M20 10 C22 14 22 16 20 20 C18 16 18 14 20 10Z" fill="#9a7050" opacity=".65"/>
+    <path d="M20 30 C22 26 22 24 20 20 C18 24 18 26 20 30Z" fill="#9a7050" opacity=".65"/>
+    <path d="M10 20 C14 22 16 22 20 20 C16 18 14 18 10 20Z" fill="#9a7050" opacity=".65"/>
+    <path d="M30 20 C26 22 24 22 20 20 C24 18 26 18 30 20Z" fill="#9a7050" opacity=".65"/>
+    <path d="M13 13 C16 16 17 18 20 20 C18 17 16 16 13 13Z" fill="#c9a961" opacity=".7"/>
+    <path d="M27 13 C24 16 23 18 20 20 C22 17 24 16 27 13Z" fill="#c9a961" opacity=".7"/>
+    <path d="M13 27 C16 24 17 22 20 20 C18 23 16 24 13 27Z" fill="#c9a961" opacity=".7"/>
+    <path d="M27 27 C24 24 23 22 20 20 C22 23 24 24 27 27Z" fill="#c9a961" opacity=".7"/>
+    <circle cx="20" cy="20" r="3.5" fill="#c9a961"/>
+    <circle cx="20" cy="20" r="1.5" fill="#7a5030"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#fl${id})"/>
+</svg>`,
+
+// 16 IKAT — bold blurred diamond chevron
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs>
+    <filter id="ik${id}"><feGaussianBlur stdDeviation="1.2"/></filter>
+    <pattern id="ip${id}" x="0" y="0" width="24" height="24" patternUnits="userSpaceOnUse">
+      <rect width="24" height="24" fill="#3a2060"/>
+      <path d="M12 0 L24 12 L12 24 L0 12Z" fill="#9060c0" opacity=".7" filter="url(#ik${id})"/>
+      <path d="M12 4 L20 12 L12 20 L4 12Z" fill="#c090e0" opacity=".5" filter="url(#ik${id})"/>
+      <path d="M12 8 L16 12 L12 16 L8 12Z" fill="#e8c0f8" opacity=".5"/>
+    </pattern>
+  </defs>
+  <rect width="120" height="160" fill="url(#ip${id})"/>
+</svg>`,
+
+// 17 PAPERWEAVE — thin woven kraft paper texture
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="pw${id}" x="0" y="0" width="8" height="8" patternUnits="userSpaceOnUse">
+    <rect width="8" height="8" fill="#c8b89a"/>
+    <rect x="0" y="0" width="8" height="2" fill="#b8a888" opacity=".9"/>
+    <rect x="0" y="4" width="8" height="2" fill="#c0b090" opacity=".8"/>
+    <rect x="0" y="0" width="2" height="8" fill="#b0a080" opacity=".5"/>
+    <rect x="4" y="0" width="1" height="8" fill="#c8b890" opacity=".4"/>
+    <rect x="2" y="2" width="2" height="2" fill="#d0c0a0" opacity=".6"/>
+    <rect x="6" y="6" width="2" height="2" fill="#a89878" opacity=".6"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#pw${id})"/>
+</svg>`,
+
+// 18 TARTAN PLAID — classic crossband plaid
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs>
+    <pattern id="tp${id}" x="0" y="0" width="32" height="32" patternUnits="userSpaceOnUse">
+      <rect width="32" height="32" fill="#6a3020"/>
+      <rect x="0" y="0" width="8" height="32" fill="#8a4030" opacity=".9"/>
+      <rect x="12" y="0" width="8" height="32" fill="#8a4030" opacity=".9"/>
+      <rect x="24" y="0" width="4" height="32" fill="#c9a961" opacity=".5"/>
+      <rect x="0" y="0" width="32" height="8" fill="#8a4030" opacity=".5"/>
+      <rect x="0" y="12" width="32" height="8" fill="#8a4030" opacity=".5"/>
+      <rect x="0" y="24" width="32" height="4" fill="#c9a961" opacity=".35"/>
+      <line x1="4" y1="0" x2="4" y2="32" stroke="#ffffff" stroke-width=".8" opacity=".15"/>
+      <line x1="0" y1="4" x2="32" y2="4" stroke="#ffffff" stroke-width=".8" opacity=".15"/>
+    </pattern>
+  </defs>
+  <rect width="120" height="160" fill="url(#tp${id})"/>
+</svg>`,
+
+// 19 CHEVRON — bold zigzag bands
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="cv${id}" x="0" y="0" width="40" height="20" patternUnits="userSpaceOnUse">
+    <rect width="40" height="20" fill="#e0d4c0"/>
+    <path d="M0 20 L10 0 L20 20 L30 0 L40 20" fill="#c0a870" opacity=".6"/>
+    <path d="M0 20 L10 0 L20 20 L30 0 L40 20" fill="none" stroke="#a08050" stroke-width="1.5"/>
+    <path d="M-10 20 L0 0" stroke="#a08050" stroke-width="1.5"/>
+    <path d="M40 0 L50 20" stroke="#a08050" stroke-width="1.5"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#cv${id})"/>
+</svg>`,
+
+// 20 HOUNDSTOOTH — classic broken check
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="ht${id}" x="0" y="0" width="16" height="16" patternUnits="userSpaceOnUse">
+    <rect width="16" height="16" fill="#f0ece4"/>
+    <path d="M0 0 L8 0 L8 4 L4 4 L4 8 L0 8Z" fill="#2a2420"/>
+    <path d="M8 8 L16 8 L16 12 L12 12 L12 16 L8 16Z" fill="#2a2420"/>
+    <path d="M8 0 L12 0 L12 4 L16 4 L16 8 L8 8Z" fill="#2a2420" opacity=".5"/>
+    <path d="M0 8 L4 8 L4 12 L8 12 L8 16 L0 16Z" fill="#2a2420" opacity=".5"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#ht${id})"/>
+</svg>`,
+
+// 21 PERSIAN RUG — ornate medallion border
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="pr${id}" x="0" y="0" width="36" height="48" patternUnits="userSpaceOnUse">
+    <rect width="36" height="48" fill="#8a2020"/>
+    <rect x="3" y="3" width="30" height="42" fill="none" stroke="#c9a961" stroke-width="1.5"/>
+    <rect x="6" y="6" width="24" height="36" fill="#6a1818"/>
+    <polygon points="18,10 22,18 18,22 14,18" fill="#c9a961" opacity=".8"/>
+    <polygon points="18,38 22,30 18,26 14,30" fill="#c9a961" opacity=".8"/>
+    <polygon points="8,24 16,20 20,24 16,28" fill="#c9a961" opacity=".8"/>
+    <polygon points="28,24 20,20 16,24 20,28" fill="#c9a961" opacity=".8"/>
+    <circle cx="18" cy="24" r="3.5" fill="#c9a961" opacity=".9"/>
+    <circle cx="18" cy="24" r="1.5" fill="#8a2020"/>
+    <circle cx="3" cy="3" r="2" fill="#c9a961" opacity=".7"/>
+    <circle cx="33" cy="3" r="2" fill="#c9a961" opacity=".7"/>
+    <circle cx="3" cy="45" r="2" fill="#c9a961" opacity=".7"/>
+    <circle cx="33" cy="45" r="2" fill="#c9a961" opacity=".7"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#pr${id})"/>
+</svg>`,
+
+// 22 ABSTRACT BRUSH — gestural ink strokes
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <rect width="120" height="160" fill="#f8f2e8"/>
+  <path d="M10 20 Q40 10 80 30 Q110 45 100 80 Q90 110 50 100 Q20 90 30 60 Q40 35 70 50" stroke="#1c2838" stroke-width="2.5" fill="none" opacity=".5"/>
+  <path d="M5 80 Q35 70 60 90 Q85 110 100 140" stroke="#1c2838" stroke-width="1.8" fill="none" opacity=".35"/>
+  <path d="M60 10 Q90 30 80 70 Q70 100 90 130" stroke="#8a6040" stroke-width="1.5" fill="none" opacity=".4"/>
+  <ellipse cx="45" cy="55" rx="15" ry="8" fill="#c9a961" opacity=".25" transform="rotate(-20,45,55)"/>
+  <ellipse cx="75" cy="110" rx="12" ry="6" fill="#8a4040" opacity=".2" transform="rotate(15,75,110)"/>
+</svg>`,
+
+// 23 SCALES / FISH — overlapping arc scales
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="sc${id}" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
+    <rect width="20" height="20" fill="#c0d8e0"/>
+    <ellipse cx="0" cy="20" rx="10" ry="12" fill="none" stroke="#4a7898" stroke-width="1.2"/>
+    <ellipse cx="10" cy="20" rx="10" ry="12" fill="none" stroke="#5a88a8" stroke-width="1.2"/>
+    <ellipse cx="20" cy="20" rx="10" ry="12" fill="none" stroke="#4a7898" stroke-width="1.2"/>
+    <ellipse cx="5" cy="10" rx="10" ry="12" fill="rgba(74,120,152,.12)"/>
+    <ellipse cx="15" cy="10" rx="10" ry="12" fill="rgba(90,136,168,.12)"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#sc${id})"/>
+</svg>`,
+
+// 24 TILE HEXAGON — honeycomb
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="hx${id}" x="0" y="0" width="30" height="26" patternUnits="userSpaceOnUse">
+    <rect width="30" height="26" fill="#d8e8d0"/>
+    <path d="M15 1 L27 8 L27 20 L15 27 L3 20 L3 8Z" fill="none" stroke="#608060" stroke-width="1.5"/>
+    <path d="M15 5 L23 10 L23 18 L15 23 L7 18 L7 10Z" fill="rgba(96,128,96,.15)"/>
+    <circle cx="15" cy="14" r="2.5" fill="#608060" opacity=".5"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#hx${id})"/>
+</svg>`,
+
+// 25 PINSTRIPE — ultra-fine vertical lines
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="ps${id}" x="0" y="0" width="10" height="4" patternUnits="userSpaceOnUse">
+    <rect width="10" height="4" fill="#2a2830"/>
+    <rect x="0" width="1" height="4" fill="#c9a961" opacity=".85"/>
+    <rect x="5" width="1" height="4" fill="#a08040" opacity=".5"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#ps${id})"/>
+</svg>`,
+
+// 26 PAISLEY — classic teardrop paisley
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="py${id}" x="0" y="0" width="36" height="48" patternUnits="userSpaceOnUse">
+    <rect width="36" height="48" fill="#2a1840"/>
+    <path d="M18 8 C26 10 30 20 26 30 C22 40 12 42 8 36 C4 30 8 18 14 14 C10 18 10 28 16 32 C22 36 26 28 22 22 C18 16 14 16 14 20 C14 24 18 26 20 24 C22 22 20 18 18 18Z" fill="#c9a961" opacity=".6"/>
+    <circle cx="18" cy="12" r="2" fill="#e0b870" opacity=".8"/>
+    <path d="M18 8 C16 6 14 8 16 10" stroke="#e0b870" stroke-width=".8" fill="none"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#py${id})"/>
+</svg>`,
+
+// 27 SUZANI CIRCLE — central bold medallion embroidery style
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="sz${id}" x="0" y="0" width="48" height="48" patternUnits="userSpaceOnUse">
+    <rect width="48" height="48" fill="#f8eed8"/>
+    <circle cx="24" cy="24" r="16" fill="none" stroke="#c84040" stroke-width="2"/>
+    <circle cx="24" cy="24" r="10" fill="none" stroke="#4080c0" stroke-width="1.5"/>
+    <circle cx="24" cy="24" r="4" fill="#c84040" opacity=".7"/>
+    <path d="M24 8 L26 16 L24 14 L22 16Z" fill="#c84040" opacity=".8"/>
+    <path d="M24 40 L26 32 L24 34 L22 32Z" fill="#c84040" opacity=".8"/>
+    <path d="M8 24 L16 26 L14 24 L16 22Z" fill="#c84040" opacity=".8"/>
+    <path d="M40 24 L32 26 L34 24 L32 22Z" fill="#c84040" opacity=".8"/>
+    <circle cx="24" cy="8" r="2" fill="#4080c0" opacity=".7"/>
+    <circle cx="24" cy="40" r="2" fill="#4080c0" opacity=".7"/>
+    <circle cx="8" cy="24" r="2" fill="#4080c0" opacity=".7"/>
+    <circle cx="40" cy="24" r="2" fill="#4080c0" opacity=".7"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#sz${id})"/>
+</svg>`,
+
+// 28 QUATREFOIL — gothic arch cut-out repeat
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="qf${id}" x="0" y="0" width="28" height="28" patternUnits="userSpaceOnUse">
+    <rect width="28" height="28" fill="#e0d8c8"/>
+    <circle cx="14" cy="7" r="6" fill="none" stroke="#806040" stroke-width="1.3"/>
+    <circle cx="14" cy="21" r="6" fill="none" stroke="#806040" stroke-width="1.3"/>
+    <circle cx="7" cy="14" r="6" fill="none" stroke="#806040" stroke-width="1.3"/>
+    <circle cx="21" cy="14" r="6" fill="none" stroke="#806040" stroke-width="1.3"/>
+    <circle cx="14" cy="14" r="3" fill="#c9a961" opacity=".5"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#qf${id})"/>
+</svg>`,
+
+// 29 SEAGRASS — loose organic horizontal bundles
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="sg${id}" x="0" y="0" width="16" height="10" patternUnits="userSpaceOnUse">
+    <rect width="16" height="10" fill="#b8c8a0"/>
+    <path d="M0 3 Q4 2 8 3 Q12 4 16 3" stroke="#809060" stroke-width="1.2" fill="none"/>
+    <path d="M0 6 Q4 7 8 6 Q12 5 16 6" stroke="#90a870" stroke-width="1" fill="none" opacity=".8"/>
+    <path d="M0 9 Q4 8 8 9 Q12 10 16 9" stroke="#708050" stroke-width=".9" fill="none" opacity=".7"/>
+    <path d="M0 1 Q2 0 4 1" stroke="#a0b880" stroke-width=".6" fill="none"/>
+    <path d="M10 8 Q13 9 16 8" stroke="#a0b880" stroke-width=".6" fill="none"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#sg${id})"/>
+</svg>`,
+
+// 30 ART DECO SUNBURST — fan rays emanating from corner
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="ad${id}" x="0" y="0" width="60" height="80" patternUnits="userSpaceOnUse">
+    <rect width="60" height="80" fill="#1a1628"/>
+    <path d="M0 80 L60 80 A60 80 0 0 0 0 0Z" fill="#c9a961" opacity=".08"/>
+    <line x1="0" y1="80" x2="60" y2="0" stroke="#c9a961" stroke-width="1.2" opacity=".5"/>
+    <line x1="0" y1="80" x2="45" y2="0" stroke="#c9a961" stroke-width=".9" opacity=".4"/>
+    <line x1="0" y1="80" x2="30" y2="0" stroke="#c9a961" stroke-width=".9" opacity=".4"/>
+    <line x1="0" y1="80" x2="15" y2="0" stroke="#c9a961" stroke-width=".9" opacity=".35"/>
+    <line x1="0" y1="80" x2="0" y2="0" stroke="#c9a961" stroke-width="1" opacity=".45"/>
+    <line x1="0" y1="80" x2="60" y2="20" stroke="#c9a961" stroke-width=".8" opacity=".3"/>
+    <line x1="0" y1="80" x2="60" y2="40" stroke="#c9a961" stroke-width=".8" opacity=".3"/>
+    <line x1="0" y1="80" x2="60" y2="60" stroke="#c9a961" stroke-width=".8" opacity=".3"/>
+    <circle cx="0" cy="80" r="8" fill="none" stroke="#c9a961" stroke-width="1" opacity=".4"/>
+    <circle cx="0" cy="80" r="16" fill="none" stroke="#c9a961" stroke-width=".7" opacity=".25"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#ad${id})"/>
+</svg>`,
+
+// 31 BLOCK PRINT TULIP — hand-printed floral stamp
+(id)=>`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+  <defs><pattern id="bp${id}" x="0" y="0" width="32" height="44" patternUnits="userSpaceOnUse">
+    <rect width="32" height="44" fill="#f4eadc"/>
+    <path d="M16 8 C20 8 22 14 22 20 L16 20 L10 20 C10 14 12 8 16 8Z" fill="#d06080" opacity=".75"/>
+    <path d="M16 14 C18 14 19 17 19 20 L16 20 L13 20 C13 17 14 14 16 14Z" fill="#e890a0" opacity=".6"/>
+    <rect x="14" y="20" width="4" height="14" fill="#507040" opacity=".8"/>
+    <path d="M14 28 Q8 24 6 30" stroke="#507040" stroke-width="1.3" fill="none"/>
+    <path d="M18 32 Q24 28 26 34" stroke="#507040" stroke-width="1.3" fill="none"/>
+    <circle cx="6" cy="36" r="3" fill="#507040" opacity=".5"/>
+    <circle cx="26" cy="40" r="3" fill="#507040" opacity=".5"/>
+  </pattern></defs>
+  <rect width="120" height="160" fill="url(#bp${id})"/>
+</svg>`,
+
+];
+
+/* ──────────────────────────────────────────────
+   CARD BACK SVG (DW monogram)
+────────────────────────────────────────────── */
+function cardBackSVG() {
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" style="display:block;width:100%;height:100%">
+    <rect width="120" height="160" fill="#0d0b09"/>
+    <rect x="4" y="4" width="112" height="152" fill="none" stroke="#c9a961" stroke-width=".8" opacity=".5"/>
+    <rect x="8" y="8" width="104" height="144" fill="none" stroke="#c9a961" stroke-width=".4" opacity=".3"/>
+    <text x="60" y="88" text-anchor="middle" font-family="Georgia,serif" font-size="38" fill="#c9a961" opacity=".9" letter-spacing="2">DW</text>
+    <text x="60" y="108" text-anchor="middle" font-family="Georgia,serif" font-size="7" fill="#c9a961" opacity=".5" letter-spacing="3.5">DESIGNER</text>
+    <line x1="25" y1="116" x2="95" y2="116" stroke="#c9a961" stroke-width=".6" opacity=".4"/>
+    <text x="60" y="128" text-anchor="middle" font-family="Georgia,serif" font-size="7" fill="#c9a961" opacity=".4" letter-spacing="2">WALLCOVERINGS</text>
+    <circle cx="60" cy="65" r="18" fill="none" stroke="#c9a961" stroke-width=".7" opacity=".3"/>
+    <circle cx="60" cy="65" r="22" fill="none" stroke="#c9a961" stroke-width=".4" opacity=".2"/>
+  </svg>`;
+}
+
+/* ──────────────────────────────────────────────
+   AUDIO ENGINE
+────────────────────────────────────────────── */
+let audioCtx = null;
+let muted = false;
+
+function getAudio() {
+  if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
+  return audioCtx;
+}
+
+function playTone(freq, type, dur, vol=0.18, delay=0) {
+  if (muted) return;
+  try {
+    const ctx = getAudio();
+    const t = ctx.currentTime + delay;
+    const osc = ctx.createOscillator();
+    const gain = ctx.createGain();
+    osc.connect(gain); gain.connect(ctx.destination);
+    osc.type = type; osc.frequency.setValueAtTime(freq, t);
+    gain.gain.setValueAtTime(0, t);
+    gain.gain.linearRampToValueAtTime(vol, t + 0.01);
+    gain.gain.exponentialRampToValueAtTime(0.001, t + dur);
+    osc.start(t); osc.stop(t + dur);
+  } catch(e){}
+}
+
+function sfxFlip()  { playTone(520,'sine',.08,.12); }
+function sfxMiss()  { playTone(260,'sawtooth',.18,.09); }
+function sfxMatch() {
+  playTone(660,'sine',.15,.14,0);
+  playTone(880,'sine',.2,.12,.1);
+  playTone(1100,'sine',.25,.1,.2);
+}
+function sfxWin() {
+  [440,550,660,880,1100].forEach((f,i)=>playTone(f,'sine',.35,.13,i*.1));
+}
+
+/* ──────────────────────────────────────────────
+   GAME STATE
+────────────────────────────────────────────── */
+let diff = 4;         // 4|6|8
+let cards = [];       // DOM card els
+let swatchIds = [];   // swatch index per card
+let flipped = [];     // indices of currently face-up unmatched
+let matched = new Set();
+let moves = 0;
+let pairs = 0;
+let totalPairs = 0;
+let timerInterval = null;
+let seconds = 0;
+let gameStarted = false;
+let locked = false;
+
+const grid = document.getElementById('grid');
+const movesVal = document.getElementById('movesVal');
+const timerVal = document.getElementById('timerVal');
+const pairsVal = document.getElementById('pairsVal');
+const bestVal = document.getElementById('bestVal');
+const winOverlay = document.getElementById('winOverlay');
+const wMoves = document.getElementById('wMoves');
+const wTime = document.getElementById('wTime');
+const wMovesBest = document.getElementById('wMovesBest');
+const wTimeBest = document.getElementById('wTimeBest');
+const muteBtn = document.getElementById('muteBtn');
+
+function fmtTime(s) {
+  return Math.floor(s/60)+':'+(s%60).toString().padStart(2,'0');
+}
+
+function bestKey() { return `dw-memory-best-${diff}`; }
+
+function loadBest() {
+  try { return JSON.parse(localStorage.getItem(bestKey())||'null'); } catch(e){ return null; }
+}
+
+function saveBest(mv, sec) {
+  const prev = loadBest();
+  if (!prev || mv < prev.moves || (mv === prev.moves && sec < prev.secs)) {
+    localStorage.setItem(bestKey(), JSON.stringify({moves:mv,secs:sec}));
+    return true;
+  }
+  return false;
+}
+
+function renderBestHud() {
+  const b = loadBest();
+  bestVal.textContent = b ? `${b.moves}mv · ${fmtTime(b.secs)}` : '—';
+}
+
+/* ──────────────────────────────────────────────
+   SHUFFLE
+────────────────────────────────────────────── */
+function shuffle(arr) {
+  for (let i=arr.length-1;i>0;i--){
+    const j=Math.floor(Math.random()*(i+1));
+    [arr[i],arr[j]]=[arr[j],arr[i]];
+  }
+  return arr;
+}
+
+/* ──────────────────────────────────────────────
+   BUILD GRID
+────────────────────────────────────────────── */
+function buildGrid() {
+  stopTimer();
+  gameStarted = false;
+  locked = false;
+  moves = 0; pairs = 0; seconds = 0;
+  flipped = []; matched = new Set();
+  movesVal.textContent = '0';
+  timerVal.textContent = '0:00';
+  pairsVal.textContent = '0';
+  renderBestHud();
+
+  const n = diff * diff;
+  totalPairs = n / 2;
+
+  // pick distinct swatches
+  const pool = Array.from({length:SWATCHES.length},(_,i)=>i);
+  shuffle(pool);
+  const chosen = pool.slice(0, totalPairs);
+  swatchIds = shuffle([...chosen,...chosen]);
+
+  grid.className = `grid g${diff}`;
+  grid.innerHTML = '';
+  cards = [];
+
+  swatchIds.forEach((swIdx, i) => {
+    const card = document.createElement('div');
+    card.className = 'card';
+    card.setAttribute('role','gridcell');
+    card.setAttribute('aria-label',`Card ${i+1}`);
+    card.setAttribute('tabindex','0');
+    card.dataset.index = i;
+    card.dataset.swatch = swIdx;
+
+    const inner = document.createElement('div');
+    inner.className = 'card-inner';
+
+    const back = document.createElement('div');
+    back.className = 'card-face card-back';
+    back.innerHTML = cardBackSVG();
+
+    const front = document.createElement('div');
+    front.className = 'card-face card-front';
+    front.innerHTML = SWATCHES[swIdx](i);
+
+    inner.appendChild(back);
+    inner.appendChild(front);
+    card.appendChild(inner);
+
+    card.addEventListener('click', () => onCardClick(i));
+    card.addEventListener('keydown', (e) => {
+      if (e.key==='Enter'||e.key===' ') { e.preventDefault(); onCardClick(i); }
+    });
+
+    grid.appendChild(card);
+    cards.push(card);
+  });
+
+  hideWinOverlay();
+}
+
+/* ──────────────────────────────────────────────
+   TIMER
+────────────────────────────────────────────── */
+function startTimer() {
+  if (timerInterval) return;
+  timerInterval = setInterval(() => {
+    seconds++;
+    timerVal.textContent = fmtTime(seconds);
+  }, 1000);
+}
+
+function stopTimer() {
+  clearInterval(timerInterval);
+  timerInterval = null;
+}
+
+/* ──────────────────────────────────────────────
+   CARD CLICK
+────────────────────────────────────────────── */
+function onCardClick(idx) {
+  if (locked) return;
+  if (matched.has(idx)) return;
+  if (flipped.includes(idx)) return;
+  if (flipped.length >= 2) return;
+
+  if (!gameStarted) { gameStarted=true; startTimer(); }
+
+  sfxFlip();
+  cards[idx].classList.add('flipped');
+  flipped.push(idx);
+
+  if (flipped.length === 2) {
+    moves++;
+    movesVal.textContent = moves;
+    const [a,b] = flipped;
+    if (swatchIds[a] === swatchIds[b]) {
+      // MATCH
+      sfxMatch();
+      pairs++;
+      pairsVal.textContent = pairs;
+      matched.add(a); matched.add(b);
+      cards[a].classList.add('matched');
+      cards[b].classList.add('matched');
+      cards[a].setAttribute('aria-label','Matched');
+      cards[b].setAttribute('aria-label','Matched');
+      flipped = [];
+      if (pairs === totalPairs) setTimeout(onWin, 420);
+    } else {
+      // MISMATCH
+      locked = true;
+      setTimeout(() => {
+        sfxMiss();
+        cards[a].classList.remove('flipped');
+        cards[b].classList.remove('flipped');
+        flipped = [];
+        locked = false;
+      }, 900);
+    }
+  }
+}
+
+/* ──────────────────────────────────────────────
+   WIN
+────────────────────────────────────────────── */
+function onWin() {
+  stopTimer();
+  sfxWin();
+  const isNew = saveBest(moves, seconds);
+  const b = loadBest();
+  wMoves.textContent = moves;
+  wTime.textContent = fmtTime(seconds);
+  wMovesBest.textContent = isNew ? 'New best!' : (b ? `Best: ${b.moves} mv` : '');
+  wTimeBest.textContent = isNew ? '' : (b ? `Best: ${fmtTime(b.secs)}` : '');
+  renderBestHud();
+  showWinOverlay();
+}
+
+function showWinOverlay() {
+  winOverlay.classList.add('show');
+  winOverlay.setAttribute('aria-hidden','false');
+  document.getElementById('playAgainBtn').focus();
+}
+
+function hideWinOverlay() {
+  winOverlay.classList.remove('show');
+  winOverlay.setAttribute('aria-hidden','true');
+}
+
+/* ──────────────────────────────────────────────
+   CONTROLS
+────────────────────────────────────────────── */
+document.querySelectorAll('.diff-btn').forEach(btn => {
+  btn.addEventListener('click', () => {
+    document.querySelectorAll('.diff-btn').forEach(b => {
+      b.classList.remove('active');
+      b.setAttribute('aria-pressed','false');
+    });
+    btn.classList.add('active');
+    btn.setAttribute('aria-pressed','true');
+    diff = parseInt(btn.dataset.diff);
+    buildGrid();
+  });
+});
+
+document.getElementById('playAgainBtn').addEventListener('click', () => {
+  buildGrid();
+});
+
+muteBtn.addEventListener('click', () => {
+  muted = !muted;
+  muteBtn.textContent = muted ? '♩' : '♪';
+  muteBtn.setAttribute('aria-label', muted ? 'Unmute sound' : 'Mute sound');
+});
+
+// Close win overlay on Escape
+document.addEventListener('keydown', (e) => {
+  if (e.key==='Escape' && winOverlay.classList.contains('show')) hideWinOverlay();
+});
+
+// Click outside win-card to dismiss
+winOverlay.addEventListener('click', (e) => {
+  if (e.target === winOverlay) { hideWinOverlay(); buildGrid(); }
+});
+
+/* ──────────────────────────────────────────────
+   INIT
+────────────────────────────────────────────── */
+buildGrid();
+</script>
+</body>
+</html>

← 193a975 Agent-driven high-detail: DW Find Your Style quiz (token-saf  ·  back to Model Arena  ·  Agent-driven tool: DW Shop by Color (frontend-developer) — i 2866307 →