[object Object]

← back to Model Arena

Agent-driven tool: DW Sample Box (frontend-developer) — sample-box builder, fly-to-slot, curate, request flow. Registered.

105d0ec2715028eab7c454b7e8b1e5551300cbb6 · 2026-07-25 13:16:35 -0700 · Steve Abrams

Files touched

Diff

commit 105d0ec2715028eab7c454b7e8b1e5551300cbb6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Jul 25 13:16:35 2026 -0700

    Agent-driven tool: DW Sample Box (frontend-developer) — sample-box builder, fly-to-slot, curate, request flow. Registered.
---
 data/arcade-games.json              |   7 +
 public/games/dw-sample-box.html     | 728 ++++++++++++++++++++++++++++
 public/games/dw-the-big-reveal.html | 928 ++++++++++++++++++++++++++++++++++++
 3 files changed, 1663 insertions(+)

diff --git a/data/arcade-games.json b/data/arcade-games.json
index 63cc757..dd8a6dd 100644
--- a/data/arcade-games.json
+++ b/data/arcade-games.json
@@ -195,6 +195,13 @@
       "credit": "frontend-developer",
       "category": "Art Toys",
       "note": "Agent-built toy \u2014 color a hand-drawn toile pastoral scene (50+ regions), designer palette, toile-classic preset, PNG"
+    },
+    {
+      "slug": "dw-sample-box",
+      "title": "DW Sample Box",
+      "credit": "frontend-developer",
+      "category": "Tools",
+      "note": "Agent-built commerce flow \u2014 build a sample box from 12 swatches, fly-to-slot, curate, request-your-box"
     }
   ]
 }
\ No newline at end of file
diff --git a/public/games/dw-sample-box.html b/public/games/dw-sample-box.html
new file mode 100644
index 0000000..0eaf579
--- /dev/null
+++ b/public/games/dw-sample-box.html
@@ -0,0 +1,728 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Sample Box — Designer Wallcoverings</title>
+<style>
+:root{
+  --bg:#f4f1ea;--bg2:#edeade;--ink:#1c1a17;--ink2:#4a4540;--ink3:#7a736b;
+  --accent:#c9a961;--accent2:#a8853a;--white:#fffdf8;--red:#c0392b;
+  --serif:'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif;
+  --sans:'Inter','Helvetica Neue',Arial,sans-serif;
+  --ease:cubic-bezier(.4,0,.2,1);
+  --r:6px;--shadow:0 4px 24px rgba(28,26,23,.13);
+}
+*{box-sizing:border-box;margin:0;padding:0}
+body{background:var(--bg);color:var(--ink);font-family:var(--sans);min-height:100vh;overflow-x:hidden}
+
+/* HEADER */
+header{
+  background:var(--white);border-bottom:1px solid rgba(201,169,97,.3);
+  padding:18px 32px 14px;display:flex;align-items:center;justify-content:space-between;
+  position:sticky;top:0;z-index:200;
+}
+.wordmark{
+  font-family:var(--serif);font-size:clamp(22px,3vw,40px);font-weight:400;
+  text-transform:uppercase;letter-spacing:.18em;color:var(--ink);line-height:1;
+}
+.wordmark span{display:block;font-size:.38em;letter-spacing:.32em;color:var(--accent);margin-top:2px;font-weight:400}
+.box-counter-pill{
+  display:flex;align-items:center;gap:8px;background:var(--ink);color:var(--white);
+  border-radius:24px;padding:7px 18px;font-size:.82rem;letter-spacing:.06em;cursor:pointer;
+  transition:background .2s var(--ease);font-family:var(--sans);
+}
+.box-counter-pill:hover{background:var(--accent2)}
+.pill-icon{font-size:1rem}
+
+/* LAYOUT */
+main{display:grid;grid-template-columns:1fr 340px;gap:0;min-height:calc(100vh - 73px)}
+@media(max-width:860px){main{grid-template-columns:1fr;}}
+
+/* CATALOG SIDE */
+.catalog-side{padding:28px 28px 40px;border-right:1px solid rgba(28,26,23,.09)}
+.catalog-header{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:20px}
+.catalog-title{font-family:var(--serif);font-size:2rem;font-weight:400;letter-spacing:.04em}
+.filters{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:20px}
+.filter-btn{
+  padding:5px 14px;border-radius:20px;border:1px solid rgba(28,26,23,.25);
+  background:transparent;color:var(--ink2);font-size:.78rem;letter-spacing:.05em;
+  cursor:pointer;transition:all .18s var(--ease);font-family:var(--sans);
+}
+.filter-btn.active,.filter-btn:hover{background:var(--ink);color:var(--white);border-color:var(--ink)}
+.filter-btn.color-filter.active{background:var(--accent);border-color:var(--accent);color:var(--ink)}
+
+/* SWATCH GRID */
+.swatch-grid{
+  display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:16px;
+  transition:opacity .2s var(--ease);
+}
+.swatch-card{
+  background:var(--white);border-radius:var(--r);box-shadow:0 1px 6px rgba(28,26,23,.08);
+  overflow:hidden;cursor:pointer;transition:transform .22s var(--ease),box-shadow .22s var(--ease);
+  position:relative;
+}
+.swatch-card:hover{transform:translateY(-4px) scale(1.015);box-shadow:var(--shadow)}
+.swatch-card.in-box .swatch-img::after{
+  content:'✓';position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
+  font-size:2rem;color:var(--white);background:rgba(201,169,97,.55);
+}
+.swatch-img{
+  width:100%;height:130px;position:relative;overflow:hidden;
+}
+.swatch-img svg{width:100%;height:100%;display:block}
+.swatch-info{padding:10px 11px 12px}
+.swatch-name{font-family:var(--serif);font-size:1.05rem;font-weight:400;letter-spacing:.02em;color:var(--ink);margin-bottom:2px}
+.swatch-colorway{font-size:.72rem;color:var(--ink3);letter-spacing:.06em;margin-bottom:6px;text-transform:uppercase}
+.swatch-tags{display:flex;gap:4px;flex-wrap:wrap}
+.tag{
+  font-size:.65rem;letter-spacing:.07em;text-transform:uppercase;
+  padding:2px 7px;border-radius:10px;border:1px solid rgba(28,26,23,.18);color:var(--ink2);
+}
+.tag.material{background:rgba(201,169,97,.12);border-color:rgba(201,169,97,.4);color:var(--accent2)}
+.add-btn{
+  position:absolute;bottom:42px;right:8px;
+  width:28px;height:28px;border-radius:50%;background:var(--accent);border:none;
+  color:var(--ink);font-size:1.1rem;display:flex;align-items:center;justify-content:center;
+  cursor:pointer;opacity:0;transition:opacity .18s;box-shadow:0 2px 8px rgba(0,0,0,.18);
+  line-height:1;
+}
+.swatch-card:hover .add-btn{opacity:1}
+.swatch-card.in-box .add-btn{opacity:1;background:var(--ink3);color:var(--white);font-size:.8rem}
+
+/* BOX SIDE */
+.box-side{
+  background:var(--bg2);padding:24px 20px 32px;position:sticky;top:73px;
+  height:calc(100vh - 73px);overflow-y:auto;display:flex;flex-direction:column;gap:18px;
+}
+@media(max-width:860px){.box-side{position:static;height:auto;border-top:2px solid rgba(201,169,97,.3)}}
+.box-side-title{font-family:var(--serif);font-size:1.5rem;font-weight:400;letter-spacing:.05em}
+.box-subtitle{font-size:.77rem;color:var(--ink3);letter-spacing:.05em}
+
+/* SVG BOX ILLUSTRATION */
+.box-illustration{width:100%;max-width:300px;margin:0 auto;display:block}
+
+/* SLOTS GRID */
+.slots-grid{
+  display:grid;grid-template-columns:repeat(4,1fr);gap:8px;
+}
+.slot{
+  aspect-ratio:1;border-radius:5px;border:1.5px dashed rgba(28,26,23,.25);
+  background:rgba(255,253,248,.6);overflow:hidden;position:relative;
+  transition:border-color .18s,transform .18s var(--ease);cursor:default;
+}
+.slot.filled{border-style:solid;border-color:rgba(201,169,97,.5);cursor:pointer}
+.slot.filled:hover{transform:scale(1.06);border-color:var(--accent)}
+.slot.filled::after{
+  content:'✕';position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
+  font-size:1rem;color:var(--white);background:rgba(28,26,23,.45);
+  opacity:0;transition:opacity .15s;
+}
+.slot.filled:hover::after{opacity:1}
+.slot svg{width:100%;height:100%;display:block}
+.slot-pop{animation:slotpop .35s var(--ease)}
+@keyframes slotpop{0%{transform:scale(0.5)}60%{transform:scale(1.12)}100%{transform:scale(1)}}
+
+/* BOX SUMMARY */
+.box-summary{
+  background:var(--white);border-radius:var(--r);padding:14px 16px;
+  box-shadow:0 1px 4px rgba(28,26,23,.06);
+}
+.summary-row{display:flex;justify-content:space-between;align-items:center;font-size:.82rem;margin-bottom:4px}
+.summary-row.total{font-family:var(--serif);font-size:1.1rem;border-top:1px solid rgba(28,26,23,.1);padding-top:8px;margin-top:4px}
+.summary-count{color:var(--ink3)}
+
+.box-actions{display:flex;flex-direction:column;gap:8px}
+.btn-primary{
+  width:100%;padding:13px;border-radius:var(--r);border:none;
+  background:var(--ink);color:var(--white);font-family:var(--serif);
+  font-size:1.05rem;letter-spacing:.08em;cursor:pointer;
+  transition:background .2s var(--ease);
+}
+.btn-primary:hover:not(:disabled){background:var(--accent2);color:var(--ink)}
+.btn-primary:disabled{opacity:.4;cursor:not-allowed}
+.btn-secondary{
+  width:100%;padding:10px;border-radius:var(--r);border:1.5px solid rgba(28,26,23,.25);
+  background:transparent;color:var(--ink2);font-size:.82rem;letter-spacing:.06em;
+  cursor:pointer;transition:all .18s var(--ease);font-family:var(--sans);
+}
+.btn-secondary:hover{border-color:var(--accent);color:var(--accent2)}
+.btn-clear{
+  width:100%;padding:8px;border:none;background:transparent;
+  color:var(--ink3);font-size:.75rem;cursor:pointer;letter-spacing:.04em;
+  text-decoration:underline;text-underline-offset:3px;
+}
+.btn-clear:hover{color:var(--red)}
+
+/* FLY ANIMATION */
+.fly-swatch{
+  position:fixed;border-radius:4px;overflow:hidden;z-index:9999;
+  pointer-events:none;box-shadow:0 4px 16px rgba(0,0,0,.3);
+  transition:all .55s cubic-bezier(.4,.1,.2,1);
+}
+
+/* CONFIRM OVERLAY */
+.overlay{
+  position:fixed;inset:0;background:rgba(28,26,23,.55);z-index:1000;
+  display:flex;align-items:center;justify-content:center;padding:20px;
+  visibility:hidden;opacity:0;transition:opacity .25s var(--ease),visibility .25s;
+}
+.overlay.open{visibility:visible;opacity:1}
+.overlay-card{
+  background:var(--white);border-radius:10px;max-width:520px;width:100%;
+  padding:36px 32px;box-shadow:0 20px 60px rgba(0,0,0,.22);
+  transform:translateY(20px);transition:transform .3s var(--ease);
+  max-height:90vh;overflow-y:auto;
+}
+.overlay.open .overlay-card{transform:translateY(0)}
+.overlay-title{font-family:var(--serif);font-size:2rem;font-weight:400;letter-spacing:.05em;margin-bottom:4px}
+.overlay-sub{color:var(--ink3);font-size:.82rem;margin-bottom:24px;letter-spacing:.03em}
+.overlay-samples{display:grid;grid-template-columns:repeat(auto-fill,minmax(70px,1fr));gap:10px;margin-bottom:24px}
+.overlay-sample{text-align:center}
+.overlay-swatch{width:100%;aspect-ratio:1;border-radius:4px;overflow:hidden;margin-bottom:4px;box-shadow:0 1px 4px rgba(0,0,0,.12)}
+.overlay-swatch svg{width:100%;height:100%;display:block}
+.overlay-sample-name{font-size:.65rem;color:var(--ink2);letter-spacing:.04em;line-height:1.3}
+.overlay-total{font-family:var(--serif);font-size:1.15rem;border-top:1px solid rgba(28,26,23,.12);padding-top:14px;margin-bottom:20px;display:flex;justify-content:space-between}
+.thankyou{text-align:center;padding:16px 0 8px;display:none}
+.thankyou-title{font-family:var(--serif);font-size:2.2rem;font-weight:400;letter-spacing:.06em;margin-bottom:8px;color:var(--accent2)}
+.thankyou-msg{color:var(--ink2);font-size:.88rem;line-height:1.6;letter-spacing:.02em}
+.overlay-close{position:absolute;top:14px;right:18px;background:none;border:none;font-size:1.4rem;cursor:pointer;color:var(--ink3)}
+.overlay-card{position:relative}
+
+/* HIDDEN UTILITY */
+.hidden{display:none!important}
+
+/* FILTER TRANSITION */
+.swatch-card{transition:transform .22s var(--ease),box-shadow .22s var(--ease),opacity .25s,max-height .25s}
+.swatch-card.filtered-out{opacity:0;pointer-events:none;transform:scale(.96)}
+
+/* EMPTY STATE */
+.box-empty{text-align:center;padding:16px 8px;color:var(--ink3);font-size:.82rem;line-height:1.6;letter-spacing:.03em}
+</style>
+</head>
+<body>
+
+<header>
+  <div class="wordmark">
+    Designer Wallcoverings
+    <span>Sample Box</span>
+  </div>
+  <button class="box-counter-pill" onclick="scrollToBox()" aria-label="View sample box">
+    <span class="pill-icon">&#9632;</span>
+    <span id="pill-count">0 of 8 samples</span>
+  </button>
+</header>
+
+<main>
+  <!-- CATALOG -->
+  <section class="catalog-side">
+    <div class="catalog-header">
+      <h1 class="catalog-title">Our Swatches</h1>
+    </div>
+    <div class="filters" role="group" aria-label="Filter swatches">
+      <button class="filter-btn active" data-filter="all">All</button>
+      <button class="filter-btn" data-filter="mat-woven">Woven</button>
+      <button class="filter-btn" data-filter="mat-paper">Paper</button>
+      <button class="filter-btn" data-filter="mat-grass">Grasscloth</button>
+      <button class="filter-btn" data-filter="mat-silk">Silk</button>
+      <span style="width:1px;background:rgba(28,26,23,.15);margin:0 4px"></span>
+      <button class="filter-btn color-filter" data-filter="col-neutral">Neutral</button>
+      <button class="filter-btn color-filter" data-filter="col-warm">Warm</button>
+      <button class="filter-btn color-filter" data-filter="col-cool">Cool</button>
+      <button class="filter-btn color-filter" data-filter="col-dark">Dark</button>
+    </div>
+    <div class="swatch-grid" id="swatch-grid"></div>
+  </section>
+
+  <!-- SAMPLE BOX -->
+  <aside class="box-side" id="box-side">
+    <div>
+      <h2 class="box-side-title">Your Sample Box</h2>
+      <p class="box-subtitle">Up to 8 samples · $4.25 each</p>
+    </div>
+
+    <!-- Inline SVG memo box -->
+    <svg class="box-illustration" viewBox="0 0 280 120" aria-hidden="true">
+      <!-- box body -->
+      <rect x="20" y="40" width="240" height="70" rx="6" fill="#d4c9b0" stroke="#b8a87a" stroke-width="1.5"/>
+      <!-- box lid (open, lifted back) -->
+      <path d="M20 44 Q20 10 140 10 Q260 10 260 44" fill="#e8dfc8" stroke="#b8a87a" stroke-width="1.5"/>
+      <path d="M20 44 Q20 18 140 18 Q260 18 260 44" fill="none" stroke="#c9b88a" stroke-width=".8" opacity=".6"/>
+      <!-- interior -->
+      <rect x="28" y="48" width="224" height="54" rx="3" fill="#ede3cc"/>
+      <!-- ribbon accent -->
+      <rect x="128" y="48" width="24" height="54" fill="rgba(201,169,97,.25)"/>
+      <line x1="140" y1="48" x2="140" y2="102" stroke="#c9a961" stroke-width="1" opacity=".5"/>
+      <!-- brand text on lid -->
+      <text x="140" y="30" text-anchor="middle" font-family="Georgia,serif" font-size="7" fill="#8b6f2e" letter-spacing="2.5">DESIGNER WALLCOVERINGS</text>
+    </svg>
+
+    <div class="slots-grid" id="slots-grid"></div>
+
+    <div class="box-summary">
+      <div class="summary-row">
+        <span>Samples selected</span>
+        <span class="summary-count" id="summary-count">0 of 8</span>
+      </div>
+      <div class="summary-row">
+        <span>Price per sample</span>
+        <span>$4.25</span>
+      </div>
+      <div class="summary-row total">
+        <span>Subtotal</span>
+        <span id="summary-total">$0.00</span>
+      </div>
+    </div>
+
+    <div class="box-actions">
+      <button class="btn-primary" id="request-btn" disabled onclick="openOverlay()">Request Your Box</button>
+      <button class="btn-secondary" onclick="surpriseMe()">Curate a Box for Me</button>
+      <button class="btn-clear" onclick="clearBox()">Clear box</button>
+    </div>
+
+    <div id="box-empty-msg" class="box-empty">
+      Browse the catalog and add up to 8 samples.<br>Each arrives hand-wrapped within 5–7 days.
+    </div>
+  </aside>
+</main>
+
+<!-- CONFIRM OVERLAY -->
+<div class="overlay" id="overlay" role="dialog" aria-modal="true" aria-labelledby="overlay-title">
+  <div class="overlay-card">
+    <button class="overlay-close" onclick="closeOverlay()" aria-label="Close">&#215;</button>
+    <div id="overlay-form">
+      <h2 class="overlay-title" id="overlay-title">Your Sample Box</h2>
+      <p class="overlay-sub">A curated selection from Designer Wallcoverings</p>
+      <div class="overlay-samples" id="overlay-samples"></div>
+      <div class="overlay-total"><span>Subtotal</span><span id="overlay-total-val"></span></div>
+      <button class="btn-primary" onclick="submitBox()">Confirm &amp; Request Samples</button>
+    </div>
+    <div class="thankyou" id="thankyou">
+      <div class="thankyou-title">Thank You</div>
+      <p class="thankyou-msg">
+        Your sample box has been received.<br>
+        Expect your hand-wrapped selection within 5–7 business days.<br><br>
+        <em style="font-family:var(--serif);font-size:1.1rem;color:var(--accent2)">— Designer Wallcoverings</em>
+      </p>
+    </div>
+  </div>
+</div>
+
+<script>
+// ── SWATCH DEFINITIONS ──────────────────────────────────────────────────────
+const SWATCHES = [
+  {
+    id:'s1',name:'Florentine Damask',colorway:'Ivory & Champagne',material:'woven',
+    matTag:'Woven Textile',color:'neutral',
+    tags:['Damask','Traditional'],
+    svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+      <rect width="100" height="100" fill="#f5eed8"/>
+      <g opacity=".22" fill="#8b6f2e">
+        ${Array.from({length:9},(_, i)=>{
+          const cx=17+((i%3)*33), cy=17+Math.floor(i/3)*33;
+          return `<ellipse cx="${cx}" cy="${cy}" rx="10" ry="14"/>
+          <ellipse cx="${cx}" cy="${cy}" rx="5" ry="8"/>
+          <circle cx="${cx}" cy="${cy}" r="3"/>
+          <line x1="${cx-10}" y1="${cy}" x2="${cx+10}" y2="${cy}" stroke="#8b6f2e" stroke-width="1"/>
+          <line x1="${cx}" y1="${cy-14}" x2="${cx}" y2="${cy+14}" stroke="#8b6f2e" stroke-width="1"/>`;
+        }).join('')}
+      </g>
+    </svg>`
+  },
+  {
+    id:'s2',name:'Pastoral Toile',colorway:'Wedgwood Blue',material:'paper',
+    matTag:'Paper',color:'cool',
+    tags:['Toile','Pastoral'],
+    svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+      <rect width="100" height="100" fill="#dce8f0"/>
+      <g stroke="#3a6080" fill="none" stroke-width=".8" opacity=".7">
+        <ellipse cx="30" cy="30" rx="12" ry="16"/><ellipse cx="30" cy="30" rx="6" ry="9"/>
+        <path d="M18 44 Q24 38 30 44 Q36 38 42 44"/>
+        <path d="M22 22 Q30 14 38 22"/>
+        <ellipse cx="70" cy="65" rx="14" ry="10"/><ellipse cx="70" cy="65" rx="7" ry="5"/>
+        <path d="M60 58 Q65 52 70 56"/>
+        <path d="M56 75 Q62 80 68 75 Q74 80 80 75"/>
+        <circle cx="50" cy="20" r="3"/><circle cx="15" cy="68" r="4"/>
+        <path d="M46 20 Q50 12 54 20" stroke-width=".6"/>
+      </g>
+    </svg>`
+  },
+  {
+    id:'s3',name:'Sea Island Grasscloth',colorway:'Natural Flax',material:'grass',
+    matTag:'Grasscloth',color:'neutral',
+    tags:['Texture','Natural'],
+    svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+      <rect width="100" height="100" fill="#c8b48a"/>
+      ${Array.from({length:28},(_,i)=>`<line x1="0" y1="${i*3.6+1}" x2="100" y2="${i*3.6+1}" stroke="#9a8260" stroke-width="${.4+Math.random()*.5}" opacity="${.3+Math.random()*.5}"/>`).join('')}
+      ${Array.from({length:12},(_,i)=>`<line x1="${i*8+1}" y1="0" x2="${i*8+1}" y2="100" stroke="#b0926a" stroke-width=".6" opacity=".2"/>`).join('')}
+    </svg>`
+  },
+  {
+    id:'s4',name:'Ming Dynasty Chinoiserie',colorway:'Rouge & Gold',material:'paper',
+    matTag:'Paper',color:'warm',
+    tags:['Chinoiserie','Scenic'],
+    svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+      <rect width="100" height="100" fill="#f7e8d2"/>
+      <g fill="none">
+        <path d="M20 70 Q25 55 30 60 Q35 50 40 55 Q45 45 50 50" stroke="#c0392b" stroke-width="1.2"/>
+        <circle cx="22" cy="72" r="4" fill="#c0392b" opacity=".7"/>
+        <circle cx="48" cy="52" r="3" fill="#c9a961" opacity=".8"/>
+        <path d="M55 30 Q60 20 65 25 Q70 15 75 20 Q80 10 85 15" stroke="#2e7d32" stroke-width="1"/>
+        <ellipse cx="60" cy="32" rx="4" ry="6" fill="#388e3c" opacity=".6"/>
+        <ellipse cx="78" cy="17" rx="3" ry="5" fill="#2e7d32" opacity=".6"/>
+        <path d="M30 10 L36 20 L30 18 L36 28" stroke="#c9a961" stroke-width="1.2" fill="none"/>
+        <rect x="27" y="22" width="9" height="7" rx="1" stroke="#c9a961" stroke-width=".8"/>
+        <path d="M10 50 Q15 45 20 48 Q25 43 30 46" stroke="#8b4513" stroke-width=".8" opacity=".5"/>
+      </g>
+    </svg>`
+  },
+  {
+    id:'s5',name:'Marrakech Trellis',colorway:'Ebony & Cream',material:'woven',
+    matTag:'Woven Textile',color:'dark',
+    tags:['Geometric','Trellis'],
+    svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+      <rect width="100" height="100" fill="#1c1a17"/>
+      ${Array.from({length:6},(_,row)=>Array.from({length:6},(_,col)=>{
+        const cx=8+col*16, cy=8+row*16;
+        return `<path d="M${cx} ${cy-7} L${cx+7} ${cy} L${cx} ${cy+7} L${cx-7} ${cy} Z" fill="none" stroke="#c9a961" stroke-width=".9"/>
+        <circle cx="${cx}" cy="${cy}" r="1.5" fill="#c9a961" opacity=".5"/>`;
+      }).join('')).join('')}
+    </svg>`
+  },
+  {
+    id:'s6',name:'Riviera Awning Stripe',colorway:'Navy & Ecru',material:'woven',
+    matTag:'Woven Textile',color:'cool',
+    tags:['Stripe','Coastal'],
+    svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+      <rect width="100" height="100" fill="#f0ece0"/>
+      ${[0,14,28,42,56,70,84].map(x=>`<rect x="${x}" y="0" width="7" height="100" fill="#1a2e4a" opacity=".85"/>`).join('')}
+      ${[7,21,35,49,63,77,91].map(x=>`<rect x="${x}" y="0" width="2" height="100" fill="#c9a961" opacity=".4"/>`).join('')}
+    </svg>`
+  },
+  {
+    id:'s7',name:'Silk Moiré',colorway:'Champagne',material:'silk',
+    matTag:'Silk',color:'neutral',
+    tags:['Moiré','Luxe'],
+    svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+      <defs>
+        <linearGradient id="moire-g" x1="0%" y1="0%" x2="100%" y2="100%">
+          <stop offset="0%" stop-color="#f0e6c8"/>
+          <stop offset="50%" stop-color="#e8d5a0"/>
+          <stop offset="100%" stop-color="#f5edd0"/>
+        </linearGradient>
+      </defs>
+      <rect width="100" height="100" fill="url(#moire-g)"/>
+      ${Array.from({length:20},(_,i)=>`<path d="M0 ${i*5} Q50 ${i*5+(i%2?3:-3)} 100 ${i*5}" fill="none" stroke="#c9a961" stroke-width=".4" opacity="${.15+.1*(i%3)}"/>`).join('')}
+      ${Array.from({length:20},(_,i)=>`<path d="M${i*5} 0 Q${i*5+(i%2?3:-3)} 50 ${i*5} 100" fill="none" stroke="#b8953a" stroke-width=".3" opacity=".12"/>`).join('')}
+    </svg>`
+  },
+  {
+    id:'s8',name:'Belgian Linen',colorway:'Warm Greige',material:'woven',
+    matTag:'Woven Textile',color:'neutral',
+    tags:['Linen','Texture'],
+    svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+      <rect width="100" height="100" fill="#d8cdb8"/>
+      ${Array.from({length:34},(_,i)=>`<line x1="0" y1="${i*3}" x2="100" y2="${i*3}" stroke="#b8a880" stroke-width="${.6+.4*(i%3==0?1:0)}" opacity="${.4+.3*(i%5==0?1:0)}"/>`).join('')}
+      ${Array.from({length:34},(_,i)=>`<line x1="${i*3}" y1="0" x2="${i*3}" y2="100" stroke="#c0b090" stroke-width="${.5+.3*(i%3==0?1:0)}" opacity="${.35+.25*(i%5==0?1:0)}"/>`).join('')}
+    </svg>`
+  },
+  {
+    id:'s9',name:'Jardin Botanique',colorway:'Forest & Ivory',material:'paper',
+    matTag:'Paper',color:'cool',
+    tags:['Botanical','Scenic'],
+    svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+      <rect width="100" height="100" fill="#e8edd8"/>
+      <g fill="none">
+        <path d="M50 90 Q50 60 50 40" stroke="#3a5a30" stroke-width="1.5"/>
+        <path d="M50 70 Q35 58 28 48" stroke="#3a5a30" stroke-width="1"/>
+        <path d="M50 60 Q65 48 72 38" stroke="#3a5a30" stroke-width="1"/>
+        <ellipse cx="28" cy="45" rx="12" ry="8" fill="#5a7a40" opacity=".7" transform="rotate(-30,28,45)"/>
+        <ellipse cx="72" cy="35" rx="12" ry="8" fill="#4a6a35" opacity=".7" transform="rotate(20,72,35)"/>
+        <ellipse cx="50" cy="35" rx="9" ry="6" fill="#6a8a50" opacity=".6"/>
+        <circle cx="20" cy="25" r="6" fill="#8aaa60" opacity=".5"/>
+        <circle cx="80" cy="20" r="5" fill="#7a9a55" opacity=".5"/>
+        <path d="M15 55 Q22 45 30 52" stroke="#5a7a40" stroke-width=".8" opacity=".6"/>
+        <circle cx="55" cy="18" r="3" fill="#c9a961" opacity=".6"/>
+        <circle cx="35" cy="15" r="2.5" fill="#d4b870" opacity=".5"/>
+      </g>
+    </svg>`
+  },
+  {
+    id:'s10',name:'Bauhaus Geometric',colorway:'Terracotta & Slate',material:'paper',
+    matTag:'Paper',color:'warm',
+    tags:['Geometric','Modern'],
+    svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+      <rect width="100" height="100" fill="#e8e0d8"/>
+      ${Array.from({length:5},(_,row)=>Array.from({length:5},(_,col)=>{
+        const shapes=[
+          `<rect x="${col*20+2}" y="${row*20+2}" width="16" height="16" fill="#c45a30" opacity=".7"/>`,
+          `<circle cx="${col*20+10}" cy="${row*20+10}" r="8" fill="#556b7a" opacity=".6"/>`,
+          `<polygon points="${col*20+10},${row*20+2} ${col*20+18},${row*20+18} ${col*20+2},${row*20+18}" fill="#8b6f2e" opacity=".5"/>`,
+          `<rect x="${col*20+4}" y="${row*20+4}" width="12" height="12" fill="none" stroke="#c45a30" stroke-width="1.5" opacity=".7"/>`,
+          `<line x1="${col*20+2}" y1="${row*20+2}" x2="${col*20+18}" y2="${row*20+18}" stroke="#556b7a" stroke-width="2" opacity=".4"/>
+          <line x1="${col*20+18}" y1="${row*20+2}" x2="${col*20+2}" y2="${row*20+18}" stroke="#556b7a" stroke-width="2" opacity=".4"/>`,
+        ];
+        return shapes[(row*5+col)%5];
+      }).join('')).join('')}
+    </svg>`
+  },
+  {
+    id:'s11',name:'Dupioni Silk',colorway:'Midnight Indigo',material:'silk',
+    matTag:'Silk',color:'dark',
+    tags:['Silk','Luxe'],
+    svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+      <defs>
+        <linearGradient id="silk-g" x1="0%" y1="0%" x2="100%" y2="0%">
+          <stop offset="0%" stop-color="#1a1a3e"/>
+          <stop offset="45%" stop-color="#2a2a5e"/>
+          <stop offset="100%" stop-color="#1a1a3e"/>
+        </linearGradient>
+      </defs>
+      <rect width="100" height="100" fill="url(#silk-g)"/>
+      ${Array.from({length:25},(_,i)=>`<line x1="0" y1="${i*4}" x2="100" y2="${i*4+2}" stroke="#6060a0" stroke-width="${.5+(i%4==0?.5:0)}" opacity="${.25+.15*(i%3==0?1:0)}"/>`).join('')}
+      ${Array.from({length:12},(_,i)=>`<line x1="${i*9}" y1="0" x2="${i*9+3}" y2="100" stroke="#4040a0" stroke-width=".4" opacity=".15"/>`).join('')}
+      <rect width="100" height="100" fill="transparent" opacity=".1"/>
+    </svg>`
+  },
+  {
+    id:'s12',name:'Suzani Ikat',colorway:'Saffron & Crimson',material:'woven',
+    matTag:'Woven Textile',color:'warm',
+    tags:['Ikat','Global'],
+    svg:`<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+      <rect width="100" height="100" fill="#f0d090"/>
+      ${Array.from({length:4},(_,row)=>Array.from({length:4},(_,col)=>{
+        const cx=12+col*25, cy=12+row*25;
+        const r=['#c0392b','#8b0000','#c9a961','#8b4513'][(row+col)%4];
+        return `<ellipse cx="${cx}" cy="${cy}" rx="9" ry="11" fill="${r}" opacity=".7"/>
+        <ellipse cx="${cx}" cy="${cy}" rx="5" ry="7" fill="rgba(255,255,255,.25)"/>
+        <circle cx="${cx}" cy="${cy}" r="2" fill="rgba(255,255,255,.5)"/>`;
+      }).join('')).join('')}
+    </svg>`
+  }
+];
+
+const MAX=8;
+const PRICE=4.25;
+let box=[];
+let activeFilter='all';
+
+// ── STORAGE ──────────────────────────────────────────────────────────────────
+function saveBox(){try{localStorage.setItem('dw-sample-box',JSON.stringify(box))}catch(e){}}
+function loadBox(){
+  try{
+    const s=localStorage.getItem('dw-sample-box');
+    if(s){
+      const ids=JSON.parse(s);
+      box=ids.filter(id=>SWATCHES.find(sw=>sw.id===id));
+    }
+  }catch(e){}
+}
+
+// ── RENDER CATALOG ────────────────────────────────────────────────────────────
+function renderCatalog(){
+  const grid=document.getElementById('swatch-grid');
+  grid.innerHTML='';
+  SWATCHES.forEach(sw=>{
+    const inBox=box.includes(sw.id);
+    const card=document.createElement('div');
+    card.className='swatch-card'+(inBox?' in-box':'');
+    card.id='card-'+sw.id;
+    card.setAttribute('tabindex','0');
+    card.setAttribute('role','button');
+    card.setAttribute('aria-label',(inBox?'Remove ':'Add ')+sw.name+' to box');
+    card.setAttribute('aria-pressed',inBox?'true':'false');
+    card.innerHTML=`
+      <div class="swatch-img" id="simg-${sw.id}">${sw.svg}</div>
+      <div class="swatch-info">
+        <div class="swatch-name">${sw.name}</div>
+        <div class="swatch-colorway">${sw.colorway}</div>
+        <div class="swatch-tags">
+          <span class="tag material">${sw.matTag}</span>
+          ${sw.tags.map(t=>`<span class="tag">${t}</span>`).join('')}
+        </div>
+      </div>
+      <button class="add-btn" title="${inBox?'Remove from box':'Add to box'}" aria-hidden="true">
+        ${inBox?'✓':'+'}
+      </button>`;
+    card.addEventListener('click',()=>toggleSwatch(sw.id,card));
+    card.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();toggleSwatch(sw.id,card);}});
+    grid.appendChild(card);
+    applyFilter(card,sw);
+  });
+}
+
+// ── FILTER ────────────────────────────────────────────────────────────────────
+function applyFilter(card,sw){
+  const f=activeFilter;
+  if(f==='all'){card.classList.remove('filtered-out');return;}
+  const matMap={'mat-woven':'woven','mat-paper':'paper','mat-grass':'grass','mat-silk':'silk'};
+  const colMap={'col-neutral':'neutral','col-warm':'warm','col-cool':'cool','col-dark':'dark'};
+  let show=true;
+  if(matMap[f]) show=(sw.material===matMap[f]);
+  if(colMap[f]) show=(sw.color===colMap[f]);
+  card.classList.toggle('filtered-out',!show);
+}
+
+document.querySelectorAll('.filter-btn').forEach(btn=>{
+  btn.addEventListener('click',()=>{
+    document.querySelectorAll('.filter-btn').forEach(b=>b.classList.remove('active'));
+    btn.classList.add('active');
+    activeFilter=btn.dataset.filter;
+    SWATCHES.forEach(sw=>{
+      const card=document.getElementById('card-'+sw.id);
+      if(card) applyFilter(card,sw);
+    });
+  });
+});
+
+// ── SLOTS ─────────────────────────────────────────────────────────────────────
+function renderSlots(){
+  const grid=document.getElementById('slots-grid');
+  grid.innerHTML='';
+  for(let i=0;i<MAX;i++){
+    const slot=document.createElement('div');
+    slot.className='slot';
+    slot.id='slot-'+i;
+    const id=box[i];
+    if(id){
+      const sw=SWATCHES.find(s=>s.id===id);
+      slot.classList.add('filled');
+      slot.innerHTML=sw.svg;
+      slot.setAttribute('title','Click to remove '+sw.name);
+      slot.setAttribute('tabindex','0');
+      slot.setAttribute('role','button');
+      slot.setAttribute('aria-label','Remove '+sw.name+' from box');
+      slot.addEventListener('click',()=>removeFromBox(id));
+      slot.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();removeFromBox(id);}});
+    } else {
+      slot.innerHTML=`<svg viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg"><text x="20" y="25" text-anchor="middle" font-size="14" fill="rgba(28,26,23,.18)">${i+1}</text></svg>`;
+    }
+    grid.appendChild(slot);
+  }
+  updateUI();
+}
+
+function updateUI(){
+  const n=box.length;
+  document.getElementById('pill-count').textContent=n+' of '+MAX+' samples';
+  document.getElementById('summary-count').textContent=n+' of '+MAX;
+  document.getElementById('summary-total').textContent='$'+(n*PRICE).toFixed(2);
+  document.getElementById('request-btn').disabled=(n===0);
+  document.getElementById('box-empty-msg').style.display=(n===0?'block':'none');
+}
+
+// ── TOGGLE ────────────────────────────────────────────────────────────────────
+function toggleSwatch(id,card){
+  if(box.includes(id)){removeFromBox(id);}
+  else{addToBox(id,card);}
+}
+
+function addToBox(id,card){
+  if(box.includes(id)||box.length>=MAX) return;
+  const slot=document.getElementById('slot-'+box.length);
+  animateFly(card,slot,id);
+}
+
+function removeFromBox(id){
+  box=box.filter(b=>b!==id);
+  saveBox();
+  renderCatalog();
+  renderSlots();
+}
+
+function clearBox(){box=[];saveBox();renderCatalog();renderSlots();}
+
+// ── FLY ANIMATION ─────────────────────────────────────────────────────────────
+function animateFly(card,targetSlot,id){
+  const srcRect=card.querySelector('.swatch-img').getBoundingClientRect();
+  const sw=SWATCHES.find(s=>s.id===id);
+
+  const fly=document.createElement('div');
+  fly.className='fly-swatch';
+  fly.style.cssText=`left:${srcRect.left}px;top:${srcRect.top}px;width:${srcRect.width}px;height:${srcRect.height}px`;
+  fly.innerHTML=sw.svg;
+  document.body.appendChild(fly);
+
+  requestAnimationFrame(()=>{
+    requestAnimationFrame(()=>{
+      const tRect=targetSlot.getBoundingClientRect();
+      fly.style.left=tRect.left+'px';
+      fly.style.top=tRect.top+'px';
+      fly.style.width=tRect.width+'px';
+      fly.style.height=tRect.height+'px';
+      fly.style.opacity='.92';
+      fly.style.borderRadius='5px';
+    });
+  });
+
+  setTimeout(()=>{
+    document.body.removeChild(fly);
+    box.push(id);
+    saveBox();
+    // pop anim
+    targetSlot.classList.add('slot-pop');
+    setTimeout(()=>targetSlot.classList.remove('slot-pop'),400);
+    renderCatalog();
+    renderSlots();
+  },570);
+}
+
+// ── SURPRISE ME ───────────────────────────────────────────────────────────────
+function surpriseMe(){
+  box=[];
+  const shuffled=[...SWATCHES].sort(()=>Math.random()-.5);
+  box=shuffled.slice(0,MAX).map(s=>s.id);
+  saveBox();
+  renderCatalog();
+  renderSlots();
+}
+
+// ── OVERLAY ───────────────────────────────────────────────────────────────────
+function openOverlay(){
+  if(box.length===0) return;
+  document.getElementById('thankyou').style.display='none';
+  document.getElementById('overlay-form').style.display='block';
+  const samples=document.getElementById('overlay-samples');
+  samples.innerHTML=box.map(id=>{
+    const sw=SWATCHES.find(s=>s.id===id);
+    return `<div class="overlay-sample">
+      <div class="overlay-swatch">${sw.svg}</div>
+      <div class="overlay-sample-name">${sw.name}<br><em style="color:var(--ink3)">${sw.colorway}</em></div>
+    </div>`;
+  }).join('');
+  document.getElementById('overlay-total-val').textContent='$'+(box.length*PRICE).toFixed(2);
+  document.getElementById('overlay').classList.add('open');
+  document.getElementById('overlay').focus();
+}
+
+function closeOverlay(){
+  document.getElementById('overlay').classList.remove('open');
+}
+
+function submitBox(){
+  document.getElementById('overlay-form').style.display='none';
+  document.getElementById('thankyou').style.display='block';
+  setTimeout(()=>{
+    box=[];saveBox();renderCatalog();renderSlots();
+  },3200);
+  setTimeout(closeOverlay,4800);
+}
+
+document.getElementById('overlay').addEventListener('click',e=>{
+  if(e.target===document.getElementById('overlay')) closeOverlay();
+});
+document.addEventListener('keydown',e=>{
+  if(e.key==='Escape') closeOverlay();
+});
+
+function scrollToBox(){
+  document.getElementById('box-side').scrollIntoView({behavior:'smooth',block:'start'});
+}
+
+// ── INIT ──────────────────────────────────────────────────────────────────────
+loadBox();
+renderCatalog();
+renderSlots();
+</script>
+</body>
+</html>
diff --git a/public/games/dw-the-big-reveal.html b/public/games/dw-the-big-reveal.html
new file mode 100644
index 0000000..4c150ba
--- /dev/null
+++ b/public/games/dw-the-big-reveal.html
@@ -0,0 +1,928 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Designer Wallcoverings — The Big Reveal</title>
+<style>
+:root{
+  --bg:#0d0b09;
+  --surface:#1a1714;
+  --ink:#f4f1ea;
+  --ink-dim:#9a9080;
+  --accent:#c9a961;
+  --accent-dim:#8a6e38;
+  --serif:'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif;
+  --sans:system-ui,-apple-system,sans-serif;
+  --ease:cubic-bezier(.4,0,.2,1);
+  --radius:4px;
+}
+*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
+html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--ink);font-family:var(--sans)}
+body{display:flex;flex-direction:column}
+
+header{
+  flex:0 0 auto;
+  display:flex;align-items:center;justify-content:space-between;
+  padding:12px 20px 10px;
+  border-bottom:1px solid rgba(201,169,97,.18);
+  background:var(--bg);
+  z-index:10;
+}
+.wordmark{
+  font-family:var(--serif);
+  font-size:clamp(22px,3.5vw,42px);
+  font-weight:400;
+  letter-spacing:.18em;
+  text-transform:uppercase;
+  color:var(--accent);
+  line-height:1;
+  user-select:none;
+}
+.header-right{display:flex;align-items:center;gap:12px}
+
+button{
+  font-family:var(--sans);
+  font-size:12px;
+  letter-spacing:.08em;
+  text-transform:uppercase;
+  border:1px solid rgba(201,169,97,.4);
+  background:transparent;
+  color:var(--accent);
+  padding:6px 14px;
+  border-radius:var(--radius);
+  cursor:pointer;
+  transition:background .2s var(--ease),color .2s var(--ease);
+}
+button:hover{background:var(--accent);color:#0d0b09}
+button.muted{opacity:.45}
+
+#game-area{
+  flex:1 1 auto;
+  display:flex;
+  flex-direction:column;
+  align-items:center;
+  justify-content:center;
+  position:relative;
+  overflow:hidden;
+  padding:0 0 8px;
+}
+
+#wall-wrap{
+  position:relative;
+  width:min(680px,96vw);
+  height:min(460px,62vh);
+  border-radius:3px;
+  overflow:hidden;
+  box-shadow:0 8px 48px rgba(0,0,0,.7);
+  cursor:crosshair;
+  touch-action:none;
+}
+#canvas-reveal,#canvas-top{
+  position:absolute;
+  top:0;left:0;
+  width:100%;height:100%;
+}
+#canvas-top{pointer-events:none}
+
+/* HUD */
+#hud{
+  width:min(680px,96vw);
+  display:flex;align-items:center;gap:14px;
+  padding:10px 0 0;
+}
+#reveal-bar-wrap{
+  flex:1;
+  height:6px;
+  background:rgba(255,255,255,.08);
+  border-radius:3px;
+  overflow:hidden;
+}
+#reveal-bar{
+  height:100%;
+  width:0%;
+  background:linear-gradient(90deg,var(--accent-dim),var(--accent));
+  border-radius:3px;
+  transition:width .12s linear;
+}
+#pct-label{
+  font-family:var(--serif);
+  font-size:16px;
+  color:var(--accent);
+  min-width:42px;
+  text-align:right;
+}
+#score-label{
+  font-size:12px;
+  color:var(--ink-dim);
+  letter-spacing:.06em;
+  white-space:nowrap;
+}
+
+/* Timer */
+#timer-wrap{
+  position:absolute;
+  top:10px;right:12px;
+  font-family:var(--serif);
+  font-size:22px;
+  color:var(--accent);
+  pointer-events:none;
+  opacity:0;
+  transition:opacity .3s;
+}
+#timer-wrap.active{opacity:1}
+
+/* Completion overlay */
+#completion{
+  position:absolute;
+  inset:0;
+  background:rgba(13,11,9,.82);
+  display:flex;flex-direction:column;
+  align-items:center;justify-content:center;
+  gap:16px;
+  visibility:hidden;
+  opacity:0;
+  transition:opacity .4s var(--ease),visibility .4s var(--ease);
+  border-radius:3px;
+  backdrop-filter:blur(4px);
+}
+#completion.show{visibility:visible;opacity:1}
+#completion-title{
+  font-family:var(--serif);
+  font-size:clamp(24px,5vw,42px);
+  letter-spacing:.12em;
+  text-transform:uppercase;
+  color:var(--accent);
+  text-align:center;
+}
+#completion-sub{
+  font-family:var(--serif);
+  font-style:italic;
+  font-size:clamp(14px,2.5vw,20px);
+  color:var(--ink-dim);
+  text-align:center;
+}
+#completion-score{
+  font-size:13px;
+  letter-spacing:.1em;
+  color:var(--accent);
+}
+.flourish{font-size:28px;color:var(--accent);animation:spin 1.4s ease-out forwards}
+@keyframes spin{from{transform:rotate(-30deg) scale(.5);opacity:0}to{transform:rotate(0) scale(1);opacity:1}}
+
+/* Score mode toggle */
+#mode-btn{border-color:rgba(201,169,97,.3)}
+#mode-btn.active{background:var(--accent-dim);color:#f4f1ea;border-color:var(--accent-dim)}
+
+/* Dust canvas */
+#dust-canvas{
+  position:absolute;inset:0;
+  pointer-events:none;
+  border-radius:3px;
+}
+</style>
+</head>
+<body>
+
+<header>
+  <div class="wordmark">Designer Wallcoverings</div>
+  <div class="header-right">
+    <span id="best-label" style="font-size:12px;color:var(--ink-dim);letter-spacing:.06em"></span>
+    <button id="mode-btn" title="Toggle timed score mode">Score Mode</button>
+    <button id="new-btn">New Wall</button>
+    <button id="mute-btn">Mute</button>
+  </div>
+</header>
+
+<div id="game-area">
+  <div id="wall-wrap">
+    <canvas id="canvas-reveal"></canvas>
+    <canvas id="canvas-top"></canvas>
+    <canvas id="dust-canvas"></canvas>
+    <div id="timer-wrap"><span id="timer-val">30</span>s</div>
+    <div id="completion">
+      <div class="flourish">✦</div>
+      <div id="completion-title">Revealed</div>
+      <div id="completion-sub">italic pattern name here</div>
+      <div id="completion-score"></div>
+      <button id="next-btn" style="margin-top:8px;padding:10px 28px;font-size:13px">Next Wall</button>
+    </div>
+  </div>
+  <div id="hud">
+    <div id="reveal-bar-wrap"><div id="reveal-bar"></div></div>
+    <div id="pct-label">0%</div>
+    <div id="score-label"></div>
+  </div>
+</div>
+
+<script>
+/* ─────────────────────────────────────────────────────────
+   WALL PAIRINGS
+   Each pairing: old layer description + new DW pattern SVG
+───────────────────────────────────────────────────────── */
+const PAIRINGS = [
+  {
+    oldName:'Faded Beige Stripe',
+    newName:'Versailles Damask',
+    newColor:'#2a1f0e',
+    accentColor:'#c9a961',
+    drawOld(ctx,W,H){drawOldStripe(ctx,W,H,'#c8b89a','#bda882','#d4c4a8')},
+    drawNew(ctx,W,H){drawDamask(ctx,W,H,'#1e140a','#c9a961','#8a6e38')}
+  },
+  {
+    oldName:'Peeling Plaster',
+    newName:'Ming Chinoiserie',
+    newColor:'#0a1a2a',
+    accentColor:'#7eb8c9',
+    drawOld(ctx,W,H){drawOldPlaster(ctx,W,H,'#c4bfb0','#b5ae9e','#d6cfc0')},
+    drawNew(ctx,W,H){drawChinoiserie(ctx,W,H,'#0d2030','#7eb8c9','#c9d8e0')}
+  },
+  {
+    oldName:'70s Floral Print',
+    newName:'Highland Grasscloth',
+    newColor:'#1a1408',
+    accentColor:'#8a9e6a',
+    drawOld(ctx,W,H){drawOldFloral(ctx,W,H,'#c2b87e','#a89d62','#d4c98e')},
+    drawNew(ctx,W,H){drawGrasscloth(ctx,W,H,'#1a1408','#8a9e6a','#6b7a4e')}
+  },
+  {
+    oldName:'Stained Textured White',
+    newName:'Toile de Jouy',
+    newColor:'#f0e8d8',
+    accentColor:'#7a4a2e',
+    drawOld(ctx,W,H){drawOldTexture(ctx,W,H,'#dfd8c8','#cfc8b4','#e8e0d0')},
+    drawNew(ctx,W,H){drawToile(ctx,W,H,'#f4ede0','#7a4a2e')}
+  }
+];
+
+/* ─── OLD LAYER RENDERERS ─── */
+function drawOldStripe(ctx,W,H,c1,c2,c3){
+  ctx.fillStyle=c1; ctx.fillRect(0,0,W,H);
+  const sw=38,fade=0.55;
+  for(let x=0;x<W;x+=sw*2){
+    ctx.fillStyle=c2; ctx.globalAlpha=fade;
+    ctx.fillRect(x,0,sw,H);
+  }
+  ctx.globalAlpha=1;
+  // grime/stain patches
+  for(let i=0;i<18;i++){
+    const gx=Math.random()*W,gy=Math.random()*H;
+    const g=ctx.createRadialGradient(gx,gy,0,gx,gy,60+Math.random()*80);
+    g.addColorStop(0,'rgba(100,80,50,0.18)');
+    g.addColorStop(1,'rgba(100,80,50,0)');
+    ctx.fillStyle=g; ctx.beginPath(); ctx.ellipse(gx,gy,70,45,Math.random()*Math.PI,0,Math.PI*2);
+    ctx.fill();
+  }
+  // age cracks
+  ctx.strokeStyle='rgba(80,60,40,0.12)'; ctx.lineWidth=1;
+  for(let i=0;i<22;i++){
+    ctx.beginPath();
+    let cx=Math.random()*W,cy=Math.random()*H;
+    ctx.moveTo(cx,cy);
+    for(let j=0;j<6;j++){cx+=Math.random()*28-14;cy+=Math.random()*18-9;ctx.lineTo(cx,cy);}
+    ctx.stroke();
+  }
+}
+function drawOldPlaster(ctx,W,H,c1,c2,c3){
+  ctx.fillStyle=c1; ctx.fillRect(0,0,W,H);
+  const id=ctx.createImageData(W,H);
+  const d=id.data;
+  for(let i=0;i<d.length;i+=4){const n=(Math.random()-.5)*22;d[i]=180+n;d[i+1]=175+n;d[i+2]=162+n;d[i+3]=255;}
+  ctx.putImageData(id,0,0);
+  // peel/crack lines
+  ctx.strokeStyle='rgba(60,50,40,0.25)'; ctx.lineWidth=1.2;
+  for(let i=0;i<30;i++){
+    ctx.beginPath();let px=Math.random()*W,py=Math.random()*H;
+    ctx.moveTo(px,py);
+    for(let j=0;j<8;j++){px+=Math.random()*24-12;py+=Math.random()*14-7;ctx.lineTo(px,py);}
+    ctx.stroke();
+  }
+  // water stains
+  for(let i=0;i<8;i++){
+    const gx=Math.random()*W,gy=Math.random()*H*0.6;
+    const g=ctx.createRadialGradient(gx,gy,10,gx,gy+40,100);
+    g.addColorStop(0,'rgba(140,120,80,0.22)');
+    g.addColorStop(1,'rgba(140,120,80,0)');
+    ctx.fillStyle=g; ctx.beginPath(); ctx.ellipse(gx,gy+40,50,80,0,0,Math.PI*2); ctx.fill();
+  }
+}
+function drawOldFloral(ctx,W,H,c1,c2,c3){
+  ctx.fillStyle=c1; ctx.fillRect(0,0,W,H);
+  // tile a tiny faded flower
+  function flower(x,y,r,col){
+    ctx.fillStyle=col; ctx.globalAlpha=0.35;
+    for(let p=0;p<6;p++){
+      const a=p/6*Math.PI*2;
+      ctx.beginPath(); ctx.ellipse(x+Math.cos(a)*r*.7,y+Math.sin(a)*r*.7,r*.55,r*.3,a,0,Math.PI*2); ctx.fill();
+    }
+    ctx.fillStyle='rgba(180,160,80,0.4)'; ctx.globalAlpha=0.4;
+    ctx.beginPath(); ctx.arc(x,y,r*.35,0,Math.PI*2); ctx.fill();
+    ctx.globalAlpha=1;
+  }
+  const cols=['#8a7040','#6a5a30','#a08050'];
+  for(let gy=0;gy<H;gy+=72){
+    for(let gx=0;gx<W;gx+=72){
+      flower(gx+(gy%144?36:0),gy,18,cols[Math.floor(Math.random()*cols.length)]);
+    }
+  }
+  // grime overlay
+  for(let i=0;i<12;i++){
+    const gx=Math.random()*W,gy=Math.random()*H;
+    const g=ctx.createRadialGradient(gx,gy,0,gx,gy,90);
+    g.addColorStop(0,'rgba(80,60,20,0.16)'); g.addColorStop(1,'rgba(80,60,20,0)');
+    ctx.fillStyle=g; ctx.beginPath(); ctx.arc(gx,gy,90,0,Math.PI*2); ctx.fill();
+  }
+}
+function drawOldTexture(ctx,W,H,c1,c2,c3){
+  ctx.fillStyle=c1; ctx.fillRect(0,0,W,H);
+  const id=ctx.createImageData(W,H);
+  const d=id.data;
+  for(let i=0;i<d.length;i+=4){
+    const n=(Math.random()-.5)*18;
+    d[i]=210+n;d[i+1]=205+n;d[i+2]=195+n;d[i+3]=255;
+  }
+  ctx.putImageData(id,0,0);
+  // stain rings
+  for(let i=0;i<14;i++){
+    const gx=Math.random()*W,gy=Math.random()*H,r=20+Math.random()*60;
+    ctx.strokeStyle=`rgba(120,100,70,${0.08+Math.random()*0.12})`;
+    ctx.lineWidth=1+Math.random()*2;
+    ctx.beginPath(); ctx.arc(gx,gy,r,0,Math.PI*2); ctx.stroke();
+    ctx.beginPath(); ctx.arc(gx,gy,r*0.6,0,Math.PI*2); ctx.stroke();
+  }
+}
+
+/* ─── NEW LAYER RENDERERS ─── */
+function drawDamask(ctx,W,H,bg,c1,c2){
+  ctx.fillStyle=bg; ctx.fillRect(0,0,W,H);
+  // gold shimmer base
+  const grad=ctx.createLinearGradient(0,0,W,H);
+  grad.addColorStop(0,'rgba(201,169,97,0.04)'); grad.addColorStop(.5,'rgba(201,169,97,0.09)'); grad.addColorStop(1,'rgba(201,169,97,0.04)');
+  ctx.fillStyle=grad; ctx.fillRect(0,0,W,H);
+  // damask motif tiling
+  const TW=110,TH=140;
+  for(let ty=0;ty<H+TH;ty+=TH){
+    for(let tx=0;tx<W+TW;tx+=TW){
+      const ox=(ty/TH%2===0)?0:TW/2;
+      damaskMotif(ctx,tx+ox,ty,TW,TH,c1,c2);
+    }
+  }
+}
+function damaskMotif(ctx,cx,cy,TW,TH,c1,c2){
+  ctx.save(); ctx.translate(cx,cy);
+  // central urn/medallion
+  ctx.strokeStyle=c1; ctx.lineWidth=1.1; ctx.fillStyle='rgba(201,169,97,0.07)';
+  // oval center
+  ctx.beginPath(); ctx.ellipse(0,0,12,18,0,0,Math.PI*2); ctx.stroke(); ctx.fill();
+  // top crown flourish
+  function petal(rx,ry,count,dist,rot){
+    for(let i=0;i<count;i++){
+      const a=rot+i/count*Math.PI*2;
+      const bx=Math.cos(a)*dist, by=Math.sin(a)*dist;
+      ctx.beginPath();
+      ctx.moveTo(0,0);
+      ctx.quadraticCurveTo(bx*.4+Math.cos(a+Math.PI/2)*rx,by*.4+Math.sin(a+Math.PI/2)*ry,bx,by);
+      ctx.quadraticCurveTo(bx*.4+Math.cos(a-Math.PI/2)*rx,by*.4+Math.sin(a-Math.PI/2)*ry,0,0);
+      ctx.fillStyle=c2; ctx.globalAlpha=0.6; ctx.fill(); ctx.globalAlpha=1;
+    }
+  }
+  petal(5,2,8,30,-Math.PI/2);
+  // acanthus scrolls top/bottom
+  for(const dy of[-38,38]){
+    ctx.beginPath();
+    ctx.moveTo(0,dy<0?-20:20);
+    ctx.bezierCurveTo(-14,dy*.6,-18,dy*.85,0,dy);
+    ctx.bezierCurveTo(18,dy*.85,14,dy*.6,0,dy<0?-20:20);
+    ctx.strokeStyle=c1; ctx.lineWidth=.9; ctx.stroke();
+    // leaf tip
+    ctx.beginPath(); ctx.arc(0,dy,3.5,0,Math.PI*2);
+    ctx.fillStyle=c1; ctx.globalAlpha=.7; ctx.fill(); ctx.globalAlpha=1;
+  }
+  // side scrolls
+  for(const dx of[-50,50]){
+    ctx.beginPath();
+    ctx.moveTo(dx<0?-14:14,0);
+    ctx.bezierCurveTo(dx*.55,-12,dx*.85,-10,dx,0);
+    ctx.bezierCurveTo(dx*.85,10,dx*.55,12,dx<0?-14:14,0);
+    ctx.strokeStyle=c1; ctx.lineWidth=.8; ctx.stroke();
+  }
+  ctx.restore();
+}
+
+function drawChinoiserie(ctx,W,H,bg,c1,c2){
+  ctx.fillStyle=bg; ctx.fillRect(0,0,W,H);
+  // subtle linen texture
+  for(let y=0;y<H;y+=3){
+    ctx.strokeStyle=`rgba(120,180,200,${0.03+Math.random()*0.02})`;
+    ctx.lineWidth=1; ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y+Math.random()*2-1); ctx.stroke();
+  }
+  // pagoda + branches tiling
+  const TW=160,TH=200;
+  for(let ty=-20;ty<H+TH;ty+=TH){
+    for(let tx=0;tx<W+TW;tx+=TW){
+      chinoisMotif(ctx,tx+TW/2,ty+TH/2,c1,c2);
+    }
+  }
+}
+function chinoisMotif(ctx,cx,cy,c1,c2){
+  ctx.save(); ctx.translate(cx,cy);
+  ctx.strokeStyle=c1; ctx.fillStyle='rgba(126,184,201,0.12)'; ctx.lineWidth=1;
+  // pagoda tower
+  for(let l=0;l<3;l++){
+    const lw=(3-l)*18,lh=12,ly=-l*28;
+    ctx.beginPath(); ctx.moveTo(-lw/2,ly); ctx.lineTo(lw/2,ly);
+    ctx.lineTo(lw/2*.8,ly-lh); ctx.lineTo(-lw/2*.8,ly-lh); ctx.closePath();
+    ctx.stroke(); ctx.fill();
+    // eave curl
+    ctx.beginPath(); ctx.moveTo(-lw/2,ly); ctx.quadraticCurveTo(-lw/2-8,ly-4,-lw/2-4,ly-10); ctx.stroke();
+    ctx.beginPath(); ctx.moveTo(lw/2,ly); ctx.quadraticCurveTo(lw/2+8,ly-4,lw/2+4,ly-10); ctx.stroke();
+  }
+  // branches left
+  function branch(sx,sy,ang,len,depth){
+    if(depth===0||len<5)return;
+    const ex=sx+Math.cos(ang)*len, ey=sy+Math.sin(ang)*len;
+    ctx.beginPath(); ctx.moveTo(sx,sy); ctx.lineTo(ex,ey);
+    ctx.strokeStyle=c1; ctx.lineWidth=depth*.5; ctx.stroke();
+    // blossom
+    if(depth===1){ctx.beginPath();ctx.arc(ex,ey,3,0,Math.PI*2);ctx.fillStyle=c2;ctx.globalAlpha=.6;ctx.fill();ctx.globalAlpha=1;}
+    branch(ex,ey,ang-0.45,len*.65,depth-1);
+    branch(ex,ey,ang+0.35,len*.6,depth-1);
+  }
+  branch(-42,30,-Math.PI/3.5,28,3);
+  branch(42,30,-Math.PI*2/3.5,28,3);
+  // bird silhouette
+  ctx.beginPath(); ctx.fillStyle=c1; ctx.globalAlpha=.55;
+  ctx.ellipse(55,-60,8,3.5,-.4,0,Math.PI*2); ctx.fill();
+  ctx.beginPath(); ctx.arc(63,-62.5,3.5,0,Math.PI*2); ctx.fill();
+  ctx.globalAlpha=1;
+  ctx.restore();
+}
+
+function drawGrasscloth(ctx,W,H,bg,c1,c2){
+  ctx.fillStyle=bg; ctx.fillRect(0,0,W,H);
+  // weave
+  const sw=4;
+  for(let y=0;y<H;y+=sw){
+    for(let x=0;x<W;x+=sw*2){
+      const ox=(Math.floor(y/sw)%2)*sw;
+      ctx.fillStyle=c1; ctx.globalAlpha=0.25+Math.random()*.15;
+      ctx.fillRect(x+ox,y,sw,sw);
+    }
+  }
+  ctx.globalAlpha=1;
+  // vertical fibers
+  for(let x=0;x<W;x+=2+Math.random()*3){
+    ctx.strokeStyle=`rgba(138,158,106,${0.06+Math.random()*0.1})`;
+    ctx.lineWidth=0.5+Math.random()*.8;
+    ctx.beginPath(); ctx.moveTo(x,0);
+    let py=0;
+    while(py<H){py+=4+Math.random()*8;ctx.lineTo(x+(Math.random()-.5)*2,py);}
+    ctx.stroke();
+  }
+  // seam lines
+  const seamW=W/3;
+  for(let s=1;s<3;s++){
+    ctx.strokeStyle='rgba(30,20,8,0.18)'; ctx.lineWidth=1.2;
+    ctx.beginPath(); ctx.moveTo(s*seamW,0); ctx.lineTo(s*seamW+Math.random()*4-2,H); ctx.stroke();
+  }
+}
+
+function drawToile(ctx,W,H,bg,ink){
+  ctx.fillStyle=bg; ctx.fillRect(0,0,W,H);
+  // subtle linen
+  const id=ctx.createImageData(W,H); const d=id.data;
+  for(let i=0;i<d.length;i+=4){const n=(Math.random()-.5)*10;d[i]=244+n;d[i+1]=237+n;d[i+2]=224+n;d[i+3]=255;}
+  ctx.putImageData(id,0,0);
+  // toile vignette scenes tiled
+  const TW=140,TH=160;
+  for(let ty=0;ty<H+TH;ty+=TH){
+    for(let tx=0;tx<W+TW;tx+=TW){
+      const ox=(ty/TH%2===0)?0:TW/2;
+      toileScene(ctx,tx+ox,ty,ink,(tx+ty)%280===0?'tree':'urn');
+    }
+  }
+}
+function toileScene(ctx,cx,cy,ink,type){
+  ctx.save(); ctx.translate(cx,cy);
+  ctx.strokeStyle=ink; ctx.fillStyle='rgba(122,74,46,0.08)'; ctx.lineWidth=0.8;
+  if(type==='urn'){
+    // classical urn
+    ctx.beginPath(); ctx.moveTo(-10,20); ctx.quadraticCurveTo(-18,0,-8,-20); ctx.lineTo(8,-20);
+    ctx.quadraticCurveTo(18,0,10,20); ctx.closePath(); ctx.stroke(); ctx.fill();
+    ctx.beginPath(); ctx.moveTo(-14,-20); ctx.lineTo(14,-20); ctx.stroke();
+    ctx.beginPath(); ctx.ellipse(0,-22,10,4,0,0,Math.PI*2); ctx.stroke();
+    // handles
+    ctx.beginPath(); ctx.moveTo(-8,-10); ctx.bezierCurveTo(-20,-10,-20,5,-8,5); ctx.stroke();
+    ctx.beginPath(); ctx.moveTo(8,-10); ctx.bezierCurveTo(20,-10,20,5,8,5); ctx.stroke();
+    // flowers from urn
+    for(let f=0;f<3;f++){
+      const a=-Math.PI/2+f*.4-.4;
+      ctx.beginPath(); ctx.moveTo(0,-22); ctx.quadraticCurveTo(Math.cos(a)*16,-22+Math.sin(a)*28,Math.cos(a)*22,-22+Math.sin(a)*40); ctx.stroke();
+      ctx.beginPath(); ctx.arc(Math.cos(a)*22,-22+Math.sin(a)*40,4,0,Math.PI*2); ctx.stroke();
+    }
+  } else {
+    // weeping willow silhouette
+    ctx.beginPath(); ctx.moveTo(0,30); ctx.lineTo(0,-35); ctx.stroke();
+    for(let b=0;b<7;b++){
+      const a=-Math.PI*.55+b*.18;
+      const bx=Math.cos(a)*30, by=Math.sin(a)*30-30;
+      ctx.beginPath(); ctx.moveTo(0,-10);
+      ctx.quadraticCurveTo(bx*.5,by*.5-10,bx,by);
+      ctx.quadraticCurveTo(bx+Math.cos(a+.4)*12,by+15,bx+Math.cos(a+.6)*8,by+28); ctx.stroke();
+    }
+  }
+  ctx.restore();
+}
+
+/* ─────────────────────────────────────────────────────────
+   AUDIO ENGINE (procedural Web Audio)
+───────────────────────────────────────────────────────── */
+let audioCtx=null, muted=false;
+function ensureAudio(){
+  if(!audioCtx) audioCtx=new(window.AudioContext||window.webkitAudioContext)();
+}
+function tearSound(intensity=0.5){
+  if(muted||!audioCtx)return;
+  const buf=audioCtx.createBuffer(1,audioCtx.sampleRate*0.06,audioCtx.sampleRate);
+  const d=buf.getChannelData(0);
+  for(let i=0;i<d.length;i++){
+    d[i]=(Math.random()*2-1)*Math.exp(-i/d.length*18)*intensity;
+  }
+  const src=audioCtx.createBufferSource();
+  src.buffer=buf;
+  const f=audioCtx.createBiquadFilter();
+  f.type='bandpass'; f.frequency.value=1800+Math.random()*600; f.Q.value=0.8;
+  const g=audioCtx.createGain(); g.gain.value=0.35;
+  src.connect(f); f.connect(g); g.connect(audioCtx.destination);
+  src.start();
+}
+function chimeSound(){
+  if(muted||!audioCtx)return;
+  const freqs=[523,659,784,1047];
+  freqs.forEach((hz,i)=>{
+    const o=audioCtx.createOscillator();
+    const g=audioCtx.createGain();
+    o.type='sine'; o.frequency.value=hz;
+    g.gain.setValueAtTime(0,audioCtx.currentTime+i*.12);
+    g.gain.linearRampToValueAtTime(0.18,audioCtx.currentTime+i*.12+.03);
+    g.gain.exponentialRampToValueAtTime(0.001,audioCtx.currentTime+i*.12+1.1);
+    o.connect(g); g.connect(audioCtx.destination);
+    o.start(audioCtx.currentTime+i*.12);
+    o.stop(audioCtx.currentTime+i*.12+1.2);
+  });
+}
+
+/* ─────────────────────────────────────────────────────────
+   DUST PARTICLES
+───────────────────────────────────────────────────────── */
+const dustCanvas=document.getElementById('dust-canvas');
+const dctx=dustCanvas.getContext('2d');
+let particles=[];
+function spawnDust(x,y,count=12){
+  for(let i=0;i<count;i++){
+    particles.push({
+      x,y,vx:(Math.random()-.5)*3.5,vy:(Math.random()-.5)*3-1.5,
+      life:1,decay:0.02+Math.random()*0.04,r:1.5+Math.random()*3,
+      hue:Math.random()>.5?'rgba(201,169,97,':'rgba(220,200,160,'
+    });
+  }
+}
+function updateDust(){
+  dctx.clearRect(0,0,dustCanvas.width,dustCanvas.height);
+  particles=particles.filter(p=>{
+    p.x+=p.vx; p.y+=p.vy; p.vy+=0.08; p.life-=p.decay;
+    if(p.life<=0)return false;
+    dctx.beginPath();
+    dctx.fillStyle=p.hue+p.life*.6+')';
+    dctx.arc(p.x,p.y,p.r*p.life,0,Math.PI*2);
+    dctx.fill();
+    return true;
+  });
+}
+
+/* ─────────────────────────────────────────────────────────
+   MAIN GAME
+───────────────────────────────────────────────────────── */
+const wrap=document.getElementById('wall-wrap');
+const revCanvas=document.getElementById('canvas-reveal');
+const topCanvas=document.getElementById('canvas-top');
+const rctx=revCanvas.getContext('2d');
+const tctx=topCanvas.getContext('2d');
+const revBar=document.getElementById('reveal-bar');
+const pctLabel=document.getElementById('pct-label');
+const scoreLabel=document.getElementById('score-label');
+const timerWrap=document.getElementById('timer-wrap');
+const timerVal=document.getElementById('timer-val');
+const completion=document.getElementById('completion');
+const completionTitle=document.getElementById('completion-title');
+const completionSub=document.getElementById('completion-sub');
+const completionScore=document.getElementById('completion-score');
+const bestLabel=document.getElementById('best-label');
+
+let W=0,H=0,pairIdx=0,peelMask=null,totalPx=0,revealedPx=0;
+let isDragging=false,lastX=0,lastY=0;
+let completed=false,peelRadius=44;
+// torn edge points for rendering
+let tornEdges=[];
+let peelProgress=0; // 0..1
+
+// Score mode
+let scoreMode=false,timerSec=30,timerInt=null,gameScore=0;
+let bestScore=parseInt(localStorage.getItem('dw_reveal_best')||'0');
+
+function resize(){
+  const rect=wrap.getBoundingClientRect();
+  W=Math.round(rect.width); H=Math.round(rect.height);
+  revCanvas.width=topCanvas.width=dustCanvas.width=W;
+  revCanvas.height=topCanvas.height=dustCanvas.height=H;
+  peelMask=new Uint8Array(W*H);
+  totalPx=W*H;
+  if(W>0&&H>0) initWall();
+}
+
+function initWall(){
+  completed=false; revealedPx=0; peelProgress=0;
+  particles=[];
+  peelMask.fill(0);
+  tornEdges=[];
+  // draw reveal (new) layer
+  rctx.clearRect(0,0,W,H);
+  PAIRINGS[pairIdx].drawNew(rctx,W,H);
+  // draw old layer on top canvas
+  tctx.clearRect(0,0,W,H);
+  PAIRINGS[pairIdx].drawOld(tctx,W,H);
+  // reset mask on top canvas — initially fully opaque old layer
+  updateTopLayer();
+  updateHUD();
+  completion.classList.remove('show');
+  revBar.style.width='0%';
+  pctLabel.textContent='0%';
+  if(scoreMode&&timerInt){clearInterval(timerInt);timerInt=null;}
+}
+
+// Track which pixels are peeled using an off-screen canvas for efficiency
+// We maintain a mask array and redraw the top canvas using compositing
+const BRUSH_STAMP=64; // max brush
+function peel(x,y,brushR){
+  brushR=Math.min(brushR,BRUSH_STAMP);
+  const x0=Math.max(0,x-brushR|0), x1=Math.min(W,x+brushR|0);
+  const y0=Math.max(0,y-brushR|0), y1=Math.min(H,y+brushR|0);
+  let newRevealed=0;
+  const r2=brushR*brushR;
+  // irregular tear edge: use elliptical brush with jagged boundary
+  for(let py=y0;py<y1;py++){
+    for(let px=x0;px<x1;px++){
+      const dx=px-x, dy=py-y;
+      // jagged: add noise based on angle
+      const ang=Math.atan2(dy,dx);
+      const noise=1+0.28*Math.sin(ang*7+px*.3+py*.4);
+      const dist2=dx*dx+dy*dy;
+      if(dist2<r2*noise*noise && peelMask[py*W+px]===0){
+        peelMask[py*W+px]=1;
+        newRevealed++;
+      }
+    }
+  }
+  return newRevealed;
+}
+
+function updateTopLayer(){
+  // Redraw old layer then cut holes via destination-out
+  tctx.clearRect(0,0,W,H);
+  PAIRINGS[pairIdx].drawOld(tctx,W,H);
+  // draw torn paper backing strip along edges — subtle cream underside
+  // Cut out peeled region
+  tctx.save();
+  tctx.globalCompositeOperation='destination-out';
+  // Build peeled region path from mask using imagedata
+  // For performance, just putImageData with alpha=0 where peeled
+  tctx.restore();
+  // More efficient: directly write alpha channel
+  const id=tctx.getImageData(0,0,W,H);
+  const d=id.data;
+  for(let i=0;i<peelMask.length;i++){
+    if(peelMask[i]===1){
+      d[i*4+3]=0;
+    }
+  }
+  tctx.putImageData(id,0,0);
+  // Draw torn paper edge effect over transition
+  drawTornEdge();
+}
+
+// Torn edge rendering: find boundary pixels and draw curled paper effect
+function drawTornEdge(){
+  // Sample boundary pixels
+  const edgePts=[];
+  const step=6;
+  for(let y=step;y<H-step;y+=step){
+    for(let x=step;x<W-step;x+=step){
+      if(peelMask[y*W+x]===1){
+        // check if any neighbor is 0
+        const hasNeighbor=(
+          peelMask[(y-step)*W+x]===0||peelMask[(y+step)*W+x]===0||
+          peelMask[y*W+(x-step)]===0||peelMask[y*W+(x+step)]===0
+        );
+        if(hasNeighbor) edgePts.push([x,y]);
+      }
+    }
+  }
+  if(edgePts.length<3)return;
+  // Draw backing paper crescent (cream-beige tinted edge) around each edge cluster
+  tctx.save();
+  for(const [ex,ey] of edgePts){
+    // Shadow cast into revealed area
+    const g=tctx.createRadialGradient(ex,ey,0,ex,ey,12);
+    g.addColorStop(0,'rgba(0,0,0,0.22)');
+    g.addColorStop(1,'rgba(0,0,0,0)');
+    tctx.fillStyle=g;
+    tctx.beginPath(); tctx.arc(ex,ey,12,0,Math.PI*2); tctx.fill();
+  }
+  // Backing paper crescent — cream underside of old wallpaper
+  for(const [ex,ey] of edgePts){
+    // Only draw on the peeled side inward a few px
+    if(peelMask[ey*W+ex]===1){
+      const g=tctx.createRadialGradient(ex,ey,0,ex,ey,7);
+      g.addColorStop(0,'rgba(245,235,210,0.9)');
+      g.addColorStop(.5,'rgba(220,205,180,0.55)');
+      g.addColorStop(1,'rgba(200,185,160,0)');
+      tctx.fillStyle=g;
+      tctx.beginPath(); tctx.arc(ex,ey,7,0,Math.PI*2); tctx.fill();
+    }
+  }
+  tctx.restore();
+}
+
+function updateHUD(){
+  const pct=Math.round(revealedPx/totalPx*100);
+  revBar.style.width=pct+'%';
+  pctLabel.textContent=pct+'%';
+  peelProgress=revealedPx/totalPx;
+}
+
+// Shimmer overlay on reveal
+let shimmerT=0;
+function drawShimmer(){
+  if(peelProgress<0.05)return;
+  tctx.save();
+  shimmerT+=0.018;
+  const sx=W/2+Math.cos(shimmerT)*W*.4;
+  const g=tctx.createLinearGradient(sx-120,0,sx+120,H);
+  g.addColorStop(0,'rgba(255,255,255,0)');
+  g.addColorStop(.5,`rgba(255,255,255,${0.04*peelProgress})`);
+  g.addColorStop(1,'rgba(255,255,255,0)');
+  tctx.globalCompositeOperation='source-over';
+  tctx.fillStyle=g;
+  // Only apply shimmer where revealed
+  // Use destination-in trick: draw shimmer only in revealed area
+  // Build temp canvas
+  tctx.restore();
+}
+
+let lastTearX=-999,lastTearY=-999;
+function onDrag(x,y){
+  if(completed)return;
+  ensureAudio();
+  const dx=x-lastTearX, dy=y-lastTearY;
+  const dist=Math.sqrt(dx*dx+dy*dy);
+  if(dist<6)return;
+  lastTearX=x; lastTearY=y;
+  // adaptive radius: faster drag = bigger brush
+  const speed=Math.min(dist,60);
+  const radius=peelRadius+speed*.22;
+  const newPx=peel(x,y,radius);
+  revealedPx=Math.min(totalPx,revealedPx+newPx);
+  updateTopLayer();
+  updateHUD();
+  // Tear sound every ~40px drag
+  if(dist>10) tearSound(0.3+Math.min(speed/60,.7)*.4);
+  // Dust puff
+  if(dist>14) spawnDust(x,y,Math.round(4+speed*.15));
+  // Score mode
+  if(scoreMode) gameScore+=Math.floor(newPx*10);
+  scoreLabel.textContent=scoreMode?'Score: '+gameScore.toLocaleString():'';
+  // Check completion
+  const pct=revealedPx/totalPx;
+  if(pct>=0.82&&!completed) triggerCompletion();
+}
+
+function triggerCompletion(){
+  completed=true;
+  chimeSound();
+  completionTitle.textContent='Revealed';
+  completionSub.textContent=PAIRINGS[pairIdx].newName;
+  let scoreStr='';
+  if(scoreMode){
+    clearInterval(timerInt); timerInt=null; timerWrap.classList.remove('active');
+    const elapsed=30-timerSec;
+    scoreStr=`Score: ${gameScore.toLocaleString()} · ${elapsed}s`;
+    if(gameScore>bestScore){
+      bestScore=gameScore;
+      localStorage.setItem('dw_reveal_best',bestScore);
+      bestLabel.textContent='Best: '+bestScore.toLocaleString();
+      scoreStr+=' · New Best!';
+    }
+  }
+  completionScore.textContent=scoreStr;
+  completion.classList.add('show');
+  // Shimmer pulse — trigger 3 bursts
+  let bursts=0;
+  function burst(){
+    if(bursts>=4||!completed)return;
+    spawnDust(W/2,H/2,30);
+    for(let i=0;i<6;i++) setTimeout(()=>spawnDust(Math.random()*W,Math.random()*H,8),i*80);
+    bursts++; setTimeout(burst,400);
+  }
+  burst();
+}
+
+/* ─── SCORE MODE ─── */
+function startScoreTimer(){
+  timerSec=30; gameScore=0; scoreLabel.textContent='Score: 0';
+  timerVal.textContent=timerSec;
+  timerWrap.classList.add('active');
+  clearInterval(timerInt);
+  timerInt=setInterval(()=>{
+    timerSec--;
+    timerVal.textContent=timerSec;
+    if(timerSec<=0){
+      clearInterval(timerInt); timerInt=null;
+      if(!completed) triggerCompletion();
+    }
+  },1000);
+}
+
+/* ─── RAF LOOP ─── */
+function loop(){
+  updateDust();
+  requestAnimationFrame(loop);
+}
+requestAnimationFrame(loop);
+
+/* ─── INPUT ─── */
+function getXY(e){
+  const rect=wrap.getBoundingClientRect();
+  const src=e.touches?e.touches[0]:e;
+  return [(src.clientX-rect.left)*(W/rect.width)|0,(src.clientY-rect.top)*(H/rect.height)|0];
+}
+wrap.addEventListener('mousedown',e=>{
+  ensureAudio();
+  isDragging=true;
+  const [x,y]=getXY(e);
+  lastTearX=x; lastTearY=y;
+  spawnDust(x,y,6);
+});
+wrap.addEventListener('mousemove',e=>{if(isDragging){const [x,y]=getXY(e);onDrag(x,y);}});
+window.addEventListener('mouseup',()=>{isDragging=false;});
+wrap.addEventListener('touchstart',e=>{
+  e.preventDefault(); ensureAudio();
+  isDragging=true;
+  const [x,y]=getXY(e);
+  lastTearX=x; lastTearY=y;
+},{passive:false});
+wrap.addEventListener('touchmove',e=>{
+  e.preventDefault();
+  if(isDragging){const [x,y]=getXY(e);onDrag(x,y);}
+},{passive:false});
+wrap.addEventListener('touchend',()=>{isDragging=false;});
+
+/* ─── BUTTONS ─── */
+document.getElementById('mute-btn').addEventListener('click',function(){
+  muted=!muted;
+  this.textContent=muted?'Unmute':'Mute';
+  this.classList.toggle('muted',muted);
+});
+const newBtn=document.getElementById('new-btn');
+const nextBtn=document.getElementById('next-btn');
+function nextWall(){
+  pairIdx=(pairIdx+1)%PAIRINGS.length;
+  initWall();
+  if(scoreMode) startScoreTimer();
+}
+newBtn.addEventListener('click',nextWall);
+nextBtn.addEventListener('click',()=>{nextWall();});
+
+document.getElementById('mode-btn').addEventListener('click',function(){
+  scoreMode=!scoreMode;
+  this.classList.toggle('active',scoreMode);
+  this.textContent=scoreMode?'Score: ON':'Score Mode';
+  if(scoreMode){
+    initWall(); startScoreTimer();
+  } else {
+    clearInterval(timerInt); timerInt=null;
+    timerWrap.classList.remove('active');
+    scoreLabel.textContent='';
+    initWall();
+  }
+});
+
+// Show best score
+if(bestScore>0) bestLabel.textContent='Best: '+bestScore.toLocaleString();
+
+/* ─── INIT ─── */
+const ro=new ResizeObserver(resize);
+ro.observe(wrap);
+resize();
+</script>
+</body>
+</html>

← 410f295 Agent-driven toy: DW Toile Coloring (frontend-developer) — f  ·  back to Model Arena  ·  Agent-driven toy: DW The Big Reveal (frontend-developer) — d 5d097ae →