[object Object]

← back to Model Arena

Agent-driven game: DW Parquet (frontend-developer) — herringbone sliding puzzle, wood-grain SVG, 3 sizes. Registered.

170d67a1f25b145f3740000616866f70ec92754d · 2026-07-25 12:32:38 -0700 · Steve Abrams

Files touched

Diff

commit 170d67a1f25b145f3740000616866f70ec92754d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Jul 25 12:32:38 2026 -0700

    Agent-driven game: DW Parquet (frontend-developer) — herringbone sliding puzzle, wood-grain SVG, 3 sizes. Registered.
---
 data/arcade-games.json                    |   7 +
 data/artifacts/724633a205b0/qwen3-14b.png | Bin 0 -> 310129 bytes
 data/challenges.json                      |   3 +-
 public/games/dw-chinoiserie-studio.html   | 893 ++++++++++++++++++++++++++++++
 public/games/dw-parquet.html              | 733 ++++++++++++++++++++++++
 5 files changed, 1635 insertions(+), 1 deletion(-)

diff --git a/data/arcade-games.json b/data/arcade-games.json
index 3c16199..3f0957a 100644
--- a/data/arcade-games.json
+++ b/data/arcade-games.json
@@ -153,6 +153,13 @@
       "credit": "frontend-developer",
       "note": "Agent-built tool \u2014 custom stripe wallcovering designer, 6 styles, drag bands, room preview, seamless PNG",
       "category": "Tools"
+    },
+    {
+      "slug": "dw-parquet",
+      "title": "DW Parquet",
+      "credit": "frontend-developer",
+      "category": "Games",
+      "note": "Agent-built sliding puzzle \u2014 reassemble a herringbone/parquet wood-grain panel, 3x3/4x4/5x5, peek + timer"
     }
   ]
 }
\ No newline at end of file
diff --git a/data/artifacts/724633a205b0/qwen3-14b.png b/data/artifacts/724633a205b0/qwen3-14b.png
new file mode 100644
index 0000000..59a1674
Binary files /dev/null and b/data/artifacts/724633a205b0/qwen3-14b.png differ
diff --git a/data/challenges.json b/data/challenges.json
index 856e972..cda0c1b 100644
--- a/data/challenges.json
+++ b/data/challenges.json
@@ -14437,7 +14437,8 @@
         "started_at": "2026-07-25T07:35:21.628Z",
         "finished_at": "2026-07-25T07:35:48.331Z",
         "queued_at": "2026-07-25T07:27:36.977Z",
-        "bytes": 3574
+        "bytes": 3574,
+        "thumb": true
       },
       {
         "model": "gemma3-12b",
diff --git a/public/games/dw-chinoiserie-studio.html b/public/games/dw-chinoiserie-studio.html
new file mode 100644
index 0000000..1897fe0
--- /dev/null
+++ b/public/games/dw-chinoiserie-studio.html
@@ -0,0 +1,893 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Chinoiserie Studio — Designer Wallcoverings</title>
+<style>
+:root{
+  --bg:#f4f1ea;--bg2:#edeae0;--ink:#1c1a17;--ink2:#3a3630;
+  --accent:#c9a961;--accent2:#a8863e;--silk:#f9f6ef;
+  --serif:'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif;
+  --sans:'Optima','Gill Sans','Trebuchet MS',sans-serif;
+  --ease:cubic-bezier(.4,0,.2,1);
+  --radius:3px;
+  --panel-w:260px;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+html,body{height:100%;background:var(--bg);color:var(--ink);font-family:var(--sans)}
+header{
+  position:fixed;top:0;left:0;right:0;z-index:100;
+  background:var(--bg);border-bottom:1px solid rgba(201,169,97,.35);
+  display:flex;align-items:center;gap:16px;
+  padding:10px 20px;
+}
+.wordmark{
+  font-family:var(--serif);font-size:22px;font-weight:400;
+  text-transform:uppercase;letter-spacing:.18em;color:var(--ink);
+  white-space:nowrap;line-height:1.1;
+}
+.wordmark span{display:block;font-size:10px;letter-spacing:.25em;color:var(--accent2);margin-top:1px}
+.header-sep{flex:1}
+.header-btn{
+  background:none;border:1px solid var(--accent);color:var(--accent2);
+  font-family:var(--sans);font-size:11px;letter-spacing:.1em;
+  padding:5px 12px;border-radius:var(--radius);cursor:pointer;
+  transition:background .2s var(--ease),color .2s var(--ease);
+}
+.header-btn:hover{background:var(--accent);color:#fff}
+.header-btn.primary{background:var(--accent);color:#fff;border-color:var(--accent)}
+.header-btn.primary:hover{background:var(--accent2)}
+
+/* LAYOUT */
+#app{display:flex;height:100vh;padding-top:50px}
+#sidebar{
+  width:var(--panel-w);min-width:var(--panel-w);
+  background:var(--bg2);border-right:1px solid rgba(28,26,23,.1);
+  display:flex;flex-direction:column;overflow-y:auto;
+}
+#stage{flex:1;display:flex;align-items:center;justify-content:center;overflow:hidden;background:#2a2520;padding:20px}
+canvas#panel{display:none}/* hidden; SVG is primary */
+#svg-wrap{position:relative;user-select:none}
+#silk{display:block;box-shadow:0 8px 40px rgba(0,0,0,.45);cursor:crosshair}
+#overlay{position:absolute;top:0;left:0;pointer-events:none}
+
+/* SIDEBAR */
+.sb-section{padding:14px 14px 8px;border-bottom:1px solid rgba(28,26,23,.1)}
+.sb-label{font-size:9px;letter-spacing:.2em;text-transform:uppercase;color:var(--accent2);margin-bottom:8px}
+.grounds{display:flex;flex-wrap:wrap;gap:7px}
+.ground-swatch{
+  width:36px;height:36px;border-radius:50%;cursor:pointer;border:3px solid transparent;
+  transition:border-color .2s var(--ease),transform .15s var(--ease);
+}
+.ground-swatch:hover{transform:scale(1.12)}
+.ground-swatch.active{border-color:var(--accent)}
+.tools-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px}
+.tool-btn{
+  background:var(--bg);border:1px solid rgba(28,26,23,.15);
+  border-radius:var(--radius);padding:8px 6px 6px;
+  cursor:pointer;text-align:center;transition:all .15s var(--ease);
+  display:flex;flex-direction:column;align-items:center;gap:4px;
+}
+.tool-btn:hover{border-color:var(--accent);background:#fff}
+.tool-btn.active{border-color:var(--accent);background:rgba(201,169,97,.12);box-shadow:0 0 0 2px rgba(201,169,97,.25)}
+.tool-btn svg{display:block}
+.tool-name{font-size:9px;letter-spacing:.08em;color:var(--ink2);line-height:1.2}
+.actions{display:flex;flex-direction:column;gap:7px;padding:12px 14px}
+.act-btn{
+  width:100%;padding:8px 10px;font-family:var(--sans);font-size:11px;
+  letter-spacing:.06em;border-radius:var(--radius);cursor:pointer;
+  border:1px solid rgba(28,26,23,.2);background:var(--bg);color:var(--ink);
+  transition:all .15s var(--ease);display:flex;align-items:center;gap:7px;
+}
+.act-btn:hover{border-color:var(--accent);background:rgba(201,169,97,.08)}
+.act-btn.compose{background:var(--ink);color:var(--silk);border-color:var(--ink)}
+.act-btn.compose:hover{background:var(--ink2)}
+.act-btn span{font-size:15px;line-height:1}
+
+/* SELECTION HANDLES */
+.sel-handle{
+  position:absolute;width:10px;height:10px;background:#fff;
+  border:2px solid var(--accent);border-radius:50%;cursor:pointer;
+  transform:translate(-50%,-50%);
+}
+.sel-ring{
+  position:absolute;border:1.5px dashed var(--accent);pointer-events:none;
+  border-radius:2px;
+}
+
+/* TOAST */
+#toast{
+  position:fixed;bottom:24px;left:50%;transform:translateX(-50%) translateY(20px);
+  background:var(--ink);color:var(--silk);font-size:12px;letter-spacing:.08em;
+  padding:8px 20px;border-radius:20px;opacity:0;transition:all .3s var(--ease);
+  pointer-events:none;z-index:200;
+}
+#toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
+
+/* RESPONSIVE */
+@media(max-width:600px){
+  :root{--panel-w:200px}
+  .wordmark{font-size:14px}
+  #stage{padding:8px}
+}
+</style>
+</head>
+<body>
+
+<header>
+  <div class="wordmark">
+    Designer Wallcoverings
+    <span>Chinoiserie Studio</span>
+  </div>
+  <div class="header-sep"></div>
+  <button class="header-btn" id="undoBtn" title="Undo (Ctrl+Z)">&#8630; Undo</button>
+  <button class="header-btn" id="redoBtn" title="Redo (Ctrl+Y)">&#8631; Redo</button>
+  <button class="header-btn primary" id="dlBtn">&#8595; Export PNG</button>
+</header>
+
+<div id="app">
+  <div id="sidebar">
+
+    <div class="sb-section">
+      <div class="sb-label">Silk Ground</div>
+      <div class="grounds" id="grounds"></div>
+    </div>
+
+    <div class="sb-section">
+      <div class="sb-label">Elements — Click to select tool, then click canvas</div>
+      <div class="tools-grid" id="toolGrid"></div>
+    </div>
+
+    <div class="actions">
+      <button class="act-btn compose" id="composeBtn"><span>&#10041;</span>Compose for Me</button>
+      <button class="act-btn" id="clearBtn"><span>&#9633;</span>Clear Canvas</button>
+      <button class="act-btn" id="deleteSelBtn"><span>&#9003;</span>Delete Selected</button>
+      <button class="act-btn" id="bringFwdBtn"><span>&#11014;</span>Bring Forward</button>
+      <button class="act-btn" id="sendBkBtn"><span>&#11015;</span>Send Backward</button>
+    </div>
+
+  </div>
+
+  <div id="stage">
+    <div id="svg-wrap">
+      <svg id="silk" xmlns="http://www.w3.org/2000/svg"></svg>
+      <div id="overlay"></div>
+    </div>
+  </div>
+</div>
+
+<div id="toast"></div>
+
+<script>
+// ─── CONFIG ───────────────────────────────────────────────────────────────────
+const GROUNDS = [
+  {id:'celadon', label:'Celadon', color:'#a8c5b0', tint:'#7aa68a'},
+  {id:'blush',   label:'Blush',   color:'#e8c5b8', tint:'#c9906f'},
+  {id:'ink',     label:'Ink',     color:'#1d2535', tint:'#3a4a60'},
+  {id:'goldtea', label:'Gold Tea',color:'#d4b878', tint:'#a8883a'},
+  {id:'aubergine',label:'Aubergine',color:'#4a2d4e',tint:'#7a4d82'},
+];
+
+// ─── SVG ELEMENT LIBRARY (hand-painted chinoiserie style) ─────────────────────
+const ELEMENTS = {
+  blossom:{
+    label:'Blossom Branch',
+    w:160,h:180,
+    svg:(t)=>`
+    <g stroke="${t.ink}" stroke-linecap="round" fill="none">
+      <path d="M80,170 Q65,130 50,90 Q35,55 60,20" stroke-width="2.5"/>
+      <path d="M50,90 Q30,70 15,50" stroke-width="1.8"/>
+      <path d="M60,55 Q80,35 95,18" stroke-width="1.5"/>
+      <path d="M50,90 Q70,80 85,65" stroke-width="1.5"/>
+    </g>
+    ${blossom(t,50,30,10)}${blossom(t,62,45,9)}${blossom(t,95,18,8)}
+    ${blossom(t,18,52,9)}${blossom(t,30,38,7)}${blossom(t,82,65,8)}
+    ${petals(t,40,58,6)}${petals(t,72,30,5)}
+    `
+  },
+  plum:{
+    label:'Plum Branch',
+    w:150,h:170,
+    svg:(t)=>`
+    <g stroke="${t.ink}" stroke-linecap="round" fill="none">
+      <path d="M75,165 Q80,120 90,80 Q105,40 85,10" stroke-width="2.8"/>
+      <path d="M90,80 Q115,65 130,45" stroke-width="1.8"/>
+      <path d="M85,35 Q65,22 48,30" stroke-width="1.5"/>
+    </g>
+    ${plumFlower(t,83,10,10)}${plumFlower(t,45,30,9)}${plumFlower(t,130,45,8)}
+    ${plumFlower(t,115,65,7)}${plumFlower(t,95,52,9)}${plumFlower(t,60,70,7)}
+    `
+  },
+  songbird:{
+    label:'Songbird',
+    w:80,h:70,
+    svg:(t)=>`
+    <g fill="${t.accent}" opacity=".9">
+      <ellipse cx="40" cy="38" rx="22" ry="13"/>
+      <ellipse cx="57" cy="32" rx="13" ry="9"/>
+    </g>
+    <ellipse cx="40" cy="38" rx="22" ry="13" fill="${t.accent}" opacity=".2"/>
+    <g stroke="${t.ink}" fill="none" stroke-width="1">
+      <ellipse cx="40" cy="38" rx="22" ry="13"/>
+      <ellipse cx="57" cy="32" rx="13" ry="9"/>
+      <path d="M57,23 Q62,15 70,14 Q65,20 63,23"/>
+      <circle cx="65" cy="28" r="2" fill="${t.ink}"/>
+      <path d="M18,36 Q8,50 12,60 Q20,52 22,42"/>
+      <path d="M15,40 Q5,48 10,55"/>
+      <path d="M35,50 L30,62 M40,51 L38,63 M45,50 L43,62" stroke-width="1.2"/>
+    </g>
+    `
+  },
+  crane:{
+    label:'Crane',
+    w:120,h:160,
+    svg:(t)=>`
+    <g stroke="${t.ink}" fill="none" stroke-linecap="round">
+      <path d="M60,130 Q58,100 55,75 Q52,55 60,38" stroke-width="1.5"/>
+      <ellipse cx="60" cy="35" rx="14" ry="10" fill="white" stroke="${t.ink}" stroke-width="1.2"/>
+      <path d="M60,25 L65,15" stroke-width="1.8"/>
+      <rect x="63" y="12" width="9" height="3" rx="1" fill="${t.accent}" stroke="${t.accent}" stroke-width=".5"/>
+      <circle cx="68" cy="22" r="2.5" fill="${t.ink}"/>
+      <circle cx="57" cy="22" r="3.5" fill="#c0392b"/>
+      <path d="M55,75 Q30,65 10,80 Q25,72 38,80 Q50,75 55,75" fill="${t.silk}" stroke="${t.ink}" stroke-width="1"/>
+      <path d="M55,75 Q80,60 110,68 Q90,64 75,74 Q65,75 55,75" fill="${t.silk}" stroke="${t.ink}" stroke-width="1"/>
+      <path d="M58,130 L45,155 M60,130 L62,155 M62,130 L72,152" stroke-width="1.5"/>
+      <path d="M45,155 Q38,160 35,158 M62,155 Q64,162 62,160 M72,152 Q79,158 77,160" stroke-width="1.2"/>
+    </g>
+    `
+  },
+  butterfly:{
+    label:'Butterfly',
+    w:100,h:80,
+    svg:(t)=>`
+    <g opacity=".88">
+      <path d="M50,40 Q28,20 10,28 Q5,42 22,50 Q36,54 50,40Z" fill="${t.accent}" opacity=".7" stroke="${t.ink}" stroke-width=".8"/>
+      <path d="M50,40 Q72,20 90,28 Q95,42 78,50 Q64,54 50,40Z" fill="${t.accent}" opacity=".7" stroke="${t.ink}" stroke-width=".8"/>
+      <path d="M50,40 Q33,48 22,62 Q30,68 44,58 Q50,50 50,40Z" fill="${t.tint}" opacity=".55" stroke="${t.ink}" stroke-width=".8"/>
+      <path d="M50,40 Q67,48 78,62 Q70,68 56,58 Q50,50 50,40Z" fill="${t.tint}" opacity=".55" stroke="${t.ink}" stroke-width=".8"/>
+      <g fill="none" stroke="${t.ink}" stroke-width=".6" opacity=".6">
+        <path d="M50,40 Q35,30 18,33"/>
+        <path d="M50,40 Q65,30 82,33"/>
+        <path d="M50,40 Q40,52 32,60"/>
+        <path d="M50,40 Q60,52 68,60"/>
+      </g>
+      <ellipse cx="50" cy="42" rx="3" ry="10" fill="${t.ink}"/>
+      <path d="M50,32 Q45,22 42,16 M50,32 Q55,22 58,16" stroke="${t.ink}" stroke-width=".9" fill="none"/>
+    </g>
+    `
+  },
+  pagoda:{
+    label:'Pagoda',
+    w:110,h:170,
+    svg:(t)=>`
+    <g stroke="${t.ink}" fill="none" stroke-width="1">
+      <rect x="38" y="140" width="34" height="25" fill="${t.tint}" opacity=".3" stroke="${t.ink}"/>
+      <path d="M38,140 Q55,132 72,140" fill="${t.tint}" opacity=".4"/>
+      <rect x="30" y="108" width="50" height="34" fill="${t.tint}" opacity=".25"/>
+      <path d="M25,108 Q55,96 85,108" fill="${t.tint}" opacity=".35"/>
+      <path d="M22,108 L8,112 M88,108 L102,112" stroke-width=".8"/>
+      <rect x="20" y="72" width="70" height="38" fill="${t.tint}" opacity=".2"/>
+      <path d="M15,72 Q55,56 95,72" fill="${t.tint}" opacity=".3"/>
+      <path d="M12,72 L-2,76 M98,72 L112,76" stroke-width=".8"/>
+      <rect x="28" y="38" width="54" height="36" fill="${t.tint}" opacity=".18"/>
+      <path d="M22,38 Q55,22 88,38" fill="${t.tint}" opacity=".28"/>
+      <path d="M18,38 L5,42 M92,38 L105,42" stroke-width=".8"/>
+      <path d="M55,22 L55,8" stroke-width="1.5"/>
+      <path d="M48,12 Q55,4 62,12" fill="${t.accent}" opacity=".6"/>
+      <line x1="38" y1="148" x2="72" y2="148" stroke-width=".7"/>
+      <line x1="30" y1="122" x2="80" y2="122" stroke-width=".7"/>
+      <line x1="20" y1="90" x2="90" y2="90" stroke-width=".7"/>
+    </g>
+    `
+  },
+  rock:{
+    label:'Garden Rock',
+    w:100,h:90,
+    svg:(t)=>`
+    <g stroke="${t.ink}" stroke-width="1.2" fill="none">
+      <path d="M50,82 Q18,85 10,60 Q6,42 22,28 Q35,16 52,18 Q70,14 82,30 Q95,48 88,65 Q80,80 50,82Z" fill="${t.tint}" opacity=".22"/>
+      <path d="M50,82 Q18,85 10,60 Q6,42 22,28 Q35,16 52,18 Q70,14 82,30 Q95,48 88,65 Q80,80 50,82Z"/>
+      <path d="M22,28 Q32,22 45,25 Q55,20 68,25" stroke-width=".8" opacity=".6"/>
+      <path d="M10,60 Q18,48 15,38" stroke-width=".8" opacity=".5"/>
+      <path d="M88,65 Q84,52 90,42" stroke-width=".8" opacity=".5"/>
+      <path d="M35,82 Q28,70 32,55 Q38,42 50,38 Q62,42 68,55 Q72,70 65,82" stroke-width=".9" fill="${t.tint}" opacity=".18"/>
+      <circle cx="38" cy="48" r="3" fill="${t.tint}" opacity=".4"/>
+      <circle cx="62" cy="42" r="2" fill="${t.tint}" opacity=".4"/>
+    </g>
+    `
+  },
+  peony:{
+    label:'Peony',
+    w:110,h:110,
+    svg:(t)=>`
+    <g fill="${t.accent}" stroke="${t.ink}" stroke-width=".8">
+      ${peonyPetals(t)}
+    </g>
+    <g stroke="${t.ink}" fill="none" stroke-width="1">
+      <path d="M55,90 Q40,100 30,108"/>
+      <path d="M55,90 Q60,102 65,110"/>
+      <path d="M55,90 Q70,98 80,105"/>
+    </g>
+    `
+  },
+  bamboo:{
+    label:'Bamboo',
+    w:80,h:200,
+    svg:(t)=>`
+    <g stroke="${t.ink}" fill="none" stroke-linecap="round">
+      <rect x="28" y="10" width="14" height="42" rx="5" fill="${t.tint}" opacity=".3" stroke="${t.ink}" stroke-width="1"/>
+      <rect x="28" y="54" width="14" height="40" rx="4" fill="${t.tint}" opacity=".25" stroke="${t.ink}" stroke-width="1"/>
+      <rect x="28" y="96" width="14" height="44" rx="4" fill="${t.tint}" opacity=".2" stroke="${t.ink}" stroke-width="1"/>
+      <rect x="28" y="142" width="14" height="42" rx="4" fill="${t.tint}" opacity=".15" stroke="${t.ink}" stroke-width="1"/>
+      <line x1="28" y1="54" x2="42" y2="54" stroke-width="1.5"/>
+      <line x1="28" y1="96" x2="42" y2="96" stroke-width="1.5"/>
+      <line x1="28" y1="142" x2="42" y2="142" stroke-width="1.5"/>
+      <rect x="44" y="30" width="10" height="32" rx="4" fill="${t.tint}" opacity=".2" stroke="${t.ink}" stroke-width=".8"/>
+      <rect x="44" y="64" width="10" height="35" rx="4" fill="${t.tint}" opacity=".18" stroke="${t.ink}" stroke-width=".8"/>
+      <line x1="44" y1="64" x2="54" y2="64" stroke-width="1.2"/>
+      ${bambooLeaf(t,42,52,1)}${bambooLeaf(t,28,94,-1)}${bambooLeaf(t,42,138,1)}${bambooLeaf(t,28,54,-0.5)}
+    </g>
+    `
+  }
+};
+
+// ─── HELPER SVG PRIMITIVES ────────────────────────────────────────────────────
+function blossom(t,cx,cy,r){
+  const c=t.accent;
+  return `<g>
+    ${[0,72,144,216,288].map(a=>{
+      const rx=Math.cos(a*Math.PI/180)*r*1.1,ry=Math.sin(a*Math.PI/180)*r*1.1;
+      return `<ellipse cx="${cx+rx}" cy="${cy+ry}" rx="${r*.7}" ry="${r*.45}" fill="${c}" opacity=".75" stroke="${t.ink}" stroke-width=".6" transform="rotate(${a},${cx+rx},${cy+ry})"/>`;
+    }).join('')}
+    <circle cx="${cx}" cy="${cy}" r="${r*.35}" fill="#f0d070" stroke="${t.ink}" stroke-width=".5"/>
+  </g>`;
+}
+function petals(t,cx,cy,r){
+  return `<circle cx="${cx}" cy="${cy}" r="${r}" fill="${t.accent}" opacity=".5" stroke="${t.ink}" stroke-width=".7"/>
+  <circle cx="${cx}" cy="${cy}" r="${r*.4}" fill="#f5d060" stroke="${t.ink}" stroke-width=".4"/>`;
+}
+function plumFlower(t,cx,cy,r){
+  return [0,72,144,216,288].map(a=>{
+    const rx=Math.cos(a*Math.PI/180)*r,ry=Math.sin(a*Math.PI/180)*r;
+    return `<circle cx="${cx+rx}" cy="${cy+ry}" r="${r*.65}" fill="${t.accent}" opacity=".8" stroke="${t.ink}" stroke-width=".6"/>`;
+  }).join('')+`<circle cx="${cx}" cy="${cy}" r="${r*.4}" fill="#f0d880" stroke="${t.ink}" stroke-width=".4"/>`;
+}
+function peonyPetals(t){
+  let out='';
+  const layers=[
+    {count:5,rx:38,ry:24,dist:22,op:.7},
+    {count:7,rx:28,ry:18,dist:30,op:.8},
+    {count:9,rx:20,ry:13,dist:36,op:.85},
+    {count:6,rx:14,ry:10,dist:18,op:.9},
+  ];
+  layers.forEach(l=>{
+    for(let i=0;i<l.count;i++){
+      const a=(i/l.count)*360, rad=a*Math.PI/180;
+      const cx=55+Math.cos(rad)*l.dist, cy=55+Math.sin(rad)*l.dist;
+      out+=`<ellipse cx="${cx}" cy="${cy}" rx="${l.rx}" ry="${l.ry}" fill="${t.accent}" opacity="${l.op}" stroke="${t.ink}" stroke-width=".7" transform="rotate(${a},${cx},${cy})"/>`;
+    }
+  });
+  out+=`<circle cx="55" cy="55" r="12" fill="#f5e08a" opacity=".9" stroke="${t.ink}" stroke-width=".8"/>`;
+  out+=`<circle cx="55" cy="55" r="6" fill="#e8c850" stroke="${t.ink}" stroke-width=".5"/>`;
+  return out;
+}
+function bambooLeaf(t,x,y,side){
+  const w=22,s=side>0?1:-1;
+  return `<path d="M${x},${y} Q${x+s*w*.8},${y-12} ${x+s*w},${y-24} Q${x+s*w*.3},${y-10} ${x},${y}Z" fill="${t.tint}" opacity=".55" stroke="${t.ink}" stroke-width=".8"/>`;
+}
+
+// ─── GROUND TINT RESOLVER ─────────────────────────────────────────────────────
+function tintForGround(gid){
+  const g=GROUNDS.find(x=>x.id===gid)||GROUNDS[0];
+  const dark=g.id==='ink'||g.id==='aubergine';
+  return {
+    ink: dark?'rgba(255,255,255,.85)':'#1c1a17',
+    accent: dark?'#e8c87a':'#8b5c2a',
+    tint: dark?'rgba(255,255,255,.35)':g.tint,
+    silk: dark?'rgba(255,255,255,.9)':'#f9f6ef',
+  };
+}
+
+// ─── STATE ────────────────────────────────────────────────────────────────────
+let groundId='celadon';
+let activeTool=null;
+let items=[];       // {id,type,x,y,w,h,scale,rot,zIndex}
+let selectedId=null;
+let nextId=1;
+let nextZ=1;
+let history=[];     // snapshots of items
+let future=[];
+let svgW=700, svgH=900;
+
+// drag state
+let drag={active:false,id:null,startX:0,startY:0,origX:0,origY:0};
+// rotate state
+let rotating={active:false,id:null,startAngle:0,origRot:0};
+// scale state
+let scaling={active:false,id:null,startDist:0,origScale:1};
+
+// ─── DOM REFS ─────────────────────────────────────────────────────────────────
+const silk=document.getElementById('silk');
+const overlay=document.getElementById('overlay');
+const toast=document.getElementById('toast');
+
+// ─── INIT ─────────────────────────────────────────────────────────────────────
+function init(){
+  buildGrounds();
+  buildTools();
+  sizeSVG();
+  renderGround();
+  window.addEventListener('resize',onResize);
+  silk.addEventListener('click',onCanvasClick);
+  silk.addEventListener('mousedown',onCanvasMD);
+  window.addEventListener('mousemove',onMouseMove);
+  window.addEventListener('mouseup',onMouseUp);
+  window.addEventListener('keydown',onKey);
+  document.getElementById('undoBtn').addEventListener('click',undo);
+  document.getElementById('redoBtn').addEventListener('click',redo);
+  document.getElementById('dlBtn').addEventListener('click',exportPNG);
+  document.getElementById('composeBtn').addEventListener('click',compose);
+  document.getElementById('clearBtn').addEventListener('click',clearAll);
+  document.getElementById('deleteSelBtn').addEventListener('click',deleteSelected);
+  document.getElementById('bringFwdBtn').addEventListener('click',bringForward);
+  document.getElementById('sendBkBtn').addEventListener('click',sendBackward);
+  saveHistory();
+  showToast('Choose a tool, then click the canvas to place elements');
+}
+
+function sizeSVG(){
+  const stage=document.getElementById('stage');
+  const sw=stage.clientWidth-40, sh=stage.clientHeight-40;
+  const ratio=700/900;
+  if(sw/sh>ratio){svgH=Math.min(sh,900);svgW=Math.round(svgH*ratio);}
+  else{svgW=Math.min(sw,700);svgH=Math.round(svgW/ratio);}
+  silk.setAttribute('width',svgW);
+  silk.setAttribute('height',svgH);
+  silk.setAttribute('viewBox',`0 0 700 900`);
+  overlay.style.width=svgW+'px';
+  overlay.style.height=svgH+'px';
+}
+function onResize(){sizeSVG();renderAll();}
+
+// ─── GROUNDS ──────────────────────────────────────────────────────────────────
+function buildGrounds(){
+  const c=document.getElementById('grounds');
+  GROUNDS.forEach(g=>{
+    const d=document.createElement('div');
+    d.className='ground-swatch'+(g.id===groundId?' active':'');
+    d.title=g.label;
+    d.style.background=g.color;
+    d.addEventListener('click',()=>setGround(g.id));
+    c.appendChild(d);
+  });
+}
+function setGround(id){
+  groundId=id;
+  document.querySelectorAll('.ground-swatch').forEach((el,i)=>{
+    el.classList.toggle('active',GROUNDS[i].id===id);
+  });
+  renderAll();
+}
+
+// ─── TOOLS ───────────────────────────────────────────────────────────────────
+const TOOL_ICONS={
+  blossom:`<svg width="36" height="36" viewBox="0 0 36 36"><circle cx="18" cy="18" r="7" fill="#e8c87a" opacity=".7"/><circle cx="18" cy="8" r="4" fill="#e8c87a" opacity=".6"/><circle cx="26" cy="13" r="4" fill="#e8c87a" opacity=".6"/><circle cx="26" cy="23" r="4" fill="#e8c87a" opacity=".6"/><circle cx="18" cy="28" r="4" fill="#e8c87a" opacity=".6"/><circle cx="10" cy="23" r="4" fill="#e8c87a" opacity=".6"/><circle cx="10" cy="13" r="4" fill="#e8c87a" opacity=".6"/></svg>`,
+  plum:`<svg width="36" height="36" viewBox="0 0 36 36"><path d="M18,32 Q14,20 16,8" stroke="#5a4030" fill="none" stroke-width="2"/><circle cx="16" cy="8" r="4" fill="#e8a0a0" opacity=".8"/><circle cx="10" cy="14" r="3.5" fill="#e8a0a0" opacity=".8"/><circle cx="25" cy="12" r="3.5" fill="#e8a0a0" opacity=".8"/></svg>`,
+  songbird:`<svg width="36" height="36" viewBox="0 0 36 36"><ellipse cx="16" cy="22" rx="10" ry="6" fill="#e8c87a" opacity=".8"/><ellipse cx="25" cy="18" rx="7" ry="5" fill="#e8c87a" opacity=".8"/><path d="M25,13 Q29,9 33,9" stroke="#3a3020" fill="none" stroke-width="1.2"/><circle cx="29" cy="15" r="1.5" fill="#1c1a17"/></svg>`,
+  crane:`<svg width="36" height="36" viewBox="0 0 36 36"><ellipse cx="18" cy="22" rx="12" ry="5" fill="white" stroke="#3a3020" stroke-width=".8"/><ellipse cx="18" cy="14" rx="5" ry="4" fill="white" stroke="#3a3020" stroke-width=".8"/><path d="M18,10 L20,7" stroke="#3a3020" stroke-width="1.2"/><rect x="18" y="6" width="5" height="2" rx="1" fill="#c9a961"/><circle cx="18" cy="11" r="1.5" fill="#c0392b"/><path d="M16,27 L14,33 M18,27 L18,33 M20,27 L22,33" stroke="#3a3020" stroke-width="1"/></svg>`,
+  butterfly:`<svg width="36" height="36" viewBox="0 0 36 36"><path d="M18,18 Q8,8 2,12 Q1,20 10,22 Q14,23 18,18Z" fill="#e8c87a" opacity=".75"/><path d="M18,18 Q28,8 34,12 Q35,20 26,22 Q22,23 18,18Z" fill="#e8c87a" opacity=".75"/><path d="M18,18 Q13,22 9,28 Q14,30 18,24Z" fill="#c9a961" opacity=".6"/><path d="M18,18 Q23,22 27,28 Q22,30 18,24Z" fill="#c9a961" opacity=".6"/><ellipse cx="18" cy="20" rx="1.5" ry="5" fill="#1c1a17"/></svg>`,
+  pagoda:`<svg width="36" height="36" viewBox="0 0 36 36"><rect x="12" y="28" width="12" height="7" fill="#c9a961" opacity=".4" stroke="#5a4030" stroke-width=".8"/><path d="M10,28 Q18,22 26,28" fill="#c9a961" opacity=".5"/><rect x="8" y="18" width="20" height="11" fill="#c9a961" opacity=".3" stroke="#5a4030" stroke-width=".8"/><path d="M5,18 Q18,11 31,18" fill="#c9a961" opacity=".4"/><path d="M18,11 L18,5" stroke="#5a4030" stroke-width="1.2"/></svg>`,
+  rock:`<svg width="36" height="36" viewBox="0 0 36 36"><path d="M18,32 Q6,33 4,22 Q3,14 10,8 Q18,4 26,8 Q34,14 33,24 Q31,32 18,32Z" fill="#b0a898" opacity=".5" stroke="#5a4030" stroke-width=".8"/></svg>`,
+  peony:`<svg width="36" height="36" viewBox="0 0 36 36">${[0,60,120,180,240,300].map(a=>`<ellipse cx="${18+Math.cos(a*Math.PI/180)*10}" cy="${18+Math.sin(a*Math.PI/180)*10}" rx="9" ry="5.5" fill="#e8c87a" opacity=".7" stroke="#5a4030" stroke-width=".5" transform="rotate(${a},${18+Math.cos(a*Math.PI/180)*10},${18+Math.sin(a*Math.PI/180)*10})"/>`).join('')}<circle cx="18" cy="18" r="5" fill="#f5e080" stroke="#5a4030" stroke-width=".6"/></svg>`,
+  bamboo:`<svg width="36" height="36" viewBox="0 0 36 36"><rect x="14" y="2" width="7" height="14" rx="3" fill="#8db878" opacity=".6" stroke="#3a5028" stroke-width=".8"/><rect x="14" y="17" width="7" height="14" rx="3" fill="#8db878" opacity=".5" stroke="#3a5028" stroke-width=".8"/><line x1="14" y1="17" x2="21" y2="17" stroke="#3a5028" stroke-width="1.2"/><path d="M21,14 Q30,10 33,6" stroke="#3a5028" fill="none" stroke-width=".9"/></svg>`,
+};
+function buildTools(){
+  const g=document.getElementById('toolGrid');
+  Object.keys(ELEMENTS).forEach(k=>{
+    const el=ELEMENTS[k];
+    const btn=document.createElement('button');
+    btn.className='tool-btn';
+    btn.dataset.tool=k;
+    btn.innerHTML=`${TOOL_ICONS[k]||''}<span class="tool-name">${el.label}</span>`;
+    btn.addEventListener('click',()=>selectTool(k));
+    g.appendChild(btn);
+  });
+}
+function selectTool(k){
+  activeTool=activeTool===k?null:k;
+  document.querySelectorAll('.tool-btn').forEach(b=>{
+    b.classList.toggle('active',b.dataset.tool===activeTool);
+  });
+  silk.style.cursor=activeTool?'crosshair':'default';
+  if(activeTool) selectedId=null, renderSelection();
+}
+
+// ─── CANVAS EVENTS ────────────────────────────────────────────────────────────
+function onCanvasClick(e){
+  if(drag.moved) return;
+  if(!activeTool) return;
+  const pt=svgPoint(e);
+  saveHistory();
+  const el=ELEMENTS[activeTool];
+  const item={
+    id:nextId++,type:activeTool,
+    x:pt.x,y:pt.y,
+    w:el.w,h:el.h,
+    scale:1,rot:0,
+    zIndex:nextZ++
+  };
+  items.push(item);
+  renderAll();
+  animatePop(item.id);
+  future=[];
+}
+function onCanvasMD(e){
+  drag.moved=false;
+  if(activeTool) return;
+  const pt=svgPoint(e);
+  // hit test items (top-most z first)
+  const sorted=[...items].sort((a,b)=>b.zIndex-a.zIndex);
+  for(const item of sorted){
+    if(hitTest(item,pt)){
+      selectedId=item.id;
+      drag={active:true,id:item.id,startX:e.clientX,startY:e.clientY,origX:item.x,origY:item.y,moved:false};
+      renderAll();
+      return;
+    }
+  }
+  selectedId=null;
+  renderAll();
+}
+function onMouseMove(e){
+  if(!drag.active) return;
+  const dx=(e.clientX-drag.startX)*(700/svgW);
+  const dy=(e.clientY-drag.startY)*(900/svgH);
+  if(Math.abs(dx)+Math.abs(dy)>3) drag.moved=true;
+  const item=items.find(i=>i.id===drag.id);
+  if(item){item.x=drag.origX+dx;item.y=drag.origY+dy;renderAll();}
+}
+function onMouseUp(e){
+  if(drag.active && drag.moved) saveHistory();
+  drag.active=false;
+}
+function onKey(e){
+  if(e.key==='Escape'){activeTool=null;document.querySelectorAll('.tool-btn').forEach(b=>b.classList.remove('active'));silk.style.cursor='default';}
+  if((e.ctrlKey||e.metaKey)&&e.key==='z'){e.preventDefault();undo();}
+  if((e.ctrlKey||e.metaKey)&&(e.key==='y'||(e.shiftKey&&e.key==='Z'))){e.preventDefault();redo();}
+  if(e.key==='Delete'||e.key==='Backspace') deleteSelected();
+  if(selectedId&&e.key===']') bringForward();
+  if(selectedId&&e.key==='[') sendBackward();
+  if(selectedId&&(e.key==='+')){ const i=items.find(x=>x.id===selectedId);if(i){saveHistory();i.scale=Math.min(4,i.scale*1.1);renderAll();}}
+  if(selectedId&&e.key==='-'){ const i=items.find(x=>x.id===selectedId);if(i){saveHistory();i.scale=Math.max(.15,i.scale*.9);renderAll();}}
+  if(selectedId&&e.key==='ArrowLeft'){ const i=items.find(x=>x.id===selectedId);if(i){saveHistory();i.rot-=5;renderAll();}}
+  if(selectedId&&e.key==='ArrowRight'){ const i=items.find(x=>x.id===selectedId);if(i){saveHistory();i.rot+=5;renderAll();}}
+}
+
+function svgPoint(e){
+  const r=silk.getBoundingClientRect();
+  return {x:(e.clientX-r.left)*(700/svgW), y:(e.clientY-r.top)*(900/svgH)};
+}
+function hitTest(item,pt){
+  const hw=item.w*item.scale/2, hh=item.h*item.scale/2;
+  return pt.x>=item.x-hw&&pt.x<=item.x+hw&&pt.y>=item.y-hh&&pt.y<=item.y+hh;
+}
+
+// ─── RENDER ───────────────────────────────────────────────────────────────────
+function renderAll(){
+  renderGround();
+  renderItems();
+  renderSelection();
+}
+
+function renderGround(){
+  const g=GROUNDS.find(x=>x.id===groundId)||GROUNDS[0];
+  const dark=groundId==='ink'||groundId==='aubergine';
+
+  // Build SVG defs once
+  silk.innerHTML=`
+  <defs>
+    <filter id="silk-tex" x="0%" y="0%" width="100%" height="100%" color-interpolation-filters="sRGB">
+      <feTurbulence type="fractalNoise" baseFrequency=".65 .3" numOctaves="4" seed="3" result="noise"/>
+      <feColorMatrix type="saturate" values="0" in="noise" result="gray"/>
+      <feBlend in="SourceGraphic" in2="gray" mode="multiply" result="blend"/>
+      <feComponentTransfer in="blend"><feFuncA type="linear" slope="1"/></feComponentTransfer>
+    </filter>
+    <filter id="aged" x="-5%" y="-5%" width="110%" height="110%">
+      <feTurbulence type="turbulence" baseFrequency=".04 .08" numOctaves="3" seed="7" result="t"/>
+      <feDisplacementMap in="SourceGraphic" in2="t" scale="4" xChannelSelector="R" yChannelSelector="G"/>
+    </filter>
+    <pattern id="silk-weave" x="0" y="0" width="6" height="6" patternUnits="userSpaceOnUse">
+      <rect width="6" height="6" fill="${g.color}"/>
+      <line x1="0" y1="0" x2="6" y2="0" stroke="${dark?'rgba(255,255,255,.03)':'rgba(0,0,0,.03)'}" stroke-width="1"/>
+      <line x1="0" y1="3" x2="6" y2="3" stroke="${dark?'rgba(255,255,255,.025)':'rgba(0,0,0,.025)'}" stroke-width=".5"/>
+      <line x1="0" y1="0" x2="0" y2="6" stroke="${dark?'rgba(255,255,255,.02)':'rgba(0,0,0,.02)'}" stroke-width=".5"/>
+      <line x1="3" y1="0" x2="3" y2="6" stroke="${dark?'rgba(255,255,255,.015)':'rgba(0,0,0,.015)'}" stroke-width=".5"/>
+    </pattern>
+    <radialGradient id="vignette" cx="50%" cy="50%" r="70%">
+      <stop offset="0%" stop-color="transparent"/>
+      <stop offset="100%" stop-color="${dark?'rgba(0,0,0,.35)':'rgba(0,0,0,.12)'}"/>
+    </radialGradient>
+  </defs>
+  <!-- Ground -->
+  <rect width="700" height="900" fill="url(#silk-weave)"/>
+  <!-- Aged texture overlay -->
+  <rect width="700" height="900" fill="${dark?'rgba(255,255,255,.03)':'rgba(180,160,120,.08)'}" filter="url(#silk-tex)"/>
+  <!-- Vignette -->
+  <rect width="700" height="900" fill="url(#vignette)"/>
+  <!-- Border -->
+  <rect x="18" y="18" width="664" height="864" fill="none" stroke="${dark?'rgba(255,255,255,.12)':'rgba(28,26,23,.12)'}" stroke-width="1.5"/>
+  <rect x="24" y="24" width="652" height="852" fill="none" stroke="${dark?'rgba(255,255,255,.07)':'rgba(28,26,23,.07)'}" stroke-width=".8"/>
+  <g id="items-group"></g>
+  `;
+
+  // Transition color smoothly via CSS
+  silk.style.transition='background .4s ease';
+  silk.style.background=g.color;
+}
+
+function renderItems(){
+  const g=document.getElementById('items-group');
+  if(!g) return;
+  const tint=tintForGround(groundId);
+  const sorted=[...items].sort((a,b)=>a.zIndex-b.zIndex);
+  g.innerHTML=sorted.map(item=>{
+    const el=ELEMENTS[item.type];
+    const s=item.scale, cx=item.x, cy=item.y;
+    const sel=item.id===selectedId;
+    return `<g id="item-${item.id}" transform="translate(${cx},${cy}) rotate(${item.rot}) scale(${s})"
+      style="cursor:move;outline:none"
+      data-id="${item.id}">
+      ${sel?`<rect x="${-el.w/2-4}" y="${-el.h/2-4}" width="${el.w+8}" height="${el.h+8}" fill="none" stroke="${tint.accent}" stroke-width="${1/s}" stroke-dasharray="${4/s} ${3/s}" rx="2" opacity=".7"/>`:``}
+      <g transform="translate(${-el.w/2},${-el.h/2})">${el.svg(tint)}</g>
+    </g>`;
+  }).join('');
+}
+
+function renderSelection(){
+  overlay.innerHTML='';
+  if(!selectedId) return;
+  const item=items.find(i=>i.id===selectedId);
+  if(!item) return;
+  // Show control handles in overlay space
+  const scaleX=svgW/700, scaleY=svgH/900;
+  const cx=item.x*scaleX, cy=item.y*scaleY;
+  const hw=item.w*item.scale/2*scaleX, hh=item.h*item.scale/2*scaleY;
+
+  // Scale handle (bottom-right)
+  const sh=makeHandle(cx+hw,cy+hh,'nwse-resize');
+  sh.title='Drag to scale';
+  sh.addEventListener('mousedown',e=>{
+    e.stopPropagation();
+    saveHistory();
+    const startDist=Math.hypot(e.clientX-cx*1,e.clientY-cy*1);
+    const origScale=item.scale;
+    const mm=ev=>{
+      const r=silk.getBoundingClientRect();
+      const d=Math.hypot(ev.clientX-r.left-item.x*scaleX, ev.clientY-r.top-item.y*scaleY);
+      item.scale=Math.max(.15,Math.min(4,origScale*(d/(hw/scaleX*origScale||1))));
+      renderAll();
+    };
+    const mu=()=>{window.removeEventListener('mousemove',mm);window.removeEventListener('mouseup',mu);};
+    window.addEventListener('mousemove',mm);
+    window.addEventListener('mouseup',mu);
+  });
+
+  // Rotate handle (top-right)
+  const rh=makeHandle(cx+hw,cy-hh,'grab');
+  rh.title='Drag to rotate';
+  rh.style.background='#a8863e';
+  rh.addEventListener('mousedown',e=>{
+    e.stopPropagation();
+    saveHistory();
+    const origRot=item.rot;
+    const r=silk.getBoundingClientRect();
+    const startAngle=Math.atan2(e.clientY-r.top-item.y*scaleY, e.clientX-r.left-item.x*scaleX)*180/Math.PI;
+    const mm=ev=>{
+      const ang=Math.atan2(ev.clientY-r.top-item.y*scaleY, ev.clientX-r.left-item.x*scaleX)*180/Math.PI;
+      item.rot=origRot+(ang-startAngle);
+      renderAll();
+    };
+    const mu=()=>{window.removeEventListener('mousemove',mm);window.removeEventListener('mouseup',mu);};
+    window.addEventListener('mousemove',mm);
+    window.addEventListener('mouseup',mu);
+  });
+
+  overlay.appendChild(sh);
+  overlay.appendChild(rh);
+
+  // Label
+  const lbl=document.createElement('div');
+  lbl.style.cssText=`position:absolute;left:${cx-hw}px;top:${cy-hh-18}px;font-size:10px;color:var(--accent);letter-spacing:.08em;pointer-events:none;white-space:nowrap`;
+  lbl.textContent=ELEMENTS[item.type]?.label||'';
+  overlay.appendChild(lbl);
+}
+
+function makeHandle(x,y,cursor){
+  const h=document.createElement('div');
+  h.className='sel-handle';
+  h.style.cssText=`left:${x}px;top:${y}px;cursor:${cursor}`;
+  return h;
+}
+
+// ─── POP ANIMATION ───────────────────────────────────────────────────────────
+function animatePop(id){
+  // Quick scale keyframe via JS
+  let t=0;
+  const tick=()=>{
+    t+=.06;
+    if(t>1) return;
+    const scale=1+Math.sin(t*Math.PI)*.18;
+    const item=items.find(i=>i.id===id);
+    if(!item) return;
+    const el=document.getElementById('item-'+id);
+    if(el){const base=item.scale;el.setAttribute('transform',`translate(${item.x},${item.y}) rotate(${item.rot}) scale(${base*scale})`);}
+    requestAnimationFrame(tick);
+  };
+  requestAnimationFrame(tick);
+}
+
+// ─── COMPOSE ──────────────────────────────────────────────────────────────────
+function compose(){
+  saveHistory();
+  items=[];
+  nextZ=1;
+  selectedId=null;
+
+  // Curated balanced scene
+  const scene=[
+    // Background elements (large, lower z)
+    {type:'pagoda', x:540, y:720, scale:.85, rot:-3},
+    {type:'rock',   x:120, y:820, scale:1.1, rot:5},
+    {type:'rock',   x:580, y:850, scale:.8,  rot:-8},
+    // Mid-ground branches
+    {type:'blossom',x:180, y:320, scale:1.1, rot:8},
+    {type:'plum',   x:520, y:280, scale:1.0, rot:-12},
+    {type:'bamboo', x:80,  y:450, scale:1.0, rot:3},
+    {type:'bamboo', x:630, y:550, scale:.85, rot:-4},
+    // Flowers
+    {type:'peony',  x:340, y:580, scale:.9,  rot:0},
+    {type:'peony',  x:160, y:650, scale:.75, rot:15},
+    // Birds
+    {type:'crane',  x:350, y:200, scale:1.0, rot:0},
+    {type:'songbird',x:240, y:390, scale:.9, rot:-5},
+    {type:'songbird',x:470, y:440, scale:.8, rot:10},
+    // Butterflies
+    {type:'butterfly',x:420, y:160, scale:.85, rot:-20},
+    {type:'butterfly',x:130, y:200, scale:.75, rot:15},
+  ];
+
+  scene.forEach(s=>{
+    const el=ELEMENTS[s.type];
+    items.push({
+      id:nextId++,
+      type:s.type,
+      x:s.x,y:s.y,
+      w:el.w,h:el.h,
+      scale:s.scale,
+      rot:s.rot,
+      zIndex:nextZ++
+    });
+  });
+  future=[];
+  renderAll();
+  showToast('Scene composed — click any element to select and adjust');
+}
+
+// ─── HISTORY ─────────────────────────────────────────────────────────────────
+function saveHistory(){
+  history.push(JSON.stringify(items));
+  if(history.length>50) history.shift();
+}
+function undo(){
+  if(!history.length) return;
+  future.push(JSON.stringify(items));
+  items=JSON.parse(history.pop());
+  selectedId=null;
+  renderAll();
+  showToast('Undo');
+}
+function redo(){
+  if(!future.length) return;
+  history.push(JSON.stringify(items));
+  items=JSON.parse(future.pop());
+  selectedId=null;
+  renderAll();
+  showToast('Redo');
+}
+
+// ─── ITEM OPS ─────────────────────────────────────────────────────────────────
+function deleteSelected(){
+  if(!selectedId) return;
+  saveHistory();
+  items=items.filter(i=>i.id!==selectedId);
+  selectedId=null;
+  renderAll();
+  future=[];
+}
+function clearAll(){
+  saveHistory();
+  items=[];selectedId=null;future=[];
+  renderAll();
+  showToast('Canvas cleared');
+}
+function bringForward(){
+  if(!selectedId) return;
+  const item=items.find(i=>i.id===selectedId);
+  if(!item) return;
+  saveHistory();
+  item.zIndex=nextZ++;
+  renderAll();future=[];
+}
+function sendBackward(){
+  if(!selectedId) return;
+  const item=items.find(i=>i.id===selectedId);
+  if(!item) return;
+  saveHistory();
+  const minZ=Math.min(...items.map(i=>i.zIndex));
+  item.zIndex=minZ-1;
+  renderAll();future=[];
+}
+
+// ─── EXPORT PNG ───────────────────────────────────────────────────────────────
+function exportPNG(){
+  selectedId=null;renderAll();
+  // Serialize SVG → canvas → data URI
+  const svgEl=document.getElementById('silk');
+  const serializer=new XMLSerializer();
+  const svgStr=serializer.serializeToString(svgEl);
+  const blob='data:image/svg+xml;charset=utf-8,'+encodeURIComponent(svgStr);
+  const img=new Image();
+  img.onload=()=>{
+    const c=document.createElement('canvas');
+    c.width=700;c.height=900;
+    const ctx=c.getContext('2d');
+    ctx.drawImage(img,0,0,700,900);
+    const url=c.toDataURL('image/png');
+    const a=document.createElement('a');
+    a.href=url;
+    a.download='chinoiserie-mural-dw.png';
+    a.click();
+    showToast('Mural exported as PNG');
+  };
+  img.onerror=()=>showToast('Export error — try a different browser');
+  img.src=blob;
+}
+
+// ─── TOAST ───────────────────────────────────────────────────────────────────
+let toastTimer=null;
+function showToast(msg){
+  toast.textContent=msg;
+  toast.classList.add('show');
+  clearTimeout(toastTimer);
+  toastTimer=setTimeout(()=>toast.classList.remove('show'),3000);
+}
+
+// ─── TOUCH SUPPORT ────────────────────────────────────────────────────────────
+silk.addEventListener('touchstart',e=>{
+  if(e.touches.length===1){
+    const t=e.touches[0];
+    const me=new MouseEvent('mousedown',{clientX:t.clientX,clientY:t.clientY,bubbles:true});
+    silk.dispatchEvent(me);
+  }
+},{passive:true});
+window.addEventListener('touchmove',e=>{
+  if(e.touches.length===1){
+    const t=e.touches[0];
+    onMouseMove({clientX:t.clientX,clientY:t.clientY});
+  }
+},{passive:true});
+window.addEventListener('touchend',()=>onMouseUp({}),{passive:true});
+silk.addEventListener('touchend',e=>{
+  if(!drag.moved){
+    const t=e.changedTouches[0];
+    onCanvasClick({clientX:t.clientX,clientY:t.clientY,target:silk});
+  }
+  drag.active=false;
+},{passive:true});
+
+// ─── START ────────────────────────────────────────────────────────────────────
+init();
+</script>
+</body>
+</html>
diff --git a/public/games/dw-parquet.html b/public/games/dw-parquet.html
new file mode 100644
index 0000000..b3c3088
--- /dev/null
+++ b/public/games/dw-parquet.html
@@ -0,0 +1,733 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Parquet — Designer Wallcoverings</title>
+<style>
+:root{
+  --bg:#0d0b09;
+  --surface:#1a1710;
+  --surface2:#252118;
+  --ink:#f4f1ea;
+  --ink-dim:#9a8f7a;
+  --accent:#c9a961;
+  --accent-dim:#8a6f35;
+  --accent-glow:rgba(201,169,97,.22);
+  --serif:'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif;
+  --sans:'Inter','Helvetica Neue',Arial,sans-serif;
+  --ease:cubic-bezier(.4,0,.2,1);
+  --ease-spring:cubic-bezier(.34,1.56,.64,1);
+  --r:10px;
+  --tile-gap:3px;
+}
+*,*::before,*::after{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{
+  display:flex;align-items:center;justify-content:space-between;
+  padding:16px 28px 14px;
+  border-bottom:1px solid rgba(201,169,97,.15);
+}
+.wordmark{
+  font-family:var(--serif);
+  font-size:clamp(18px,2.5vw,28px);
+  letter-spacing:.18em;
+  text-transform:uppercase;
+  color:var(--ink);
+  line-height:1;
+}
+.wordmark span{color:var(--accent)}
+.header-sub{
+  font-size:10px;letter-spacing:.25em;text-transform:uppercase;
+  color:var(--ink-dim);margin-top:3px;
+}
+.hdr-controls{display:flex;gap:10px;align-items:center}
+button.ghost{
+  background:none;border:1px solid rgba(201,169,97,.3);
+  color:var(--accent);font-size:11px;letter-spacing:.12em;text-transform:uppercase;
+  font-family:var(--sans);padding:6px 14px;border-radius:4px;cursor:pointer;
+  transition:background .2s,border-color .2s;
+}
+button.ghost:hover{background:var(--accent-glow);border-color:var(--accent)}
+
+/* ── LAYOUT ── */
+main{
+  display:flex;flex-direction:column;align-items:center;
+  padding:28px 16px 40px;min-height:calc(100vh - 65px);gap:20px;
+}
+.game-title{
+  font-family:var(--serif);font-size:clamp(26px,4vw,44px);
+  letter-spacing:.08em;color:var(--accent);text-align:center;line-height:1;
+}
+.game-subtitle{
+  font-size:11px;letter-spacing:.22em;text-transform:uppercase;
+  color:var(--ink-dim);text-align:center;margin-top:4px;
+}
+
+/* ── HUD ── */
+.hud{
+  display:flex;gap:28px;align-items:center;
+  background:var(--surface);border:1px solid rgba(201,169,97,.15);
+  border-radius:var(--r);padding:10px 24px;
+}
+.hud-cell{text-align:center}
+.hud-val{
+  font-family:var(--serif);font-size:28px;color:var(--accent);
+  line-height:1;letter-spacing:.04em;
+}
+.hud-lbl{font-size:9px;letter-spacing:.2em;text-transform:uppercase;color:var(--ink-dim);margin-top:2px}
+.hud-sep{width:1px;height:36px;background:rgba(201,169,97,.18)}
+
+/* ── SIZE PICKER ── */
+.size-row{display:flex;gap:8px}
+.sz-btn{
+  background:none;border:1px solid rgba(201,169,97,.25);
+  color:var(--ink-dim);font-size:11px;letter-spacing:.1em;
+  font-family:var(--sans);padding:5px 14px;border-radius:4px;cursor:pointer;
+  transition:all .2s;
+}
+.sz-btn:hover,.sz-btn.active{
+  background:var(--accent-glow);border-color:var(--accent);color:var(--accent);
+}
+
+/* ── BOARD ── */
+.board-wrap{position:relative}
+#board{
+  display:grid;
+  gap:var(--tile-gap);
+  border:1px solid rgba(201,169,97,.25);
+  border-radius:var(--r);
+  padding:var(--tile-gap);
+  background:rgba(201,169,97,.08);
+  /* set via JS: grid-template-columns */
+  touch-action:none;
+  user-select:none;
+}
+.tile{
+  border-radius:5px;
+  overflow:hidden;
+  cursor:pointer;
+  position:relative;
+  transition:transform .15s var(--ease),box-shadow .15s;
+  will-change:transform;
+}
+.tile:hover{
+  transform:scale(1.025);
+  box-shadow:0 0 0 2px var(--accent);
+  z-index:2;
+}
+.tile.empty{
+  background:rgba(0,0,0,.55);
+  cursor:default;border:1px dashed rgba(201,169,97,.12);
+}
+.tile.empty:hover{transform:none;box-shadow:none}
+.tile svg{display:block;width:100%;height:100%}
+
+/* number badge */
+.tile-num{
+  position:absolute;bottom:3px;right:5px;
+  font-size:9px;color:rgba(255,255,255,.28);font-family:var(--sans);
+  pointer-events:none;display:none;
+}
+.show-nums .tile-num{display:block}
+
+/* ── PEEK PREVIEW ── */
+.peek-wrap{position:relative;cursor:pointer}
+.peek-label{
+  font-size:9px;letter-spacing:.18em;text-transform:uppercase;
+  color:var(--ink-dim);text-align:center;margin-bottom:6px;
+}
+#peek-svg{
+  border:1px solid rgba(201,169,97,.2);border-radius:6px;
+  opacity:.35;transition:opacity .25s;display:block;
+}
+.peek-wrap:hover #peek-svg{opacity:.9}
+.peek-hint{
+  font-size:9px;color:var(--ink-dim);text-align:center;margin-top:4px;letter-spacing:.08em;
+}
+
+/* ── ACTION ROW ── */
+.actions{display:flex;gap:10px;flex-wrap:wrap;justify-content:center}
+button.primary{
+  background:linear-gradient(135deg,#b8922d,#c9a961 60%,#e0c07a);
+  border:none;color:#1c1a17;
+  font-family:var(--serif);font-size:15px;letter-spacing:.12em;
+  padding:11px 30px;border-radius:5px;cursor:pointer;
+  text-transform:uppercase;
+  box-shadow:0 2px 18px rgba(201,169,97,.25);
+  transition:filter .2s,transform .12s;
+}
+button.primary:hover{filter:brightness(1.1);transform:translateY(-1px)}
+button.primary:active{transform:translateY(0)}
+
+/* ── WIN OVERLAY ── */
+#win-overlay{
+  position:fixed;inset:0;background:rgba(13,11,9,.88);
+  display:flex;flex-direction:column;align-items:center;justify-content:center;
+  gap:20px;z-index:100;
+  visibility:hidden;opacity:0;
+  transition:opacity .4s var(--ease),visibility .4s;
+}
+#win-overlay.show{visibility:visible;opacity:1}
+.win-art{border-radius:12px;border:2px solid var(--accent);box-shadow:0 0 60px var(--accent-glow)}
+.win-title{
+  font-family:var(--serif);font-size:clamp(32px,6vw,60px);
+  color:var(--accent);letter-spacing:.12em;text-transform:uppercase;
+  animation:shimmer 2s linear infinite;
+}
+@keyframes shimmer{
+  0%{text-shadow:0 0 20px rgba(201,169,97,0)}
+  50%{text-shadow:0 0 40px rgba(201,169,97,.7)}
+  100%{text-shadow:0 0 20px rgba(201,169,97,0)}
+}
+.win-stats{display:flex;gap:32px;font-family:var(--serif)}
+.win-stat{text-align:center}
+.win-stat-v{font-size:36px;color:var(--accent);line-height:1}
+.win-stat-l{font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-dim)}
+.win-sub{font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--ink-dim);text-align:center}
+.win-buttons{display:flex;gap:12px}
+
+/* ── CONFETTI ── */
+.confetti-piece{
+  position:fixed;pointer-events:none;z-index:200;
+  width:8px;height:8px;border-radius:1px;
+  animation:fall linear forwards;
+}
+@keyframes fall{
+  0%{transform:translateY(-40px) rotate(0deg);opacity:1}
+  100%{transform:translateY(110vh) rotate(720deg);opacity:0}
+}
+
+/* ── RESPONSIVE ── */
+@media(max-width:600px){
+  header{padding:12px 16px 10px}
+  .hud{gap:16px;padding:8px 16px}
+  .hud-val{font-size:22px}
+}
+</style>
+</head>
+<body>
+
+<header>
+  <div>
+    <div class="wordmark">Designer <span>Wallcoverings</span></div>
+    <div class="header-sub">Parquet — Sliding Puzzle</div>
+  </div>
+  <div class="hdr-controls">
+    <button class="ghost" id="mute-btn" title="Toggle sound">♪ Sound</button>
+  </div>
+</header>
+
+<main>
+  <div>
+    <div class="game-title">Parquet</div>
+    <div class="game-subtitle">Restore the herringbone panel</div>
+  </div>
+
+  <div class="hud">
+    <div class="hud-cell">
+      <div class="hud-val" id="moves-display">0</div>
+      <div class="hud-lbl">Moves</div>
+    </div>
+    <div class="hud-sep"></div>
+    <div class="hud-cell">
+      <div class="hud-val" id="timer-display">0:00</div>
+      <div class="hud-lbl">Time</div>
+    </div>
+    <div class="hud-sep"></div>
+    <div class="hud-cell">
+      <div class="hud-val" id="best-display">—</div>
+      <div class="hud-lbl">Best (moves)</div>
+    </div>
+  </div>
+
+  <div class="size-row">
+    <button class="sz-btn" data-size="3">3×3</button>
+    <button class="sz-btn active" data-size="4">4×4</button>
+    <button class="sz-btn" data-size="5">5×5</button>
+  </div>
+
+  <div style="display:flex;gap:28px;align-items:flex-start;flex-wrap:wrap;justify-content:center">
+    <div id="board" role="grid" aria-label="Sliding puzzle board"></div>
+    <div class="peek-wrap" title="Hover to peek at solved state">
+      <div class="peek-label">Target Panel</div>
+      <svg id="peek-svg" width="120" height="120" viewBox="0 0 120 120"></svg>
+      <div class="peek-hint">Hover to reveal</div>
+    </div>
+  </div>
+
+  <div class="actions">
+    <button class="primary" id="shuffle-btn">Shuffle</button>
+    <button class="ghost" id="solve-hint-btn">Hint</button>
+    <button class="ghost" id="nums-btn">Toggle #</button>
+  </div>
+</main>
+
+<!-- WIN OVERLAY -->
+<div id="win-overlay" role="dialog" aria-modal="true" aria-label="Puzzle solved">
+  <div class="win-title">Masterpiece</div>
+  <svg class="win-art" id="win-art-svg" width="200" height="200" viewBox="0 0 200 200"></svg>
+  <div class="win-stats">
+    <div class="win-stat">
+      <div class="win-stat-v" id="win-moves">0</div>
+      <div class="win-stat-l">Moves</div>
+    </div>
+    <div class="win-stat">
+      <div class="win-stat-v" id="win-time">0:00</div>
+      <div class="win-stat-l">Time</div>
+    </div>
+    <div class="win-stat">
+      <div class="win-stat-v" id="win-best">—</div>
+      <div class="win-stat-l">Best</div>
+    </div>
+  </div>
+  <div class="win-sub">The herringbone panel is restored</div>
+  <div class="win-buttons">
+    <button class="primary" id="play-again-btn">Play Again</button>
+    <button class="ghost" id="close-win-btn">Close</button>
+  </div>
+</div>
+
+<script>
+// ─── AUDIO ───────────────────────────────────────────────────────────────────
+let muted = false;
+let audioCtx = null;
+function getCtx(){
+  if(!audioCtx) audioCtx = new (window.AudioContext||window.webkitAudioContext)();
+  return audioCtx;
+}
+function playTone(freq,type,dur,vol=0.12,attack=0.005,decay=0.08){
+  if(muted)return;
+  try{
+    const ctx=getCtx();
+    const o=ctx.createOscillator(),g=ctx.createGain();
+    o.connect(g);g.connect(ctx.destination);
+    o.type=type;o.frequency.value=freq;
+    g.gain.setValueAtTime(0,ctx.currentTime);
+    g.gain.linearRampToValueAtTime(vol,ctx.currentTime+attack);
+    g.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+dur);
+    o.start(ctx.currentTime);o.stop(ctx.currentTime+dur+decay);
+  }catch(e){}
+}
+function sfxSlide(){playTone(300+(Math.random()*200),'sine',.12,.08)}
+function sfxIllegal(){playTone(180,'sawtooth',.08,.05)}
+function sfxWin(){
+  const notes=[523,659,784,1047];
+  notes.forEach((f,i)=>setTimeout(()=>playTone(f,'sine',.35,.1),i*100));
+}
+document.getElementById('mute-btn').addEventListener('click',function(){
+  muted=!muted;this.textContent=muted?'✕ Muted':'♪ Sound';
+});
+
+// ─── HERRINGBONE SVG ART ─────────────────────────────────────────────────────
+// Generates a beautiful marquetry/herringbone SVG for given size (px)
+function buildHerringboneSVG(size){
+  // Colour palette — warm walnut/oak tones
+  const grains=[
+    ['#7B5E3A','#9A7648','#B8934F','#C9A361'],   // dark walnut planks
+    ['#A67B4B','#BC9260','#D4AB74','#C8A055'],   // medium oak
+    ['#8B6535','#A07840','#BF9A5A','#D4B06A'],   // honey oak
+    ['#6B4E2A','#83623A','#9A7648','#B08840'],   // ebony
+    ['#C5A55A','#D4B870','#E2CB88','#CAAA5A'],   // blonde maple
+  ];
+  // plank dimensions in the herringbone
+  const pw=28,ph=10; // plank width, plank height (landscape orientation)
+  const defs=[];
+  const paths=[];
+
+  // Build a clip path for the viewBox
+  defs.push(`<clipPath id="board-clip"><rect width="${size}" height="${size}"/></clipPath>`);
+
+  // Add subtle wood grain texture filter
+  defs.push(`
+    <filter id="grain" x="0%" y="0%" width="100%" height="100%">
+      <feTurbulence type="fractalNoise" baseFrequency="0.9 0.12" numOctaves="4" seed="3" result="noise"/>
+      <feColorMatrix type="saturate" values="0" in="noise" result="grayNoise"/>
+      <feBlend in="SourceGraphic" in2="grayNoise" mode="multiply" result="blended"/>
+      <feComposite in="blended" in2="SourceGraphic" operator="in"/>
+    </filter>
+    <filter id="shadow" x="-5%" y="-5%" width="110%" height="110%">
+      <feDropShadow dx="0.5" dy="0.5" stdDeviation="0.8" flood-color="rgba(0,0,0,0.45)"/>
+    </filter>
+  `);
+
+  // Herringbone tile pattern — two orientations tiling
+  // Each "unit cell" = two planks: one horizontal, one vertical
+  const uw=ph*2, uh=pw+ph; // unit cell
+  let id=0;
+  const svgPaths=[];
+
+  function plankRect(x,y,w,h,colors,rotation,cx,cy){
+    const [c1,c2,c3,c4]=colors;
+    const pid=`p${id++}`;
+    // gradient along plank length
+    const gid=`g${id++}`;
+    const angle = rotation===0 ? 0 : 90;
+    defs.push(`<linearGradient id="${gid}" x1="0%" y1="0%" x2="100%" y2="0%" gradientTransform="rotate(${angle},0.5,0.5)">
+      <stop offset="0%" stop-color="${c1}"/>
+      <stop offset="30%" stop-color="${c2}"/>
+      <stop offset="65%" stop-color="${c3}"/>
+      <stop offset="100%" stop-color="${c4}"/>
+    </linearGradient>`);
+    const transform = rotation ? `rotate(${rotation},${cx},${cy})` : '';
+    svgPaths.push(`<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="url(#${gid})" filter="url(#grain)" ${transform?`transform="${transform}"`:''}/>
+    <rect x="${x}" y="${y}" width="${w}" height="${h}" fill="none" stroke="rgba(0,0,0,0.3)" stroke-width="0.5" ${transform?`transform="${transform}"`:''}/>`);
+  }
+
+  // Tile the herringbone pattern across the SVG
+  const cols=Math.ceil(size/uw)+3;
+  const rows=Math.ceil(size/uh)+3;
+  for(let row=-1;row<rows;row++){
+    for(let col=-1;col<cols;col++){
+      const bx=col*uw - ph;
+      const by=row*uh - ph;
+      // pick color by position for variety
+      const ci=((row*3+col*7)&0xff) % grains.length;
+      const ci2=((row*5+col*3+2)&0xff) % grains.length;
+      // Horizontal plank (top of unit)
+      plankRect(bx, by, pw, ph, grains[ci], 0,0,0);
+      // Vertical plank (rotated 90, right side of unit)
+      plankRect(bx+ph, by, ph, pw, grains[ci2], 0,0,0);
+      // Second row horizontal
+      plankRect(bx, by+ph, pw, ph, grains[(ci+2)%grains.length], 0,0,0);
+      // Second row vertical
+      plankRect(bx+ph, by+ph, ph, pw, grains[(ci2+1)%grains.length], 0,0,0);
+    }
+  }
+
+  // Inlay border frame
+  const fw=4;
+  defs.push(`<linearGradient id="frame-grad" x1="0%" y1="0%" x2="100%" y2="100%">
+    <stop offset="0%" stop-color="#c9a961"/>
+    <stop offset="50%" stop-color="#e0c07a"/>
+    <stop offset="100%" stop-color="#8a6f35"/>
+  </linearGradient>`);
+
+  const svg=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">
+  <defs>${defs.join('')}</defs>
+  <g clip-path="url(#board-clip)">
+    <rect width="${size}" height="${size}" fill="#3a2a14"/>
+    ${svgPaths.join('\n')}
+    <!-- Inner frame inlay -->
+    <rect x="${fw/2}" y="${fw/2}" width="${size-fw}" height="${size-fw}"
+      fill="none" stroke="url(#frame-grad)" stroke-width="${fw}" opacity="0.6"/>
+    <rect x="${fw}" y="${fw}" width="${size-fw*2}" height="${size-fw*2}"
+      fill="none" stroke="rgba(0,0,0,0.4)" stroke-width="0.5"/>
+  </g>
+</svg>`;
+  return svg;
+}
+
+// ─── PUZZLE ENGINE ───────────────────────────────────────────────────────────
+const board=document.getElementById('board');
+const movesEl=document.getElementById('moves-display');
+const timerEl=document.getElementById('timer-display');
+const bestEl=document.getElementById('best-display');
+const winOverlay=document.getElementById('win-overlay');
+const winMovesEl=document.getElementById('win-moves');
+const winTimeEl=document.getElementById('win-time');
+const winBestEl=document.getElementById('win-best');
+const peekSVG=document.getElementById('peek-svg');
+const winArtSVG=document.getElementById('win-art-svg');
+
+let N=4;           // grid size
+let tiles=[];      // 0..N*N-1, last = empty
+let emptyIdx=N*N-1;
+let moves=0;
+let startTime=null;
+let timerInterval=null;
+let won=false;
+let showNums=false;
+
+function bestKey(){return`dw-parquet-best-${N}`}
+function getBest(){return parseInt(localStorage.getItem(bestKey())||'0')||null}
+function setBest(m){localStorage.setItem(bestKey(),m)}
+
+function fmt(sec){
+  const m=Math.floor(sec/60),s=sec%60;
+  return `${m}:${s.toString().padStart(2,'0')}`;
+}
+function fmtMs(ms){return fmt(Math.round(ms/1000))}
+
+// Build the full SVG for the panel
+let panelSVG='';
+let tileSize=0;
+let boardPx=0;
+
+function makePanelSVG(n){
+  boardPx=Math.min(window.innerWidth-80,460);
+  boardPx=Math.max(boardPx,260);
+  // snap to nice tile
+  tileSize=Math.floor((boardPx-(n+1)*3)/n);
+  boardPx=tileSize*n+(n+1)*3;
+  return buildHerringboneSVG(boardPx);
+}
+
+function initGame(n){
+  N=n;
+  won=false;
+  moves=0;
+  movesEl.textContent='0';
+  clearInterval(timerInterval);
+  timerEl.textContent='0:00';
+  startTime=null;
+
+  panelSVG=makePanelSVG(N);
+
+  // Update peek
+  peekSVG.setAttribute('width',120);
+  peekSVG.setAttribute('height',120);
+  peekSVG.setAttribute('viewBox',`0 0 ${boardPx} ${boardPx}`);
+  peekSVG.innerHTML=`<image href="data:image/svg+xml;base64,${btoa(panelSVG)}" width="${boardPx}" height="${boardPx}"/>`;
+
+  // Update win art
+  winArtSVG.setAttribute('viewBox',`0 0 ${boardPx} ${boardPx}`);
+  winArtSVG.innerHTML=`<image href="data:image/svg+xml;base64,${btoa(panelSVG)}" width="${boardPx}" height="${boardPx}"/>`;
+
+  // Init solved order then shuffle
+  tiles=Array.from({length:N*N},(_,i)=>i);
+  emptyIdx=N*N-1;
+
+  updateBestDisplay();
+  shuffleTiles();
+  renderBoard();
+}
+
+function shuffleTiles(){
+  // Perform many random legal moves to ensure solvability
+  const moves_count=N*N*60;
+  let ei=N*N-1;
+  let lastMoved=-1;
+  for(let i=0;i<moves_count;i++){
+    const neighbors=getNeighbors(ei);
+    const valid=neighbors.filter(x=>x!==lastMoved);
+    const pick=valid[Math.floor(Math.random()*valid.length)];
+    [tiles[ei],tiles[pick]]=[tiles[pick],tiles[ei]];
+    lastMoved=ei;
+    ei=pick;
+  }
+  emptyIdx=ei;
+}
+
+function getNeighbors(idx){
+  const r=Math.floor(idx/N),c=idx%N;
+  const nb=[];
+  if(r>0)nb.push(idx-N);
+  if(r<N-1)nb.push(idx+N);
+  if(c>0)nb.push(idx-1);
+  if(c<N-1)nb.push(idx+1);
+  return nb;
+}
+
+function canMove(idx){
+  return getNeighbors(emptyIdx).includes(idx);
+}
+
+function moveTile(tileIdx){
+  if(won)return;
+  if(!canMove(tileIdx)){sfxIllegal();return;}
+  if(!startTime){
+    startTime=Date.now();
+    timerInterval=setInterval(()=>{
+      timerEl.textContent=fmtMs(Date.now()-startTime);
+    },500);
+  }
+  [tiles[emptyIdx],tiles[tileIdx]]=[tiles[tileIdx],tiles[emptyIdx]];
+  emptyIdx=tileIdx;
+  moves++;
+  movesEl.textContent=moves;
+  sfxSlide();
+  renderBoard();
+  checkWin();
+}
+
+function checkWin(){
+  if(tiles.every((v,i)=>v===i)){
+    won=true;
+    clearInterval(timerInterval);
+    const elapsed=startTime?fmtMs(Date.now()-startTime):'0:00';
+    const best=getBest();
+    if(!best||moves<best)setBest(moves);
+    updateBestDisplay();
+    setTimeout(()=>showWin(moves,elapsed),350);
+  }
+}
+
+function updateBestDisplay(){
+  const b=getBest();
+  bestEl.textContent=b||'—';
+}
+
+// ─── RENDER ──────────────────────────────────────────────────────────────────
+function renderBoard(){
+  board.style.gridTemplateColumns=`repeat(${N},${tileSize}px)`;
+  board.style.width=boardPx+'px';
+  board.innerHTML='';
+
+  // Parse the panel SVG into per-tile views
+  const svgB64=btoa(panelSVG);
+
+  for(let i=0;i<N*N;i++){
+    const val=tiles[i];
+    const el=document.createElement('div');
+    el.className='tile'+(val===N*N-1?' empty':'');
+    el.style.width=tileSize+'px';
+    el.style.height=tileSize+'px';
+    el.setAttribute('role','gridcell');
+
+    if(val!==N*N-1){
+      const tr=Math.floor(val/N); // source row
+      const tc=val%N;             // source col
+      // offset into the full panel image
+      const ox=tc*tileSize;
+      const oy=tr*tileSize;
+
+      el.innerHTML=`<svg width="${tileSize}" height="${tileSize}" viewBox="${ox} ${oy} ${tileSize} ${tileSize}" xmlns="http://www.w3.org/2000/svg">
+        <image href="data:image/svg+xml;base64,${svgB64}" x="0" y="0" width="${boardPx}" height="${boardPx}" transform="translate(-${ox},-${oy})"/>
+      </svg>
+      <div class="tile-num${showNums?'':' hidden'}">${val+1}</div>`;
+
+      el.setAttribute('aria-label',`Tile ${val+1}`);
+      el.setAttribute('tabindex','0');
+      el.addEventListener('click',()=>moveTile(i));
+      el.addEventListener('keydown',e=>{
+        if(e.key==='Enter'||e.key===' ')moveTile(i);
+      });
+    } else {
+      el.setAttribute('aria-label','Empty space');
+    }
+
+    board.appendChild(el);
+  }
+  if(showNums)board.classList.add('show-nums');
+  else board.classList.remove('show-nums');
+}
+
+// ─── WIN OVERLAY ─────────────────────────────────────────────────────────────
+function showWin(m,t){
+  sfxWin();
+  winMovesEl.textContent=m;
+  winTimeEl.textContent=t;
+  const b=getBest();
+  winBestEl.textContent=b||m;
+  winOverlay.classList.add('show');
+  launchConfetti();
+}
+
+document.getElementById('play-again-btn').addEventListener('click',()=>{
+  winOverlay.classList.remove('show');
+  initGame(N);
+});
+document.getElementById('close-win-btn').addEventListener('click',()=>{
+  winOverlay.classList.remove('show');
+});
+
+// ─── CONFETTI ────────────────────────────────────────────────────────────────
+function launchConfetti(){
+  const colors=['#c9a961','#e0c07a','#f4f1ea','#8a6f35','#d4b06a','#c9a961'];
+  for(let i=0;i<50;i++){
+    setTimeout(()=>{
+      const el=document.createElement('div');
+      el.className='confetti-piece';
+      el.style.cssText=`
+        left:${20+Math.random()*60}vw;
+        top:-20px;
+        background:${colors[Math.floor(Math.random()*colors.length)]};
+        animation-duration:${1.5+Math.random()*1.5}s;
+        animation-delay:0s;
+        transform:rotate(${Math.random()*360}deg);
+        width:${6+Math.random()*8}px;
+        height:${6+Math.random()*8}px;
+        border-radius:${Math.random()>.5?'50%':'2px'};
+      `;
+      document.body.appendChild(el);
+      el.addEventListener('animationend',()=>el.remove());
+    },i*40);
+  }
+}
+
+// ─── KEYBOARD NAVIGATION ─────────────────────────────────────────────────────
+// Arrow keys move the tile adjacent to the empty in that direction
+document.addEventListener('keydown',e=>{
+  if(won)return;
+  const arrowMap={ArrowUp:N,ArrowDown:-N,ArrowLeft:1,ArrowRight:-1};
+  if(e.key in arrowMap){
+    e.preventDefault();
+    const offset=arrowMap[e.key];
+    const target=emptyIdx+offset;
+    if(target>=0&&target<N*N){
+      // Check it's not wrapping rows for left/right
+      if(e.key==='ArrowLeft'||e.key==='ArrowRight'){
+        const er=Math.floor(emptyIdx/N),tr2=Math.floor(target/N);
+        if(er!==tr2)return;
+      }
+      moveTile(target);
+    }
+  }
+});
+
+// ─── TOUCH SWIPE ─────────────────────────────────────────────────────────────
+let touchStart=null;
+board.addEventListener('touchstart',e=>{touchStart={x:e.touches[0].clientX,y:e.touches[0].clientY}},{passive:true});
+board.addEventListener('touchend',e=>{
+  if(!touchStart)return;
+  const dx=e.changedTouches[0].clientX-touchStart.x;
+  const dy=e.changedTouches[0].clientY-touchStart.y;
+  touchStart=null;
+  if(Math.abs(dx)<20&&Math.abs(dy)<20)return;
+  // Swipe direction slides tile into empty
+  let target=-1;
+  if(Math.abs(dx)>Math.abs(dy)){
+    target=dx>0?emptyIdx-1:emptyIdx+1;
+  } else {
+    target=dy>0?emptyIdx-N:emptyIdx+N;
+  }
+  if(target>=0&&target<N*N){
+    const er=Math.floor(emptyIdx/N),tr2=Math.floor(target/N);
+    if(Math.abs(dx)>Math.abs(dy)&&er!==tr2)return;
+    moveTile(target);
+  }
+},{passive:true});
+
+// ─── CONTROLS ────────────────────────────────────────────────────────────────
+document.getElementById('shuffle-btn').addEventListener('click',()=>{
+  winOverlay.classList.remove('show');
+  initGame(N);
+});
+
+document.getElementById('nums-btn').addEventListener('click',()=>{
+  showNums=!showNums;
+  renderBoard();
+});
+
+document.getElementById('solve-hint-btn').addEventListener('click',()=>{
+  // Highlight tiles that are adjacent to empty and in their correct position
+  // Simple hint: find first tile not in place and that can move, flash it
+  for(let i=0;i<N*N;i++){
+    if(tiles[i]!==i&&i!==emptyIdx&&canMove(i)){
+      const el=board.children[i];
+      if(el){
+        el.style.boxShadow='0 0 0 2px #c9a961, 0 0 12px rgba(201,169,97,.6)';
+        setTimeout(()=>{el.style.boxShadow=''},900);
+      }
+      break;
+    }
+  }
+});
+
+document.querySelectorAll('.sz-btn').forEach(btn=>{
+  btn.addEventListener('click',function(){
+    document.querySelectorAll('.sz-btn').forEach(b=>b.classList.remove('active'));
+    this.classList.add('active');
+    winOverlay.classList.remove('show');
+    initGame(parseInt(this.dataset.size));
+  });
+});
+
+// ─── INIT ────────────────────────────────────────────────────────────────────
+initGame(4);
+</script>
+</body>
+</html>

← bf43f1b DW Suite index page (/suite, /dw) — categorized landing for  ·  back to Model Arena  ·  Agent-driven tool: DW Chinoiserie Studio (frontend-developer 4dd0e69 →