[object Object]

← back to Model Arena

Agent-driven tool: DW Pairing Studio (frontend-developer) — 60-30-10 scheme builder + room mock. Registered.

cc77e595d37b554a87f4c8981e007f025503bbcc · 2026-07-25 13:11:00 -0700 · Steve Abrams

Files touched

Diff

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

    Agent-driven tool: DW Pairing Studio (frontend-developer) — 60-30-10 scheme builder + room mock. Registered.
---
 data/arcade-games.json              |   7 +
 public/games/dw-pairing-studio.html | 826 ++++++++++++++++++++++++++++++++++++
 public/games/dw-toile-coloring.html | 789 ++++++++++++++++++++++++++++++++++
 3 files changed, 1622 insertions(+)

diff --git a/data/arcade-games.json b/data/arcade-games.json
index 01802c7..9679289 100644
--- a/data/arcade-games.json
+++ b/data/arcade-games.json
@@ -181,6 +181,13 @@
       "credit": "frontend-developer",
       "category": "Art Toys",
       "note": "Agent-built slot toy \u2014 spin Motif \u00d7 Colorway \u00d7 Finish reels into a generated wallcovering swatch + collection name, rarity jackpots"
+    },
+    {
+      "slug": "dw-pairing-studio",
+      "title": "DW Pairing Studio",
+      "credit": "frontend-developer",
+      "category": "Tools",
+      "note": "Agent-built tool \u2014 60-30-10 scheme builder: hero + coordinate + accent with designer rationale + room mock"
     }
   ]
 }
\ No newline at end of file
diff --git a/public/games/dw-pairing-studio.html b/public/games/dw-pairing-studio.html
new file mode 100644
index 0000000..344d4ab
--- /dev/null
+++ b/public/games/dw-pairing-studio.html
@@ -0,0 +1,826 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Pairing 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);
+  --r:4px;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+html,body{min-height:100%;background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:14px}
+header{
+  position:sticky;top:0;z-index:200;
+  background:var(--bg);border-bottom:1px solid rgba(201,169,97,.35);
+  display:flex;align-items:center;gap:14px;padding:10px 22px;
+}
+.wordmark{
+  font-family:var(--serif);font-size:clamp(20px,2.2vw,28px);font-weight:400;
+  text-transform:uppercase;letter-spacing:.18em;color:var(--ink);white-space:nowrap;line-height:1.1;
+}
+.wordmark small{display:block;font-size:10px;letter-spacing:.25em;color:var(--accent2);margin-top:1px}
+.tag-pill{
+  background:var(--accent);color:#1c1a17;font-size:10px;font-weight:600;
+  letter-spacing:.15em;text-transform:uppercase;padding:3px 10px;border-radius:20px;
+  margin-left:auto;white-space:nowrap;
+}
+main{max-width:1280px;margin:0 auto;padding:28px 20px 80px}
+h2{font-family:var(--serif);font-weight:400;font-size:clamp(22px,2.5vw,32px);letter-spacing:.04em;margin-bottom:4px}
+.sub{color:var(--ink2);font-size:12.5px;letter-spacing:.06em;margin-bottom:24px}
+
+/* Hero picker */
+.hero-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(154px,1fr));gap:14px;margin-bottom:36px}
+.hero-card{
+  border:2px solid transparent;border-radius:var(--r);cursor:pointer;
+  transition:border-color .22s var(--ease),transform .18s var(--ease),box-shadow .22s var(--ease);
+  overflow:hidden;background:var(--silk);
+}
+.hero-card:hover{transform:translateY(-3px);box-shadow:0 8px 24px rgba(28,26,23,.12)}
+.hero-card.active{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}
+.hero-card svg{display:block;width:100%;aspect-ratio:1}
+.hero-label{padding:8px 10px 10px;line-height:1.3}
+.hero-label strong{display:block;font-family:var(--serif);font-size:15px;font-weight:500}
+.hero-label span{font-size:11px;color:var(--ink2);letter-spacing:.04em}
+
+/* Scheme display */
+.scheme-wrap{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-bottom:36px}
+@media(max-width:720px){.scheme-wrap{grid-template-columns:1fr}}
+
+/* Swatch cards row */
+.swatches-row{display:flex;gap:14px;flex-wrap:wrap}
+.swatch-card{
+  flex:1;min-width:140px;border-radius:var(--r);overflow:hidden;
+  background:var(--silk);border:1px solid rgba(28,26,23,.1);
+  transition:transform .22s var(--ease),box-shadow .22s var(--ease);
+}
+.swatch-card:hover{transform:translateY(-2px);box-shadow:0 6px 18px rgba(28,26,23,.1)}
+.swatch-card svg{display:block;width:100%;aspect-ratio:4/3}
+.swatch-meta{padding:10px 12px 12px}
+.pct-badge{
+  display:inline-block;font-size:10px;font-weight:700;letter-spacing:.12em;
+  background:var(--accent);color:#1c1a17;padding:2px 8px;border-radius:12px;margin-bottom:6px;
+}
+.swatch-meta strong{display:block;font-family:var(--serif);font-size:15px;font-weight:500;margin-bottom:2px}
+.swatch-meta em{display:block;font-size:11px;color:var(--ink2);font-style:italic;line-height:1.45}
+.role-label{font-size:10px;text-transform:uppercase;letter-spacing:.14em;color:var(--accent2);font-weight:600;margin-bottom:3px}
+
+/* Room mock */
+.room-wrap{
+  border-radius:var(--r);overflow:hidden;border:1px solid rgba(28,26,23,.1);
+  position:relative;background:#e8e4db;
+}
+.room-wrap svg{display:block;width:100%;height:auto}
+
+/* Controls */
+.controls{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:22px;align-items:center}
+.btn{
+  font-family:var(--sans);font-size:12px;font-weight:600;letter-spacing:.12em;
+  text-transform:uppercase;padding:9px 18px;border-radius:var(--r);cursor:pointer;
+  border:1.5px solid var(--accent);background:transparent;color:var(--ink);
+  transition:background .18s var(--ease),color .18s var(--ease);white-space:nowrap;
+}
+.btn:hover{background:var(--accent);color:#1c1a17}
+.btn.primary{background:var(--accent);color:#1c1a17}
+.btn.primary:hover{background:var(--accent2)}
+
+/* Board overlay */
+.overlay{
+  position:fixed;inset:0;background:rgba(28,26,23,.72);z-index:300;
+  display:flex;align-items:center;justify-content:center;
+  visibility:hidden;opacity:0;transition:opacity .28s var(--ease),visibility 0s .28s;
+}
+.overlay.open{visibility:visible;opacity:1;transition:opacity .28s var(--ease)}
+.panel{
+  background:var(--bg);border-radius:6px;width:min(680px,95vw);max-height:88vh;
+  overflow-y:auto;padding:28px;position:relative;
+}
+.panel h3{font-family:var(--serif);font-size:22px;font-weight:400;margin-bottom:18px}
+.close-btn{
+  position:absolute;top:14px;right:16px;background:none;border:none;
+  font-size:22px;cursor:pointer;color:var(--ink2);line-height:1;
+}
+.board-item{
+  border:1px solid rgba(28,26,23,.12);border-radius:var(--r);padding:14px 16px;
+  margin-bottom:12px;display:grid;grid-template-columns:1fr auto;align-items:start;gap:12px;
+}
+.board-item-swatches{display:flex;gap:8px;margin-top:10px}
+.board-mini{width:44px;height:44px;border-radius:2px;overflow:hidden;flex-shrink:0}
+.board-mini svg{display:block;width:100%;height:100%}
+.board-del{background:none;border:none;cursor:pointer;color:#c0392b;font-size:18px;padding:0}
+.empty-board{text-align:center;padding:40px 20px;color:var(--ink2);font-style:italic}
+
+/* Export canvas (hidden) */
+#export-canvas{display:none}
+
+/* Stagger animation */
+@keyframes fadeSlide{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
+.swatch-card{animation:fadeSlide .35s var(--ease) both}
+.swatch-card:nth-child(2){animation-delay:.07s}
+.swatch-card:nth-child(3){animation-delay:.14s}
+
+/* Responsive */
+@media(max-width:500px){
+  .hero-grid{grid-template-columns:repeat(auto-fill,minmax(120px,1fr))}
+  .swatches-row{flex-direction:column}
+}
+</style>
+</head>
+<body>
+
+<header>
+  <div class="wordmark">Designer Wallcoverings<small>Pairing Studio</small></div>
+  <div class="tag-pill">60·30·10</div>
+</header>
+
+<main>
+  <h2>Choose Your Hero Pattern</h2>
+  <p class="sub">Select a wallcovering — the studio builds your curated 60·30·10 scheme</p>
+
+  <div class="hero-grid" id="heroGrid"></div>
+
+  <div id="schemeSection" style="display:none">
+    <div class="controls">
+      <button class="btn" id="reshuffleBtn">Reshuffle Coordinates</button>
+      <button class="btn primary" id="saveBtn">Save to Board</button>
+      <button class="btn" id="boardBtn">View Board</button>
+      <button class="btn" id="exportBtn">Export Scheme Card</button>
+    </div>
+
+    <div class="scheme-wrap">
+      <div>
+        <h2 style="font-size:18px;margin-bottom:6px">Your Scheme</h2>
+        <div class="swatches-row" id="swatchesRow"></div>
+      </div>
+      <div>
+        <h2 style="font-size:18px;margin-bottom:6px">Room Preview</h2>
+        <div class="room-wrap" id="roomWrap"></div>
+      </div>
+    </div>
+  </div>
+</main>
+
+<!-- Board overlay -->
+<div class="overlay" id="boardOverlay">
+  <div class="panel">
+    <button class="close-btn" id="closeBoardBtn">&times;</button>
+    <h3>Saved Schemes</h3>
+    <div id="boardList"></div>
+  </div>
+</div>
+
+<canvas id="export-canvas" width="900" height="360"></canvas>
+
+<script>
+// ─── SVG Generators ──────────────────────────────────────────────────────────
+
+function svgDamask(c1,c2,w,h){
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 80 80">
+  <rect width="80" height="80" fill="${c1}"/>
+  <g fill="none" stroke="${c2}" stroke-width="0.7" opacity="0.85">
+    <ellipse cx="40" cy="40" rx="14" ry="20"/>
+    <ellipse cx="40" cy="40" rx="9" ry="13"/>
+    <ellipse cx="40" cy="20" rx="5" ry="8"/>
+    <ellipse cx="40" cy="60" rx="5" ry="8"/>
+    <ellipse cx="20" cy="40" rx="8" ry="5"/>
+    <ellipse cx="60" cy="40" rx="8" ry="5"/>
+    <path d="M40,20 C48,26 48,34 40,40 C32,34 32,26 40,20z" fill="${c2}" opacity="0.18"/>
+    <path d="M40,40 C48,46 48,54 40,60 C32,54 32,46 40,40z" fill="${c2}" opacity="0.18"/>
+    <circle cx="40" cy="40" r="3" fill="${c2}" opacity="0.4"/>
+    <circle cx="40" cy="20" r="1.5" fill="${c2}" opacity="0.4"/>
+    <circle cx="40" cy="60" r="1.5" fill="${c2}" opacity="0.4"/>
+  </g>
+</svg>`;
+}
+
+function svgToile(c1,c2,w,h){
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 100 100">
+  <rect width="100" height="100" fill="${c1}"/>
+  <g fill="none" stroke="${c2}" stroke-width="0.6" opacity="0.8">
+    <path d="M20,30 Q30,10 40,25 Q50,10 60,30 Q70,45 60,55 Q50,65 40,55 Q30,65 20,50 Z"/>
+    <circle cx="40" cy="38" r="4" fill="${c2}" opacity="0.2"/>
+    <path d="M38,34 Q40,28 42,34"/>
+    <path d="M20,70 Q28,55 36,68 Q44,55 52,70 Q44,80 36,76 Q28,80 20,70Z"/>
+    <circle cx="36" cy="68" r="3" fill="${c2}" opacity="0.2"/>
+    <path d="M65,60 Q78,55 75,68 Q78,78 65,75 Q58,70 65,60Z"/>
+    <path d="M66,70 Q72,72 70,66"/>
+    <line x1="40" y1="55" x2="40" y2="70" stroke-width="0.4"/>
+    <path d="M15,15 Q25,5 35,15" stroke-dasharray="2,2"/>
+    <path d="M60,15 Q70,5 80,15" stroke-dasharray="2,2"/>
+  </g>
+</svg>`;
+}
+
+function svgTrellis(c1,c2,w,h){
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 60 60">
+  <rect width="60" height="60" fill="${c1}"/>
+  <g stroke="${c2}" stroke-width="1.2" fill="none" opacity="0.75">
+    <path d="M0,30 L15,15 L30,30 L45,15 L60,30"/>
+    <path d="M0,30 L15,45 L30,30 L45,45 L60,30"/>
+    <path d="M15,15 L15,45"/>
+    <path d="M45,15 L45,45"/>
+    <circle cx="15" cy="15" r="2.5" fill="${c2}" opacity="0.5"/>
+    <circle cx="45" cy="15" r="2.5" fill="${c2}" opacity="0.5"/>
+    <circle cx="15" cy="45" r="2.5" fill="${c2}" opacity="0.5"/>
+    <circle cx="45" cy="45" r="2.5" fill="${c2}" opacity="0.5"/>
+    <circle cx="30" cy="30" r="2.5" fill="${c2}" opacity="0.5"/>
+  </g>
+</svg>`;
+}
+
+function svgChinoiserie(c1,c2,w,h){
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 100 100">
+  <rect width="100" height="100" fill="${c1}"/>
+  <g fill="${c2}" opacity="0.82">
+    <ellipse cx="50" cy="28" rx="4" ry="6"/>
+    <path d="M46,28 Q42,18 44,12 Q50,8 56,12 Q58,18 54,28Z" opacity="0.6"/>
+    <path d="M50,34 Q58,40 60,50 Q55,58 50,60 Q45,58 40,50 Q42,40 50,34Z" fill="none" stroke="${c2}" stroke-width="1"/>
+    <ellipse cx="50" cy="48" rx="5" ry="7" opacity="0.3"/>
+    <path d="M35,50 Q28,42 30,35 Q38,32 42,40"/>
+    <path d="M65,50 Q72,42 70,35 Q62,32 58,40"/>
+    <circle cx="35" cy="70" r="4" fill="none" stroke="${c2}" stroke-width="0.8"/>
+    <path d="M35,66 Q30,60 33,55 Q38,54 40,60Z"/>
+    <circle cx="65" cy="70" r="4" fill="none" stroke="${c2}" stroke-width="0.8"/>
+    <path d="M65,66 Q70,60 67,55 Q62,54 60,60Z"/>
+    <path d="M20,80 Q25,70 30,75 Q25,82 20,80Z"/>
+    <path d="M80,80 Q75,70 70,75 Q75,82 80,80Z"/>
+    <path d="M44,60 Q42,72 44,78 L50,82 L56,78 Q58,72 56,60" fill="none" stroke="${c2}" stroke-width="0.7"/>
+  </g>
+</svg>`;
+}
+
+function svgGrasscloth(c1,c2,w,h){
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 80 80">
+  <rect width="80" height="80" fill="${c1}"/>
+  <g stroke="${c2}" stroke-width="0.5" opacity="0.6">
+    ${Array.from({length:16},(_,i)=>`<line x1="${i*5}" y1="0" x2="${i*5+2}" y2="80"/>`).join('')}
+    ${Array.from({length:20},(_,i)=>`<line x1="0" y1="${i*4}" x2="80" y2="${i*4+1}"/>`).join('')}
+  </g>
+  <g stroke="${c2}" stroke-width="0.9" opacity="0.35">
+    ${Array.from({length:8},(_,i)=>`<line x1="${i*10}" y1="0" x2="${i*10+4}" y2="80"/>`).join('')}
+  </g>
+</svg>`;
+}
+
+function svgStripe(c1,c2,c3,w,h){
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 60 60">
+  <rect width="60" height="60" fill="${c1}"/>
+  <rect x="0" width="8" height="60" fill="${c2}" opacity="0.85"/>
+  <rect x="10" width="2" height="60" fill="${c2}" opacity="0.4"/>
+  <rect x="15" width="8" height="60" fill="${c3}" opacity="0.65"/>
+  <rect x="25" width="8" height="60" fill="${c2}" opacity="0.85"/>
+  <rect x="35" width="2" height="60" fill="${c2}" opacity="0.4"/>
+  <rect x="40" width="8" height="60" fill="${c3}" opacity="0.65"/>
+  <rect x="50" width="8" height="60" fill="${c2}" opacity="0.85"/>
+</svg>`;
+}
+
+function svgGeometric(c1,c2,w,h){
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 60 60">
+  <rect width="60" height="60" fill="${c1}"/>
+  <g fill="${c2}" opacity="0.7">
+    <polygon points="15,5 25,5 30,15 20,20"/>
+    <polygon points="35,5 45,5 50,15 40,20"/>
+    <polygon points="15,35 25,35 30,45 20,50"/>
+    <polygon points="35,35 45,35 50,45 40,50"/>
+    <polygon points="0,20 10,20 15,30 5,35"/>
+    <polygon points="0,50 10,50 15,60 5,60"/>
+  </g>
+  <g fill="none" stroke="${c2}" stroke-width="0.6" opacity="0.4">
+    <line x1="0" y1="30" x2="60" y2="30"/>
+    <line x1="30" y1="0" x2="30" y2="60"/>
+  </g>
+</svg>`;
+}
+
+function svgSolid(c1,w,h){
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 60 60">
+  <rect width="60" height="60" fill="${c1}"/>
+  <g stroke="rgba(0,0,0,0.06)" stroke-width="0.5">
+    ${Array.from({length:12},(_,i)=>`<line x1="${i*5}" y1="0" x2="${i*5}" y2="60"/>`).join('')}
+    ${Array.from({length:12},(_,i)=>`<line x1="0" y1="${i*5}" x2="60" y2="${i*5}"/>`).join('')}
+  </g>
+</svg>`;
+}
+
+function svgFineStripe(c1,c2,w,h){
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 40 40">
+  <rect width="40" height="40" fill="${c1}"/>
+  ${Array.from({length:8},(_,i)=>`<rect x="${i*5}" width="1.5" height="40" fill="${c2}" opacity="0.55"/>`).join('')}
+</svg>`;
+}
+
+function svgSmallMotif(c1,c2,w,h){
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 40 40">
+  <rect width="40" height="40" fill="${c1}"/>
+  <g fill="${c2}" opacity="0.65">
+    <circle cx="10" cy="10" r="2"/>
+    <circle cx="30" cy="10" r="2"/>
+    <circle cx="10" cy="30" r="2"/>
+    <circle cx="30" cy="30" r="2"/>
+    <circle cx="20" cy="20" r="2.5"/>
+    <path d="M18,20 L20,16 L22,20 L20,24Z" opacity="0.5"/>
+  </g>
+</svg>`;
+}
+
+// ─── Data ────────────────────────────────────────────────────────────────────
+
+const HEROES = [
+  {
+    id:'damask-celadon',
+    name:'Celadon Damask',
+    colorway:'Celadon & Ivory',
+    type:'damask',
+    svg:(w,h)=>svgDamask('#e8ede5','#6b9e7a',w,h),
+    palette:{ground:'#e8ede5',motif:'#6b9e7a',accent:'#c9a961'},
+    era:'Georgian',
+    family:'botanical-green',
+    coords:[
+      {
+        variant:0,
+        secondary:{
+          name:'Vine Stripe',type:'fine-stripe',
+          svg:(w,h)=>svgFineStripe('#e8ede5','#6b9e7a',w,h),
+          rationale:'Shares the celadon ground; contrasts the damask\'s bold repeat with a hairline stripe'
+        },
+        accent:{
+          name:'Sage Bouclé',type:'solid',
+          svg:(w,h)=>svgSolid('#8aaf8a',w,h),
+          rationale:'Pulls the deeper green from the damask motif as a grounding solid'
+        }
+      },
+      {
+        variant:1,
+        secondary:{
+          name:'Georgian Trellis',type:'trellis',
+          svg:(w,h)=>svgTrellis('#f0f4ef','#6b9e7a',w,h),
+          rationale:'Same era and green family; trellis geometry counters the organic damask curve'
+        },
+        accent:{
+          name:'Brass Linen',type:'solid',
+          svg:(w,h)=>svgSolid('#c9a961',w,h),
+          rationale:'Introduces the brass accent tone for warmth against cool celadon'
+        }
+      }
+    ]
+  },
+  {
+    id:'toile-indigo',
+    name:'Indigo Toile',
+    colorway:'Indigo & Parchment',
+    type:'toile',
+    svg:(w,h)=>svgToile('#f5f0e6','#2e4a7a',w,h),
+    palette:{ground:'#f5f0e6',motif:'#2e4a7a',accent:'#c9a961'},
+    era:'French Provincial',
+    family:'blue-indigo',
+    coords:[
+      {
+        variant:0,
+        secondary:{
+          name:'Classic Ticking',type:'fine-stripe',
+          svg:(w,h)=>svgFineStripe('#f5f0e6','#2e4a7a',w,h),
+          rationale:'Traditional toile companion; the indigo stripe at 1/20th the scale creates rhythm'
+        },
+        accent:{
+          name:'Navy Grasscloth',type:'grasscloth',
+          svg:(w,h)=>svgGrasscloth('#2e4a7a','#1a2f50',w,h),
+          rationale:'Textural depth in the same blue family — the woven surface anchors the scene'
+        }
+      },
+      {
+        variant:1,
+        secondary:{
+          name:'Provincial Trellis',type:'trellis',
+          svg:(w,h)=>svgTrellis('#f5f0e6','#2e4a7a',w,h),
+          rationale:'Geometric partner in matching indigo — keeps French Provincial formality'
+        },
+        accent:{
+          name:'Ivory Linen Solid',type:'solid',
+          svg:(w,h)=>svgSolid('#f0e8d5',w,h),
+          rationale:'Parchment solid lets the toile breathe; reads as natural linen in a room'
+        }
+      }
+    ]
+  },
+  {
+    id:'chinoiserie-jade',
+    name:'Jade Chinoiserie',
+    colorway:'Jade & Gold',
+    type:'chinoiserie',
+    svg:(w,h)=>svgChinoiserie('#f5f2e8','#2d7a5e',w,h),
+    palette:{ground:'#f5f2e8',motif:'#2d7a5e',accent:'#c9a961'},
+    era:'18th Century',
+    family:'jade-botanical',
+    coords:[
+      {
+        variant:0,
+        secondary:{
+          name:'Gold Trellis',type:'trellis',
+          svg:(w,h)=>svgTrellis('#f5f2e8','#c9a961',w,h),
+          rationale:'Pulls the brass from the chinoiserie gilding; the lattice echoes the garden scene'
+        },
+        accent:{
+          name:'Celadon Grasscloth',type:'grasscloth',
+          svg:(w,h)=>svgGrasscloth('#cde0d0','#2d7a5e',w,h),
+          rationale:'Woven texture in the jade family — anchors the layered botanical palette'
+        }
+      },
+      {
+        variant:1,
+        secondary:{
+          name:'Jade Stripe',type:'fine-stripe',
+          svg:(w,h)=>svgFineStripe('#f5f2e8','#2d7a5e',w,h),
+          rationale:'Fine stripe in the same jade tone provides vertical rhythm without competing'
+        },
+        accent:{
+          name:'Antique Gold Solid',type:'solid',
+          svg:(w,h)=>svgSolid('#c9a961',w,h),
+          rationale:'Adds opulence; gold solid functions as an accent wall or cushion color'
+        }
+      }
+    ]
+  },
+  {
+    id:'grasscloth-linen',
+    name:'Natural Grasscloth',
+    colorway:'Linen & Seagrass',
+    type:'grasscloth',
+    svg:(w,h)=>svgGrasscloth('#d4c9a8','#8a7a5a',w,h),
+    palette:{ground:'#d4c9a8',motif:'#8a7a5a',accent:'#c9a961'},
+    era:'Mid-Century',
+    family:'natural-neutral',
+    coords:[
+      {
+        variant:0,
+        secondary:{
+          name:'Linen Stripe',type:'stripe',
+          svg:(w,h)=>svgStripe('#e8dfc8','#8a7a5a','#c9a961',w,h),
+          rationale:'Same sand-linen family; the broad stripe contrasts the woven grasscloth texture'
+        },
+        accent:{
+          name:'Sienna Small Motif',type:'small-motif',
+          svg:(w,h)=>svgSmallMotif('#d4c9a8','#8a4a2a',w,h),
+          rationale:'A warm terracotta motif introduces contrast tone without breaking the natural palette'
+        }
+      },
+      {
+        variant:1,
+        secondary:{
+          name:'Neutral Geometric',type:'geometric',
+          svg:(w,h)=>svgGeometric('#e8dfc8','#8a7a5a',w,h),
+          rationale:'Geometric pattern at same scale as grasscloth weave — tonal but modern contrast'
+        },
+        accent:{
+          name:'Warm Ivory Solid',type:'solid',
+          svg:(w,h)=>svgSolid('#f0e8d5',w,h),
+          rationale:'Creamy solid complements the natural palette and brightens accent details'
+        }
+      }
+    ]
+  },
+  {
+    id:'trellis-blush',
+    name:'Rose Trellis',
+    colorway:'Blush & Ecru',
+    type:'trellis',
+    svg:(w,h)=>svgTrellis('#f7ede6','#c97e7e',w,h),
+    palette:{ground:'#f7ede6',motif:'#c97e7e',accent:'#c9a961'},
+    era:'Regency',
+    family:'blush-rose',
+    coords:[
+      {
+        variant:0,
+        secondary:{
+          name:'Blush Damask',type:'damask',
+          svg:(w,h)=>svgDamask('#f7ede6','#c97e7e',w,h),
+          rationale:'Same rose family — the damask\'s organic curves relieve the trellis geometry'
+        },
+        accent:{
+          name:'Deep Rose Solid',type:'solid',
+          svg:(w,h)=>svgSolid('#c97e7e',w,h),
+          rationale:'Saturated pull of the trellis rose used as a solid accent — drapery or velvet'
+        }
+      },
+      {
+        variant:1,
+        secondary:{
+          name:'Ecru Fine Stripe',type:'fine-stripe',
+          svg:(w,h)=>svgFineStripe('#f7ede6','#e0b8a0',w,h),
+          rationale:'Softer tone stripe in peach-ecru — warms the blush trellis, adds Regency formality'
+        },
+        accent:{
+          name:'Antique Gold Brass',type:'solid',
+          svg:(w,h)=>svgSolid('#c9a961',w,h),
+          rationale:'Brass metallics are the classic Regency accent — unifies the scheme'
+        }
+      }
+    ]
+  },
+  {
+    id:'stripe-navy',
+    name:'Riviera Stripe',
+    colorway:'Navy & Cream',
+    type:'stripe',
+    svg:(w,h)=>svgStripe('#f4f1ea','#1a2e50','#c9a961',w,h),
+    palette:{ground:'#f4f1ea',motif:'#1a2e50',accent:'#c9a961'},
+    era:'Classic',
+    family:'blue-neutral',
+    coords:[
+      {
+        variant:0,
+        secondary:{
+          name:'Navy Toile',type:'toile',
+          svg:(w,h)=>svgToile('#f4f1ea','#1a2e50',w,h),
+          rationale:'Toile in matching navy — the figurative scale contrasts the clean stripe geometry'
+        },
+        accent:{
+          name:'Navy Grasscloth',type:'grasscloth',
+          svg:(w,h)=>svgGrasscloth('#1a2e50','#0d1a2e',w,h),
+          rationale:'Dark grasscloth as accent brings rich texture and anchors the navy throughout'
+        }
+      },
+      {
+        variant:1,
+        secondary:{
+          name:'Cream Small Motif',type:'small-motif',
+          svg:(w,h)=>svgSmallMotif('#f4f1ea','#1a2e50',w,h),
+          rationale:'Tonal ditsy motif on cream — keeps the scheme airy against the bold stripe'
+        },
+        accent:{
+          name:'Brass Solid',type:'solid',
+          svg:(w,h)=>svgSolid('#c9a961',w,h),
+          rationale:'Classic nautical brass — polishes the Riviera palette to a yacht-club finish'
+        }
+      }
+    ]
+  }
+];
+
+// ─── State ───────────────────────────────────────────────────────────────────
+
+let activeHeroId = null;
+let activeVariant = 0;
+let board = JSON.parse(localStorage.getItem('dw-pairing-board') || '[]');
+
+// ─── Render Hero Grid ─────────────────────────────────────────────────────────
+
+function renderHeroGrid(){
+  const grid = document.getElementById('heroGrid');
+  grid.innerHTML = HEROES.map(h=>`
+    <div class="hero-card ${activeHeroId===h.id?'active':''}" data-id="${h.id}" tabindex="0" role="button" aria-pressed="${activeHeroId===h.id}">
+      ${h.svg(160,160)}
+      <div class="hero-label">
+        <strong>${h.name}</strong>
+        <span>${h.colorway}</span>
+      </div>
+    </div>
+  `).join('');
+  grid.querySelectorAll('.hero-card').forEach(c=>{
+    c.addEventListener('click',()=>selectHero(c.dataset.id));
+    c.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' ')selectHero(c.dataset.id)});
+  });
+}
+
+function selectHero(id){
+  activeHeroId = id;
+  activeVariant = 0;
+  renderHeroGrid();
+  renderScheme(true);
+  document.getElementById('schemeSection').style.display='';
+  document.getElementById('schemeSection').scrollIntoView({behavior:'smooth',block:'start'});
+}
+
+// ─── Render Scheme ────────────────────────────────────────────────────────────
+
+function renderScheme(animate){
+  const hero = HEROES.find(h=>h.id===activeHeroId);
+  if(!hero) return;
+  const coord = hero.coords[activeVariant % hero.coords.length];
+
+  const swatchesRow = document.getElementById('swatchesRow');
+  swatchesRow.innerHTML = [
+    {pct:'60%',role:'Hero',name:hero.name,colorway:hero.colorway,svg:hero.svg(300,225),rationale:'The dominant wallcovering — sets the style era, color family, and scale for the entire scheme.'},
+    {pct:'30%',role:'Secondary',name:coord.secondary.name,colorway:'Coordinate',svg:coord.secondary.svg(300,225),rationale:coord.secondary.rationale},
+    {pct:'10%',role:'Accent',name:coord.accent.name,colorway:'Accent',svg:coord.accent.svg(300,225),rationale:coord.accent.rationale}
+  ].map((s,i)=>`
+    <div class="swatch-card" style="${animate?`animation-delay:${i*0.07}s`:''}">
+      ${s.svg}
+      <div class="swatch-meta">
+        <span class="pct-badge">${s.pct}</span>
+        <div class="role-label">${s.role}</div>
+        <strong>${s.name}</strong>
+        <em>${s.rationale}</em>
+      </div>
+    </div>
+  `).join('');
+
+  renderRoom(hero,coord);
+}
+
+// ─── Room Mock ────────────────────────────────────────────────────────────────
+
+function renderRoom(hero,coord){
+  const room = document.getElementById('roomWrap');
+  // Build a stylized isometric-ish room: back wall (hero), side wall (secondary), floor/trim (accent)
+  const hw = hero.svg(200,200);
+  const sw = coord.secondary.svg(100,200);
+  const ac = coord.accent.svg(60,60);
+  const pal = hero.palette;
+
+  room.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 340" style="display:block;width:100%;height:auto">
+  <defs>
+    <pattern id="rHero" width="80" height="80" patternUnits="userSpaceOnUse">${hw}</pattern>
+    <pattern id="rSecond" width="60" height="60" patternUnits="userSpaceOnUse">${sw}</pattern>
+    <pattern id="rAccent" width="60" height="60" patternUnits="userSpaceOnUse">${ac}</pattern>
+    <linearGradient id="floorGrad" x1="0" y1="0" x2="0" y2="1">
+      <stop offset="0%" stop-color="#c8bfa0"/>
+      <stop offset="100%" stop-color="#a89880"/>
+    </linearGradient>
+    <linearGradient id="ceilGrad" x1="0" y1="0" x2="0" y2="1">
+      <stop offset="0%" stop-color="#f0ece0"/>
+      <stop offset="100%" stop-color="#e4e0d4"/>
+    </linearGradient>
+    <linearGradient id="sideGrad" x1="0" y1="0" x2="1" y2="0">
+      <stop offset="0%" stop-color="rgba(0,0,0,0.18)"/>
+      <stop offset="100%" stop-color="rgba(0,0,0,0)"/>
+    </linearGradient>
+  </defs>
+
+  <!-- Ceiling -->
+  <polygon points="80,60 500,60 500,10 80,10" fill="url(#ceilGrad)" stroke="rgba(0,0,0,.08)" stroke-width="1"/>
+
+  <!-- Back wall (hero 60%) -->
+  <rect x="80" y="60" width="420" height="210" fill="url(#rHero)"/>
+  <rect x="80" y="60" width="420" height="210" fill="rgba(248,244,235,0.08)"/>
+
+  <!-- Left side wall (secondary 30%) -->
+  <polygon points="0,340 80,270 80,60 0,60" fill="url(#rSecond)" opacity="0.9"/>
+  <polygon points="0,340 80,270 80,60 0,60" fill="url(#sideGrad)" opacity="0.6"/>
+
+  <!-- Right side wall hint -->
+  <polygon points="580,340 500,270 500,60 580,60" fill="${pal.ground}" opacity="0.5"/>
+  <polygon points="580,340 500,270 500,60 580,60" fill="rgba(0,0,0,0.12)"/>
+
+  <!-- Floor -->
+  <polygon points="0,340 580,340 500,270 80,270" fill="url(#floorGrad)"/>
+  <polygon points="0,340 580,340 500,270 80,270" fill="rgba(0,0,0,0.04)"/>
+
+  <!-- Baseboard / trim (accent 10%) -->
+  <rect x="80" y="253" width="420" height="17" fill="url(#rAccent)" opacity="0.85"/>
+  <rect x="80" y="253" width="420" height="17" fill="rgba(255,255,255,0.15)"/>
+
+  <!-- Window frame -->
+  <rect x="220" y="90" width="140" height="130" fill="rgba(255,255,255,0.22)" stroke="${pal.accent}" stroke-width="3" rx="2"/>
+  <rect x="226" y="96" width="128" height="118" fill="rgba(200,220,240,0.35)"/>
+  <line x1="290" y1="96" x2="290" y2="214" stroke="${pal.accent}" stroke-width="1.5" opacity="0.7"/>
+  <line x1="226" y1="155" x2="354" y2="155" stroke="${pal.accent}" stroke-width="1.5" opacity="0.7"/>
+
+  <!-- Chair rail moulding -->
+  <rect x="80" y="170" width="420" height="5" fill="${pal.accent}" opacity="0.4"/>
+
+  <!-- Accent cushion on implied bench -->
+  <rect x="100" y="248" width="55" height="18" fill="url(#rAccent)" rx="3" opacity="0.9"/>
+  <rect x="425" y="248" width="55" height="18" fill="url(#rAccent)" rx="3" opacity="0.9"/>
+
+  <!-- Outline edges -->
+  <polyline points="80,60 80,270" stroke="rgba(0,0,0,.15)" stroke-width="1.5" fill="none"/>
+  <polyline points="500,60 500,270" stroke="rgba(0,0,0,.1)" stroke-width="1" fill="none"/>
+  <line x1="80" y1="60" x2="500" y2="60" stroke="rgba(0,0,0,.12)" stroke-width="1"/>
+
+  <!-- Labels -->
+  <text x="290" y="52" text-anchor="middle" font-size="9" fill="rgba(28,26,23,.5)" font-family="sans-serif" letter-spacing="2">BACK WALL — 60%</text>
+  <text x="36" y="185" text-anchor="middle" font-size="8" fill="rgba(255,255,255,.7)" font-family="sans-serif" transform="rotate(-90,36,185)">30%</text>
+  <text x="372" y="262" text-anchor="middle" font-size="8" fill="rgba(28,26,23,.45)" font-family="sans-serif" letter-spacing="1">ACCENT — 10%</text>
+</svg>`;
+}
+
+// ─── Reshuffle ────────────────────────────────────────────────────────────────
+
+document.getElementById('reshuffleBtn').addEventListener('click',()=>{
+  if(!activeHeroId) return;
+  const hero = HEROES.find(h=>h.id===activeHeroId);
+  activeVariant = (activeVariant+1) % hero.coords.length;
+  renderScheme(true);
+});
+
+// ─── Save to Board ────────────────────────────────────────────────────────────
+
+document.getElementById('saveBtn').addEventListener('click',()=>{
+  if(!activeHeroId) return;
+  const hero = HEROES.find(h=>h.id===activeHeroId);
+  const coord = hero.coords[activeVariant % hero.coords.length];
+  const entry = {
+    id: Date.now(),
+    heroId: hero.id,
+    heroName: hero.name,
+    heroColorway: hero.colorway,
+    variant: activeVariant,
+    secondaryName: coord.secondary.name,
+    accentName: coord.accent.name,
+    heroSvg: hero.svg(80,80),
+    secondarySvg: coord.secondary.svg(80,80),
+    accentSvg: coord.accent.svg(80,80)
+  };
+  board.push(entry);
+  localStorage.setItem('dw-pairing-board', JSON.stringify(board));
+  const btn = document.getElementById('saveBtn');
+  const orig = btn.textContent;
+  btn.textContent = 'Saved!';
+  setTimeout(()=>btn.textContent=orig,1200);
+});
+
+// ─── Board Overlay ────────────────────────────────────────────────────────────
+
+function openBoard(){
+  renderBoard();
+  document.getElementById('boardOverlay').classList.add('open');
+  document.getElementById('closeBoardBtn').focus();
+}
+function closeBoard(){
+  document.getElementById('boardOverlay').classList.remove('open');
+}
+document.getElementById('boardBtn').addEventListener('click',openBoard);
+document.getElementById('closeBoardBtn').addEventListener('click',closeBoard);
+document.getElementById('boardOverlay').addEventListener('click',e=>{
+  if(e.target===e.currentTarget) closeBoard();
+});
+document.addEventListener('keydown',e=>{if(e.key==='Escape') closeBoard();});
+
+function renderBoard(){
+  const list = document.getElementById('boardList');
+  if(!board.length){
+    list.innerHTML='<div class="empty-board">No saved schemes yet. Build a pairing and click Save.</div>';
+    return;
+  }
+  list.innerHTML = board.map((b,i)=>`
+    <div class="board-item">
+      <div>
+        <strong style="font-family:var(--serif);font-size:16px">${b.heroName}</strong>
+        <span style="font-size:11px;color:var(--ink2);display:block;margin-bottom:6px">${b.heroColorway} · ${b.secondaryName} · ${b.accentName}</span>
+        <div class="board-item-swatches">
+          <div class="board-mini">${b.heroSvg}</div>
+          <div class="board-mini">${b.secondarySvg}</div>
+          <div class="board-mini">${b.accentSvg}</div>
+        </div>
+      </div>
+      <button class="board-del" data-idx="${i}" title="Remove scheme">&times;</button>
+    </div>
+  `).join('');
+  list.querySelectorAll('.board-del').forEach(btn=>{
+    btn.addEventListener('click',()=>{
+      board.splice(parseInt(btn.dataset.idx),1);
+      localStorage.setItem('dw-pairing-board',JSON.stringify(board));
+      renderBoard();
+    });
+  });
+}
+
+// ─── Export ───────────────────────────────────────────────────────────────────
+
+document.getElementById('exportBtn').addEventListener('click',()=>{
+  if(!activeHeroId) return;
+  const hero = HEROES.find(h=>h.id===activeHeroId);
+  const coord = hero.coords[activeVariant % hero.coords.length];
+  const swatches = [
+    {pct:'60%',name:hero.name,svg:hero.svg(180,180)},
+    {pct:'30%',name:coord.secondary.name,svg:coord.secondary.svg(180,180)},
+    {pct:'10%',name:coord.accent.name,svg:coord.accent.svg(180,180)}
+  ];
+
+  const exportSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="900" height="360">
+  <rect width="900" height="360" fill="#f4f1ea"/>
+  <text x="40" y="44" font-family="Georgia,serif" font-size="13" letter-spacing="4" fill="#1c1a17" opacity="0.5">DESIGNER WALLCOVERINGS</text>
+  <text x="40" y="68" font-family="Georgia,serif" font-size="22" fill="#1c1a17">${hero.name} — Pairing Scheme</text>
+  ${swatches.map((s,i)=>`
+    <rect x="${40+i*286}" y="90" width="260" height="200" fill="white" rx="3"/>
+    <image href="data:image/svg+xml,${encodeURIComponent(s.svg)}" x="${40+i*286}" y="90" width="260" height="155"/>
+    <text x="${40+i*286+130}" y="264" text-anchor="middle" font-family="Georgia,serif" font-size="11" fill="#c9a961" letter-spacing="2">${s.pct}</text>
+    <text x="${40+i*286+130}" y="282" text-anchor="middle" font-family="Georgia,serif" font-size="13" fill="#1c1a17">${s.name}</text>
+  `).join('')}
+  <text x="860" y="350" text-anchor="end" font-family="sans-serif" font-size="9" fill="#1c1a17" opacity="0.3">designerwallcoverings.com</text>
+</svg>`;
+
+  const blob = new Blob([exportSVG],{type:'image/svg+xml'});
+  const url = URL.createObjectURL(blob);
+  const a = document.createElement('a');
+  a.href = url;
+  a.download = `dw-pairing-${hero.id}.svg`;
+  a.click();
+  setTimeout(()=>URL.revokeObjectURL(url),2000);
+});
+
+// ─── Init ─────────────────────────────────────────────────────────────────────
+
+renderHeroGrid();
+
+// Auto-select first hero for immediate visual demo
+selectHero(HEROES[0].id);
+</script>
+</body>
+</html>
diff --git a/public/games/dw-toile-coloring.html b/public/games/dw-toile-coloring.html
new file mode 100644
index 0000000..17640d6
--- /dev/null
+++ b/public/games/dw-toile-coloring.html
@@ -0,0 +1,789 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Toile Coloring — Designer Wallcoverings</title>
+<style>
+:root {
+  --bg: #f4f1ea;
+  --ink: #1c1a17;
+  --accent: #c9a961;
+  --serif: 'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif;
+  --sans: 'Optima','Gill Sans','Trebuchet MS',sans-serif;
+  --ease: cubic-bezier(.4,0,.2,1);
+  --paper: #f4f1ea;
+  --shadow: rgba(28,26,23,.13);
+}
+*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
+html,body{height:100%;background:var(--bg);color:var(--ink);font-family:var(--sans)}
+body{display:flex;flex-direction:column;min-height:100vh}
+
+header{
+  padding:18px 32px 14px;
+  border-bottom:1px solid rgba(28,26,23,.12);
+  display:flex;align-items:center;gap:18px;
+  background:var(--bg);
+  position:sticky;top:0;z-index:10;
+}
+.wordmark{
+  font-family:var(--serif);
+  font-size:clamp(22px,3vw,40px);
+  letter-spacing:.18em;
+  text-transform:uppercase;
+  color:var(--ink);
+  line-height:1;
+  font-weight:400;
+}
+.wordmark span{color:var(--accent);font-size:.7em;letter-spacing:.22em;}
+.header-sep{flex:1}
+.subtitle{
+  font-family:var(--serif);
+  font-size:13px;
+  letter-spacing:.12em;
+  text-transform:uppercase;
+  color:rgba(28,26,23,.5);
+  font-style:italic;
+}
+
+main{
+  display:flex;flex:1;
+  flex-wrap:wrap;
+  gap:0;
+  align-items:flex-start;
+  justify-content:center;
+  padding:24px 16px;
+  max-width:1200px;
+  margin:0 auto;
+  width:100%;
+}
+
+/* SIDEBAR */
+aside{
+  width:220px;
+  flex-shrink:0;
+  display:flex;flex-direction:column;gap:20px;
+  padding-right:20px;
+}
+.panel{
+  background:rgba(255,255,255,.6);
+  border:1px solid rgba(201,169,97,.22);
+  border-radius:4px;
+  padding:16px;
+}
+.panel-title{
+  font-family:var(--serif);
+  font-size:11px;
+  letter-spacing:.18em;
+  text-transform:uppercase;
+  color:rgba(28,26,23,.5);
+  margin-bottom:12px;
+}
+
+/* PALETTE */
+.palette{display:flex;flex-wrap:wrap;gap:8px}
+.swatch{
+  width:36px;height:36px;
+  border-radius:50%;
+  border:2.5px solid transparent;
+  cursor:pointer;
+  transition:transform .15s var(--ease),border-color .15s var(--ease),box-shadow .15s var(--ease);
+  position:relative;
+}
+.swatch:hover{transform:scale(1.12)}
+.swatch.active{
+  border-color:var(--ink);
+  box-shadow:0 0 0 2px var(--bg),0 0 0 4px var(--ink);
+}
+.swatch-label{
+  font-size:10px;
+  letter-spacing:.06em;
+  color:rgba(28,26,23,.5);
+  text-align:center;
+  margin-top:6px;
+  font-family:var(--serif);
+  font-style:italic;
+}
+
+/* PAPER GROUND */
+.paper-opts{display:flex;flex-direction:column;gap:6px}
+.paper-opt{
+  display:flex;align-items:center;gap:8px;
+  cursor:pointer;
+  font-family:var(--serif);
+  font-size:13px;
+  letter-spacing:.04em;
+  color:var(--ink);
+}
+.paper-chip{
+  width:24px;height:24px;
+  border-radius:3px;
+  border:1.5px solid rgba(28,26,23,.2);
+}
+.paper-opt input{display:none}
+.paper-opt.active .paper-chip{border-color:var(--ink);box-shadow:0 0 0 2px var(--ink)}
+
+/* BUTTONS */
+.btn-row{display:flex;flex-direction:column;gap:8px}
+.btn{
+  padding:9px 14px;
+  border:1.5px solid rgba(28,26,23,.25);
+  border-radius:3px;
+  background:transparent;
+  color:var(--ink);
+  font-family:var(--serif);
+  font-size:13px;
+  letter-spacing:.08em;
+  cursor:pointer;
+  transition:background .15s var(--ease),border-color .15s var(--ease);
+  text-align:left;
+}
+.btn:hover{background:rgba(201,169,97,.12);border-color:var(--accent)}
+.btn.accent{
+  background:var(--accent);
+  border-color:var(--accent);
+  color:#fff;
+  font-weight:600;
+}
+.btn.accent:hover{background:#b8923e}
+.btn:disabled{opacity:.4;cursor:not-allowed}
+
+/* CANVAS AREA */
+.canvas-wrap{
+  flex:1;
+  min-width:300px;
+  display:flex;
+  flex-direction:column;
+  align-items:center;
+  gap:12px;
+}
+#scene-svg{
+  background:var(--paper);
+  border:1px solid rgba(201,169,97,.3);
+  border-radius:4px;
+  box-shadow:0 4px 24px var(--shadow);
+  max-width:100%;
+  width:700px;
+  height:auto;
+  transition:background .4s var(--ease);
+  cursor:crosshair;
+  display:block;
+}
+#scene-svg .region{
+  transition:fill .35s var(--ease),opacity .15s;
+  cursor:pointer;
+  fill:none;
+  stroke:var(--ink);
+  stroke-width:1.4;
+  stroke-linejoin:round;
+  stroke-linecap:round;
+}
+#scene-svg .region:hover{opacity:.8;stroke-width:2}
+#scene-svg .region.filled{stroke-width:1.2}
+#scene-svg .decorative{
+  fill:none;stroke:var(--ink);
+  stroke-width:1.2;
+  stroke-linecap:round;
+  pointer-events:none;
+}
+#scene-svg .bg-layer{
+  fill:var(--paper);
+  stroke:none;
+  pointer-events:none;
+  transition:fill .4s var(--ease);
+}
+
+/* sparkle */
+.sparkle-dot{
+  position:fixed;
+  width:6px;height:6px;
+  border-radius:50%;
+  pointer-events:none;
+  opacity:1;
+  transform:translate(-50%,-50%) scale(1);
+  animation:sparkle-out .55s var(--ease) forwards;
+  z-index:999;
+}
+@keyframes sparkle-out{
+  0%{opacity:1;transform:translate(-50%,-50%) scale(1)}
+  100%{opacity:0;transform:translate(var(--tx),var(--ty)) scale(0)}
+}
+
+/* undo status */
+.status-bar{
+  font-family:var(--serif);
+  font-size:11px;
+  letter-spacing:.1em;
+  color:rgba(28,26,23,.4);
+  font-style:italic;
+  height:16px;
+}
+
+/* toast */
+#toast{
+  position:fixed;bottom:28px;left:50%;transform:translateX(-50%);
+  background:var(--ink);color:#f4f1ea;
+  padding:8px 20px;border-radius:3px;
+  font-family:var(--serif);font-size:13px;letter-spacing:.06em;
+  visibility:hidden;opacity:0;transition:opacity .3s;
+  z-index:100;
+  pointer-events:none;
+}
+#toast.show{visibility:visible;opacity:1}
+
+@media(max-width:640px){
+  aside{width:100%;padding-right:0;flex-direction:row;flex-wrap:wrap}
+  .panel{flex:1 1 140px}
+  main{padding:12px 8px}
+  header{padding:12px 16px}
+}
+</style>
+</head>
+<body>
+
+<header>
+  <div>
+    <div class="wordmark">Designer Wallcoverings<br><span>Toile Coloring</span></div>
+  </div>
+  <div class="header-sep"></div>
+  <div class="subtitle">A Toile de Jouy Experience</div>
+</header>
+
+<main>
+  <aside>
+    <!-- PALETTE -->
+    <div class="panel">
+      <div class="panel-title">Colour</div>
+      <div class="palette" id="palette"></div>
+      <div class="swatch-label" id="swatch-name">Toile Blue</div>
+    </div>
+
+    <!-- PAPER GROUND -->
+    <div class="panel">
+      <div class="panel-title">Paper Ground</div>
+      <div class="paper-opts" id="paper-opts">
+        <label class="paper-opt active" data-paper="#f4f1ea">
+          <input type="radio" name="paper" value="#f4f1ea" checked>
+          <span class="paper-chip" style="background:#f4f1ea"></span>
+          Cream
+        </label>
+        <label class="paper-opt" data-paper="#f0ede6">
+          <input type="radio" name="paper" value="#f0ede6">
+          <span class="paper-chip" style="background:#f0ede6"></span>
+          Alabaster
+        </label>
+        <label class="paper-opt" data-paper="#e8e2d8">
+          <input type="radio" name="paper" value="#e8e2d8">
+          <span class="paper-chip" style="background:#e8e2d8"></span>
+          Oyster
+        </label>
+      </div>
+    </div>
+
+    <!-- ACTIONS -->
+    <div class="panel">
+      <div class="panel-title">Actions</div>
+      <div class="btn-row">
+        <button class="btn accent" id="btn-toile">Toile Classic</button>
+        <button class="btn" id="btn-undo">Undo</button>
+        <button class="btn" id="btn-redo">Redo</button>
+        <button class="btn" id="btn-reset">Reset to Line Art</button>
+        <button class="btn" id="btn-download">Download PNG</button>
+      </div>
+    </div>
+  </aside>
+
+  <div class="canvas-wrap">
+    <div class="status-bar" id="status"></div>
+    <!-- TOILE SVG SCENE -->
+    <svg id="scene-svg" viewBox="0 0 700 560" xmlns="http://www.w3.org/2000/svg" aria-label="Toile de Jouy coloring scene">
+      <!-- Background fill layer -->
+      <rect class="bg-layer" id="bg-rect" x="0" y="0" width="700" height="560"/>
+
+      <!-- ═══════════ BORDER SCROLLWORK ═══════════ -->
+      <!-- Outer border frame -->
+      <rect class="decorative" x="14" y="14" width="672" height="532" rx="4" stroke-width="1.8"/>
+      <rect class="decorative" x="20" y="20" width="660" height="520" rx="2" stroke-width=".7"/>
+
+      <!-- Corner flourishes TL -->
+      <path class="decorative" d="M14,14 Q30,14 30,30 M14,14 Q14,30 30,30
+        M22,22 C28,18 36,16 40,22 C44,28 42,36 36,38
+        M22,22 C18,28 16,36 22,40 C28,44 36,42 38,36"/>
+      <!-- Corner flourishes TR -->
+      <path class="decorative" d="M686,14 Q670,14 670,30 M686,14 Q686,30 670,30
+        M678,22 C672,18 664,16 660,22 C656,28 658,36 664,38
+        M678,22 C682,28 684,36 678,40 C672,44 664,42 662,36"/>
+      <!-- Corner flourishes BL -->
+      <path class="decorative" d="M14,546 Q30,546 30,530 M14,546 Q14,530 30,530
+        M22,538 C28,542 36,544 40,538 C44,532 42,524 36,522
+        M22,538 C18,532 16,524 22,520 C28,516 36,518 38,524"/>
+      <!-- Corner flourishes BR -->
+      <path class="decorative" d="M686,546 Q670,546 670,530 M686,546 Q686,530 670,530
+        M678,538 C672,542 664,544 660,538 C656,532 658,524 664,522
+        M678,538 C682,532 684,524 678,520 C672,516 664,518 662,524"/>
+
+      <!-- Top border vine -->
+      <path class="decorative" d="M50,17 C100,12 150,22 200,17 C250,12 300,22 350,17 C400,12 450,22 500,17 C550,12 600,22 650,17" stroke-width=".8"/>
+      <!-- Top little leaves -->
+      <path class="decorative" d="M120,17 C122,14 126,14 124,17 M200,17 C202,14 206,14 204,17 M280,17 C282,14 286,14 284,17 M360,17 C362,14 366,14 364,17 M440,17 C442,14 446,14 444,17 M520,17 C522,14 526,14 524,17" stroke-width=".9"/>
+      <!-- Bottom border vine -->
+      <path class="decorative" d="M50,543 C100,548 150,538 200,543 C250,548 300,538 350,543 C400,548 450,538 500,543 C550,548 600,538 650,543" stroke-width=".8"/>
+      <path class="decorative" d="M120,543 C122,546 126,546 124,543 M200,543 C202,546 206,546 204,543 M280,543 C282,546 286,546 284,543 M360,543 C362,546 366,546 364,543 M440,543 C442,546 446,546 444,543 M520,543 C522,546 526,546 524,543" stroke-width=".9"/>
+
+      <!-- ═══════════ SKY / BACKGROUND LANDSCAPE ═══════════ -->
+      <!-- Sky region -->
+      <path id="r-sky" class="region" d="M35,35 H665 V200 Q580,180 520,190 Q460,200 400,185 Q340,170 280,188 Q220,205 160,190 Q100,175 35,195 Z"/>
+
+      <!-- Ground / lawn -->
+      <path id="r-lawn" class="region" d="M35,400 Q100,390 160,395 Q220,400 280,392 Q340,385 400,392 Q460,400 520,390 Q580,382 665,395 V525 H35 Z"/>
+
+      <!-- ═══════════ CHATEAU ═══════════ -->
+      <!-- Chateau main facade -->
+      <path id="r-chateau-facade" class="region" d="M220,195 H480 V370 H220 Z"/>
+
+      <!-- Chateau roof -->
+      <path id="r-chateau-roof" class="region" d="M200,200 L350,130 L500,200 Z"/>
+
+      <!-- Roof dormer left -->
+      <path id="r-dormer-l" class="region" d="M260,183 L280,155 L300,183 Z"/>
+      <!-- Roof dormer right -->
+      <path id="r-dormer-r" class="region" d="M400,183 L420,155 L440,183 Z"/>
+
+      <!-- Chateau left wing -->
+      <path id="r-wing-l" class="region" d="M160,235 H225 V370 H160 Z"/>
+      <!-- Chateau right wing -->
+      <path id="r-wing-r" class="region" d="M475,235 H540 V370 H475 Z"/>
+
+      <!-- Wing roofs -->
+      <path id="r-wing-roof-l" class="region" d="M148,238 L192,210 L235,238 Z"/>
+      <path id="r-wing-roof-r" class="region" d="M465,238 L508,210 L550,238 Z"/>
+
+      <!-- Chateau door -->
+      <path id="r-door" class="region" d="M315,285 H385 Q385,340 350,355 Q315,340 315,285 Z"/>
+
+      <!-- Chateau windows upper row -->
+      <path id="r-win-u1" class="region" d="M238,215 H278 Q278,245 258,250 Q238,245 238,215 Z"/>
+      <path id="r-win-u2" class="region" d="M310,215 H360 Q360,245 335,250 Q310,245 310,215 Z"/>
+      <path id="r-win-u3" class="region" d="M382,215 H432 Q432,245 407,250 Q382,245 382,215 Z"/>
+      <path id="r-win-u4" class="region" d="M454,215 H494 Q494,245 474,250 Q454,245 454,215 Z"/>
+
+      <!-- Chateau windows lower row -->
+      <path id="r-win-l1" class="region" d="M238,268 H278 Q278,298 258,303 Q238,298 238,268 Z"/>
+      <path id="r-win-l2" class="region" d="M382,268 H432 Q432,298 407,303 Q382,298 382,268 Z"/>
+      <path id="r-win-l3" class="region" d="M454,268 H494 Q494,298 474,303 Q454,298 454,268 Z"/>
+
+      <!-- Wing windows -->
+      <path id="r-win-wl" class="region" d="M172,255 H213 Q213,280 192,284 Q172,280 172,255 Z"/>
+      <path id="r-win-wr" class="region" d="M488,255 H528 Q528,280 508,284 Q488,280 488,255 Z"/>
+
+      <!-- Decorative window tracery (non-fillable) -->
+      <path class="decorative" d="M258,215 V245 M248,230 H268 M258,215 Q263,222 258,228 Q253,222 258,215" stroke-width=".9"/>
+      <path class="decorative" d="M335,215 V245 M322,230 H348 M335,215 Q341,222 335,228 Q329,222 335,215" stroke-width=".9"/>
+      <path class="decorative" d="M407,215 V245 M394,230 H420 M407,215 Q413,222 407,228 Q401,222 407,215" stroke-width=".9"/>
+      <path class="decorative" d="M474,215 V245 M461,230 H487 M474,215 Q480,222 474,228 Q468,222 474,215" stroke-width=".9"/>
+      <path class="decorative" d="M258,268 V298 M248,283 H268" stroke-width=".9"/>
+      <path class="decorative" d="M407,268 V298 M394,283 H420" stroke-width=".9"/>
+      <path class="decorative" d="M474,268 V298 M461,283 H487" stroke-width=".9"/>
+
+      <!-- Door arch detail -->
+      <path class="decorative" d="M350,285 V350 M332,320 H368 M350,285 Q362,305 362,320 Q362,340 350,354" stroke-width=".9"/>
+
+      <!-- Chimney stacks -->
+      <path id="r-chimney-l" class="region" d="M270,130 H295 V160 H270 Z"/>
+      <path id="r-chimney-r" class="region" d="M405,130 H430 V160 H405 Z"/>
+
+      <!-- Chateau balustrade / terrace -->
+      <path id="r-terrace" class="region" d="M180,365 H520 V385 Q350,395 180,385 Z"/>
+
+      <!-- Terrace baluster posts (decorative) -->
+      <path class="decorative" d="M200,365 V385 M220,365 V385 M240,365 V385 M260,365 V385 M280,365 V385 M300,365 V385 M320,365 V385 M340,365 V385 M360,365 V385 M380,365 V385 M400,365 V385 M420,365 V385 M440,365 V385 M460,365 V385 M480,365 V385 M500,365 V385" stroke-width=".8"/>
+      <path class="decorative" d="M205,370 Q210,366 215,370 Q210,374 205,370 M225,370 Q230,366 235,370 Q230,374 225,370 M245,370 Q250,366 255,370 Q250,374 245,370 M265,370 Q270,366 275,370 Q270,374 265,370 M285,370 Q290,366 295,370 Q290,374 285,370 M305,370 Q310,366 315,370 Q310,374 305,370 M325,370 Q330,366 335,370 Q330,374 325,370 M345,370 Q350,366 355,370 Q350,374 345,370 M365,370 Q370,366 375,370 Q370,374 365,370 M385,370 Q390,366 395,370 Q390,374 385,370 M405,370 Q410,366 415,370 Q410,374 405,370 M425,370 Q430,366 435,370 Q430,374 425,370 M445,370 Q450,366 455,370 Q450,374 445,370 M465,370 Q470,366 475,370 Q470,374 465,370 M485,370 Q490,366 495,370 Q490,374 485,370" stroke-width=".7"/>
+
+      <!-- ═══════════ FOUNTAIN ═══════════ -->
+      <!-- Fountain basin -->
+      <ellipse id="r-basin" class="region" cx="350" cy="465" rx="80" ry="22"/>
+      <!-- Fountain bowl mid -->
+      <ellipse id="r-bowl-mid" class="region" cx="350" cy="448" rx="38" ry="12"/>
+      <!-- Fountain pillar -->
+      <path id="r-fountain-pillar" class="region" d="M340,450 H360 V430 Q354,428 354,422 Q350,418 346,422 Q346,428 340,430 Z"/>
+      <!-- Fountain top cup -->
+      <ellipse id="r-cup-top" class="region" cx="350" cy="418" rx="22" ry="7"/>
+      <!-- Water arcs (decorative) -->
+      <path class="decorative" d="M350,412 Q365,400 375,410 M350,412 Q335,400 325,410 M350,412 Q358,395 355,408 M350,412 Q342,395 345,408" stroke-width="1.1"/>
+      <!-- Water ripple in basin -->
+      <ellipse class="decorative" cx="350" cy="465" rx="60" ry="14" stroke-width=".7"/>
+      <ellipse class="decorative" cx="350" cy="465" rx="35" ry="8" stroke-width=".6"/>
+      <!-- Fountain pedestal -->
+      <path id="r-pedestal" class="region" d="M326,483 H374 V492 H326 Z"/>
+
+      <!-- ═══════════ TREES ═══════════ -->
+      <!-- Left large cypress-style tree -->
+      <path id="r-tree-l1" class="region" d="M85,195 Q100,160 115,140 Q122,125 115,115 Q108,125 100,130 Q108,118 105,105 Q98,118 90,125 Q98,112 94,100 Q86,115 82,130 Q76,115 72,100 Q68,118 72,130 Q64,120 60,108 Q58,125 66,135 Q72,148 78,165 Q80,178 85,195 Z"/>
+      <!-- Left cypress foliage extra -->
+      <path id="r-tree-l1b" class="region" d="M85,195 Q78,210 74,230 Q70,252 78,272 Q86,285 90,295 Q98,305 105,295 Q115,280 118,262 Q122,240 115,215 Q108,202 98,195 Z"/>
+
+      <!-- Left medium tree -->
+      <path id="r-tree-l2" class="region" d="M55,320 Q62,295 70,278 Q78,262 72,250 Q66,262 62,270 Q68,255 65,245 Q58,258 54,268 Q58,252 55,242 Q48,256 46,270 Q40,254 38,242 Q36,258 40,270 Q34,258 30,248 Q28,265 36,277 Q44,293 48,312 Q50,316 55,320 Z"/>
+
+      <!-- Right large tree -->
+      <path id="r-tree-r1" class="region" d="M600,195 Q614,165 626,145 Q633,130 628,118 Q620,128 614,136 Q620,122 617,110 Q610,122 603,130 Q610,115 606,103 Q598,118 595,132 Q588,115 585,103 Q581,120 585,133 Q578,120 574,110 Q572,128 580,140 Q588,155 592,175 Q596,185 600,195 Z"/>
+      <path id="r-tree-r1b" class="region" d="M600,195 Q593,212 589,235 Q585,258 592,278 Q600,292 606,300 Q614,308 620,298 Q630,282 632,262 Q636,242 628,215 Q620,200 610,195 Z"/>
+
+      <!-- Right medium tree -->
+      <path id="r-tree-r2" class="region" d="M645,310 Q652,288 660,272 Q667,258 662,246 Q656,258 652,266 Q658,252 655,242 Q648,255 644,265 Q648,249 645,239 Q638,253 636,267 Q630,250 628,239 Q626,256 630,268 Q624,255 620,245 Q618,263 626,275 Q634,290 638,308 Q641,310 645,310 Z"/>
+
+      <!-- ═══════════ URNS / PLANTERS ═══════════ -->
+      <!-- Left urn -->
+      <path id="r-urn-l" class="region" d="M118,370 Q108,360 108,348 Q108,330 118,322 Q128,314 138,322 Q148,330 148,348 Q148,360 138,370 Q148,372 152,378 H104 Q108,372 118,370 Z"/>
+      <path id="r-urn-l-plant" class="region" d="M128,318 Q122,305 116,295 Q120,290 128,295 Q128,285 124,278 Q130,282 132,290 Q136,282 134,272 Q140,278 138,288 Q143,280 146,272 Q147,283 142,292 Q148,288 150,282 Q148,296 140,304 Q135,310 128,318 Z"/>
+      <!-- Left urn pedestal -->
+      <path id="r-urn-ped-l" class="region" d="M116,378 H140 V392 H116 Z"/>
+
+      <!-- Right urn -->
+      <path id="r-urn-r" class="region" d="M560,370 Q550,360 550,348 Q550,330 560,322 Q570,314 580,322 Q590,330 590,348 Q590,360 580,370 Q590,372 594,378 H546 Q550,372 560,370 Z"/>
+      <path id="r-urn-r-plant" class="region" d="M570,318 Q564,305 558,295 Q562,290 570,295 Q570,285 566,278 Q572,282 574,290 Q578,282 576,272 Q582,278 580,288 Q585,280 588,272 Q589,283 584,292 Q590,288 592,282 Q590,296 582,304 Q577,310 570,318 Z"/>
+      <!-- Right urn pedestal -->
+      <path id="r-urn-ped-r" class="region" d="M558,378 H582 V392 H558 Z"/>
+
+      <!-- ═══════════ GARDEN PATH ═══════════ -->
+      <path id="r-path" class="region" d="M280,390 Q310,388 350,390 Q390,392 420,390 L430,530 H270 Z"/>
+      <!-- Path stones (decorative) -->
+      <path class="decorative" d="M295,420 Q350,416 405,420 M290,440 Q350,436 410,440 M285,460 Q350,456 415,460 M282,480 Q350,476 418,480 M278,500 Q350,496 422,500 M275,520 Q350,516 425,520" stroke-width=".7"/>
+
+      <!-- ═══════════ FIGURES ═══════════ -->
+      <!-- Figure 1 - left side, standing woman with basket -->
+      <path id="r-fig1-skirt" class="region" d="M130,430 Q122,415 124,400 Q130,390 138,393 Q146,395 148,405 Q152,418 144,432 Z"/>
+      <path id="r-fig1-bodice" class="region" d="M124,400 Q122,388 126,380 Q130,374 138,376 Q144,378 146,386 Q148,394 144,400 Q138,400 132,400 Z"/>
+      <path id="r-fig1-head" class="region" d="M128,378 Q130,368 138,366 Q146,368 148,378 Q146,386 138,386 Q130,386 128,378 Z"/>
+      <!-- Fig 1 hat -->
+      <path class="decorative" d="M128,374 Q138,366 148,374 M132,374 Q138,368 144,374" stroke-width="1"/>
+      <!-- Fig 1 arms/basket -->
+      <path class="decorative" d="M124,388 Q116,395 114,400 Q116,403 120,402 M146,386 Q155,390 158,396 Q162,398 164,395 Q160,390 155,388" stroke-width="1"/>
+      <path id="r-basket" class="region" d="M158,393 Q158,388 165,386 Q172,386 172,393 Q172,400 165,400 Q158,400 158,393 Z"/>
+      <!-- Basket weave (decorative) -->
+      <path class="decorative" d="M160,390 H170 M161,394 H169 M163,388 V398 M165,388 V398 M167,388 V398" stroke-width=".7"/>
+
+      <!-- Figure 2 - right side, seated figure under tree -->
+      <path id="r-fig2-robe" class="region" d="M540,440 Q535,420 538,405 Q542,396 550,397 Q558,398 562,408 Q565,420 560,440 Z"/>
+      <path id="r-fig2-head" class="region" d="M540,404 Q542,394 550,392 Q558,394 560,404 Q558,412 550,412 Q542,412 540,404 Z"/>
+      <!-- Fig 2 hat/hair -->
+      <path class="decorative" d="M540,402 Q550,392 560,402 M543,398 Q550,393 557,398" stroke-width="1"/>
+      <!-- Fig 2 reading book -->
+      <path class="decorative" d="M555,415 Q562,413 568,418 Q562,424 555,422 Q555,420 558,418 Q555,416 555,415 M568,418 Q568,422 565,424 M558,418 V422" stroke-width=".9"/>
+
+      <!-- ═══════════ BIRDS IN FLIGHT ═══════════ -->
+      <!-- Bird cluster top-left sky -->
+      <path id="r-birds-l" class="region" d="M90,80 Q96,74 102,78 Q96,82 90,80 Z M100,72 Q106,66 112,70 Q106,74 100,72 Z M82,90 Q88,84 94,88 Q88,92 82,90 Z"/>
+      <!-- Bird cluster top-right sky -->
+      <path id="r-birds-r" class="region" d="M580,70 Q586,64 592,68 Q586,72 580,70 Z M591,60 Q597,54 603,58 Q597,62 591,60 Z M600,78 Q606,72 612,76 Q606,80 600,78 Z"/>
+      <!-- Bird center-upper -->
+      <path id="r-bird-c" class="region" d="M338,62 Q344,56 350,60 Q344,64 338,62 Z M350,55 Q356,49 362,53 Q356,57 350,55 Z"/>
+
+      <!-- ═══════════ CLOUDS ═══════════ -->
+      <path id="r-cloud-l" class="region" d="M60,100 Q65,88 75,86 Q80,78 90,80 Q98,72 108,76 Q118,70 125,78 Q133,76 136,84 Q140,88 136,96 Q130,100 120,100 Q110,100 100,100 Q90,100 80,100 Z"/>
+      <path id="r-cloud-r" class="region" d="M570,90 Q575,78 585,76 Q590,68 600,70 Q608,62 618,66 Q628,60 635,68 Q643,66 646,74 Q650,78 646,86 Q640,90 630,90 Q620,90 610,90 Q600,90 590,90 Z"/>
+
+      <!-- ═══════════ FOREGROUND FLOWERING SHRUBS ═══════════ -->
+      <!-- Left foreground shrub -->
+      <path id="r-shrub-l" class="region" d="M36,480 Q44,460 56,452 Q62,445 70,450 Q76,442 84,448 Q90,440 96,446 Q104,436 108,445 Q116,437 118,448 Q126,440 124,453 Q130,446 128,460 Q132,468 126,475 Q118,482 108,478 Q100,485 90,480 Q80,488 70,482 Q60,488 50,482 Q42,486 36,480 Z"/>
+      <!-- Shrub flowers left (decorative) -->
+      <path class="decorative" d="M70,450 Q70,444 76,444 Q82,444 82,450 Q82,456 76,456 Q70,456 70,450 M70,450 Q66,446 62,448 M70,450 Q68,456 62,458 M82,450 Q86,446 90,448 M82,450 Q84,456 90,458 M76,444 Q76,438 76,434 M90,448 Q94,442 100,444 Q100,450 94,452 Q94,448 90,448 M108,445 Q112,439 118,441 Q118,447 112,449 M62,448 Q56,444 50,448 Q50,454 56,456" stroke-width=".9"/>
+
+      <!-- Right foreground shrub -->
+      <path id="r-shrub-r" class="region" d="M576,480 Q584,460 596,452 Q602,445 610,450 Q616,442 624,448 Q630,440 636,446 Q644,436 648,445 Q656,437 658,448 Q666,440 664,453 Q670,446 668,460 Q672,468 666,475 Q658,482 648,478 Q640,485 630,480 Q620,488 610,482 Q600,488 590,482 Q582,486 576,480 Z"/>
+      <!-- Shrub flowers right (decorative) -->
+      <path class="decorative" d="M610,450 Q610,444 616,444 Q622,444 622,450 Q622,456 616,456 Q610,456 610,450 M610,450 Q606,446 602,448 M610,450 Q608,456 602,458 M622,450 Q626,446 630,448 M622,450 Q624,456 630,458 M616,444 Q616,438 616,434 M630,448 Q634,442 640,444 Q640,450 634,452 M648,445 Q652,439 658,441 Q658,447 652,449 M602,448 Q596,444 590,448 Q590,454 596,456" stroke-width=".9"/>
+
+      <!-- ═══════════ SCROLLWORK CARTOUCHE (title area) ═══════════ -->
+      <!-- Bottom center cartouche -->
+      <path class="decorative" d="M240,510 Q260,502 290,502 Q320,500 350,502 Q380,500 410,502 Q440,502 460,510 Q470,515 460,520 Q440,524 410,522 Q380,520 350,522 Q320,520 290,522 Q260,524 240,520 Q230,515 240,510 Z" stroke-width="1.1"/>
+      <path class="decorative" d="M244,512 Q260,506 290,506 Q320,504 350,506 Q380,504 410,506 Q440,506 456,512" stroke-width=".7"/>
+      <!-- Cartouche side scrolls -->
+      <path class="decorative" d="M240,515 Q232,510 228,505 Q225,500 228,495 Q232,492 237,496 Q240,500 237,505" stroke-width=".9"/>
+      <path class="decorative" d="M460,515 Q468,510 472,505 Q475,500 472,495 Q468,492 463,496 Q460,500 463,505" stroke-width=".9"/>
+    </svg>
+    <div class="status-bar" id="status2"></div>
+  </div>
+</main>
+
+<div id="toast"></div>
+
+<script>
+(function(){
+'use strict';
+
+// ── PALETTE ──────────────────────────────────────────────
+const COLORS = [
+  {name:'Toile Blue',   hex:'#3d6b8e'},
+  {name:'Faded Rose',   hex:'#c87b7b'},
+  {name:'Sage',         hex:'#7a9a7e'},
+  {name:'Sepia',        hex:'#8b6240'},
+  {name:'Aubergine',    hex:'#5a3a5a'},
+  {name:'Ink',          hex:'#1c1a17'},
+  {name:'Celadon',      hex:'#a8c4a2'},
+  {name:'Gilt',         hex:'#c9a961'},
+];
+const TOILE_CLASSIC = '#3d6b8e'; // traditional single-color
+
+// ── PAPER GROUNDS ────────────────────────────────────────
+const PAPERS = {
+  '#f4f1ea': 'Cream',
+  '#f0ede6': 'Alabaster',
+  '#e8e2d8': 'Oyster',
+};
+
+// ── STATE ────────────────────────────────────────────────
+let activeColor = COLORS[0].hex;
+let history = [];  // [{id, prevFill, nextFill}, ...]
+let future = [];
+let currentPaper = '#f4f1ea';
+
+const svg = document.getElementById('scene-svg');
+const bgRect = document.getElementById('bg-rect');
+const statusEl = document.getElementById('status');
+const status2El = document.getElementById('status2');
+const toastEl = document.getElementById('toast');
+
+// ── BUILD PALETTE UI ─────────────────────────────────────
+const paletteEl = document.getElementById('palette');
+const swatchNameEl = document.getElementById('swatch-name');
+COLORS.forEach(c => {
+  const div = document.createElement('div');
+  div.className = 'swatch' + (c.hex === activeColor ? ' active' : '');
+  div.style.background = c.hex;
+  div.title = c.name;
+  div.setAttribute('aria-label', c.name);
+  div.addEventListener('click', () => {
+    document.querySelectorAll('.swatch').forEach(s => s.classList.remove('active'));
+    div.classList.add('active');
+    activeColor = c.hex;
+    swatchNameEl.textContent = c.name;
+  });
+  paletteEl.appendChild(div);
+});
+
+// ── PAPER SELECTOR ───────────────────────────────────────
+document.getElementById('paper-opts').querySelectorAll('.paper-opt').forEach(opt => {
+  opt.addEventListener('click', () => {
+    document.querySelectorAll('.paper-opt').forEach(o => o.classList.remove('active'));
+    opt.classList.add('active');
+    currentPaper = opt.dataset.paper;
+    document.documentElement.style.setProperty('--paper', currentPaper);
+    bgRect.setAttribute('fill', currentPaper);
+    svg.style.background = currentPaper;
+  });
+});
+
+// ── REGION FILL LOGIC ────────────────────────────────────
+function getRegionFill(el) {
+  return el.style.fill || 'none';
+}
+function setRegionFill(el, color, recordHistory) {
+  const prev = getRegionFill(el);
+  if(recordHistory) {
+    history.push({id: el.id, prevFill: prev, nextFill: color});
+    future = [];
+    updateUndoRedo();
+  }
+  el.style.fill = color;
+  if(color === 'none') {
+    el.classList.remove('filled');
+  } else {
+    el.classList.add('filled');
+  }
+}
+
+// ── SPARKLE ──────────────────────────────────────────────
+function sparkle(x, y, color) {
+  const count = 6;
+  for(let i = 0; i < count; i++) {
+    const dot = document.createElement('div');
+    dot.className = 'sparkle-dot';
+    dot.style.left = x + 'px';
+    dot.style.top = y + 'px';
+    dot.style.background = color;
+    const angle = (i / count) * Math.PI * 2;
+    const dist = 18 + Math.random() * 14;
+    dot.style.setProperty('--tx', Math.cos(angle) * dist + 'px');
+    dot.style.setProperty('--ty', Math.sin(angle) * dist + 'px');
+    dot.style.animationDelay = (i * 0.04) + 's';
+    document.body.appendChild(dot);
+    setTimeout(() => dot.remove(), 700);
+  }
+}
+
+// ── SVG CLICK HANDLER ────────────────────────────────────
+svg.addEventListener('click', e => {
+  const target = e.target;
+  if(!target.classList.contains('region')) return;
+  const rect = target.getBoundingClientRect();
+  const cx = rect.left + rect.width / 2;
+  const cy = rect.top + rect.height / 2;
+  setRegionFill(target, activeColor, true);
+  sparkle(cx, cy, activeColor);
+  statusEl.textContent = 'Filled · ' + getColorName(activeColor);
+  status2El.textContent = '';
+});
+
+// hover highlight
+svg.addEventListener('mouseover', e => {
+  if(e.target.classList.contains('region')) {
+    e.target.style.opacity = '.75';
+  }
+});
+svg.addEventListener('mouseout', e => {
+  if(e.target.classList.contains('region')) {
+    e.target.style.opacity = '1';
+  }
+});
+
+// touch
+svg.addEventListener('touchend', e => {
+  e.preventDefault();
+  const touch = e.changedTouches[0];
+  const el = document.elementFromPoint(touch.clientX, touch.clientY);
+  if(el && el.classList.contains('region')) {
+    setRegionFill(el, activeColor, true);
+    sparkle(touch.clientX, touch.clientY, activeColor);
+    statusEl.textContent = 'Filled · ' + getColorName(activeColor);
+  }
+}, {passive: false});
+
+// ── TOILE CLASSIC ────────────────────────────────────────
+document.getElementById('btn-toile').addEventListener('click', () => {
+  const regions = svg.querySelectorAll('.region');
+  // batch as one history entry
+  const batch = [];
+  regions.forEach(r => {
+    batch.push({id: r.id, prevFill: getRegionFill(r), nextFill: TOILE_CLASSIC});
+    r.style.fill = TOILE_CLASSIC;
+    r.classList.add('filled');
+  });
+  history.push({batch});
+  future = [];
+  updateUndoRedo();
+  statusEl.textContent = 'Toile Classic applied';
+  toast('Toile Classic — traditional single-colour fill');
+});
+
+// ── UNDO / REDO ──────────────────────────────────────────
+document.getElementById('btn-undo').addEventListener('click', undo);
+document.getElementById('btn-redo').addEventListener('click', redo);
+
+function undo() {
+  if(!history.length) return;
+  const entry = history.pop();
+  future.push(entry);
+  if(entry.batch) {
+    entry.batch.forEach(({id, prevFill}) => {
+      const el = document.getElementById(id);
+      if(el) { el.style.fill = prevFill; el.classList.toggle('filled', prevFill !== 'none' && prevFill !== ''); }
+    });
+  } else {
+    const el = document.getElementById(entry.id);
+    if(el) { el.style.fill = entry.prevFill; el.classList.toggle('filled', entry.prevFill !== 'none' && entry.prevFill !== ''); }
+  }
+  updateUndoRedo();
+  statusEl.textContent = 'Undone';
+}
+function redo() {
+  if(!future.length) return;
+  const entry = future.pop();
+  history.push(entry);
+  if(entry.batch) {
+    entry.batch.forEach(({id, nextFill}) => {
+      const el = document.getElementById(id);
+      if(el) { el.style.fill = nextFill; el.classList.add('filled'); }
+    });
+  } else {
+    const el = document.getElementById(entry.id);
+    if(el) { el.style.fill = entry.nextFill; el.classList.add('filled'); }
+  }
+  updateUndoRedo();
+  statusEl.textContent = 'Redone';
+}
+function updateUndoRedo() {
+  document.getElementById('btn-undo').disabled = !history.length;
+  document.getElementById('btn-redo').disabled = !future.length;
+}
+updateUndoRedo();
+
+// ── RESET ────────────────────────────────────────────────
+document.getElementById('btn-reset').addEventListener('click', () => {
+  const regions = svg.querySelectorAll('.region');
+  const batch = [];
+  regions.forEach(r => {
+    batch.push({id: r.id, prevFill: getRegionFill(r), nextFill: 'none'});
+    r.style.fill = 'none';
+    r.classList.remove('filled');
+  });
+  history.push({batch});
+  future = [];
+  updateUndoRedo();
+  statusEl.textContent = 'Reset to line art';
+});
+
+// ── DOWNLOAD PNG ─────────────────────────────────────────
+document.getElementById('btn-download').addEventListener('click', () => {
+  statusEl.textContent = 'Preparing download…';
+  const svgEl = document.getElementById('scene-svg');
+  const serializer = new XMLSerializer();
+  let svgStr = serializer.serializeToString(svgEl);
+  // ensure background paper color is baked in
+  svgStr = svgStr.replace(/var\(--paper\)/g, currentPaper);
+  svgStr = svgStr.replace(/var\(--ink\)/g, '#1c1a17');
+  const blob = new Blob([svgStr], {type: 'image/svg+xml'});
+  const url = URL.createObjectURL(blob);
+  // render to canvas
+  const img = new Image();
+  img.onload = function() {
+    const canvas = document.createElement('canvas');
+    canvas.width = 700; canvas.height = 560;
+    const ctx = canvas.getContext('2d');
+    ctx.drawImage(img, 0, 0, 700, 560);
+    URL.revokeObjectURL(url);
+    const dataURL = canvas.toDataURL('image/png');
+    const a = document.createElement('a');
+    a.href = dataURL;
+    a.download = 'dw-toile-coloring.png';
+    a.click();
+    statusEl.textContent = 'Downloaded';
+    toast('Toile saved as PNG');
+  };
+  img.onerror = function() {
+    // Fallback: SVG data URI download
+    URL.revokeObjectURL(url);
+    const dataUri = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgStr);
+    const a = document.createElement('a');
+    a.href = dataUri;
+    a.download = 'dw-toile-coloring.svg';
+    a.click();
+    statusEl.textContent = 'Downloaded as SVG';
+    toast('Saved as SVG (PNG not available in this browser)');
+  };
+  img.src = url;
+});
+
+// ── UTILS ─────────────────────────────────────────────────
+function getColorName(hex) {
+  const c = COLORS.find(c => c.hex === hex);
+  return c ? c.name : hex;
+}
+let toastTimer;
+function toast(msg) {
+  toastEl.textContent = msg;
+  toastEl.classList.add('show');
+  clearTimeout(toastTimer);
+  toastTimer = setTimeout(() => toastEl.classList.remove('show'), 2800);
+}
+
+// Keyboard shortcuts
+document.addEventListener('keydown', e => {
+  if((e.metaKey || e.ctrlKey) && e.key === 'z') { e.preventDefault(); if(e.shiftKey) redo(); else undo(); }
+  if((e.metaKey || e.ctrlKey) && e.key === 'y') { e.preventDefault(); redo(); }
+});
+
+})();
+</script>
+</body>
+</html>

← a0778d8 Agent-driven art toy: DW Motif Spinner (frontend-developer)  ·  back to Model Arena  ·  Agent-driven toy: DW Toile Coloring (frontend-developer) — f 410f295 →