[object Object]

← back to Model Arena

Agent-driven art toy: DW Damask Kaleidoscope (frontend-developer) — 8-fold bezier damask, mouse-warp, 5 palettes, PNG export. Registered.

71faf2787e01120b266777ce78526cedc8be7a98 · 2026-07-25 11:47:12 -0700 · Steve Abrams

Files touched

Diff

commit 71faf2787e01120b266777ce78526cedc8be7a98
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Jul 25 11:47:12 2026 -0700

    Agent-driven art toy: DW Damask Kaleidoscope (frontend-developer) — 8-fold bezier damask, mouse-warp, 5 palettes, PNG export. Registered.
---
 data/arcade-games.json                   |   6 +
 public/games/dw-content-calendar.html    | 787 +++++++++++++++++++++++++++++++
 public/games/dw-damask-kaleidoscope.html | 502 ++++++++++++++++++++
 3 files changed, 1295 insertions(+)

diff --git a/data/arcade-games.json b/data/arcade-games.json
index 28c8f76..bd3a7b2 100644
--- a/data/arcade-games.json
+++ b/data/arcade-games.json
@@ -71,6 +71,12 @@
       "title": "DW The Lookbook",
       "credit": "frontend-developer",
       "note": "Agent-built editorial \u2014 6 swipeable style spreads (Grandmillennial\u2192Modern Minimal), each with SVG hero + palette"
+    },
+    {
+      "slug": "dw-damask-kaleidoscope",
+      "title": "DW Damask Kaleidoscope",
+      "credit": "frontend-developer",
+      "note": "Agent-built generative-art toy \u2014 8-fold bezier damask kaleidoscope, mouse-warp, 5 palettes, PNG export"
     }
   ]
 }
\ No newline at end of file
diff --git a/public/games/dw-content-calendar.html b/public/games/dw-content-calendar.html
new file mode 100644
index 0000000..eb01c04
--- /dev/null
+++ b/public/games/dw-content-calendar.html
@@ -0,0 +1,787 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Designer Wallcoverings — Content Calendar</title>
+<style>
+/* ── tokens ── */
+:root{
+  --bg:#f4f1ea;
+  --bg2:#ede9e0;
+  --bg3:#e6e1d6;
+  --ink:#1c1a17;
+  --ink2:#4a4540;
+  --ink3:#7a746c;
+  --accent:#c9a961;
+  --accent2:#a8883e;
+  --panel:#faf8f4;
+  --hair:#d6cfc3;
+  --serif:'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif;
+  --sans:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
+  --ease:cubic-bezier(.4,0,.2,1);
+  --r:6px;
+  --shadow:0 2px 16px rgba(28,26,23,.10),0 1px 4px rgba(28,26,23,.06);
+  --panel-w:340px;
+}
+*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
+html{font-size:16px;scroll-behavior:smooth}
+body{
+  background:var(--bg);
+  color:var(--ink);
+  font-family:var(--sans);
+  min-height:100vh;
+  display:flex;
+  flex-direction:column;
+}
+
+/* ── header ── */
+header{
+  background:var(--bg);
+  border-bottom:1px solid var(--hair);
+  padding:18px 28px 14px;
+  display:flex;
+  align-items:baseline;
+  gap:24px;
+  flex-wrap:wrap;
+}
+.wordmark{
+  font-family:var(--serif);
+  font-size:clamp(28px,3.5vw,44px);
+  font-weight:600;
+  letter-spacing:.18em;
+  text-transform:uppercase;
+  color:var(--ink);
+  line-height:1;
+  white-space:nowrap;
+}
+.sub{
+  font-size:11px;
+  letter-spacing:.22em;
+  text-transform:uppercase;
+  color:var(--ink3);
+  font-family:var(--sans);
+}
+
+/* ── layout shell ── */
+.shell{
+  display:flex;
+  flex:1;
+  min-height:0;
+  overflow:hidden;
+}
+.main{
+  flex:1;
+  min-width:0;
+  display:flex;
+  flex-direction:column;
+  padding:24px 20px 24px 24px;
+  gap:16px;
+  overflow-y:auto;
+}
+
+/* ── month nav ── */
+.month-nav{
+  display:flex;
+  align-items:center;
+  gap:16px;
+  flex-wrap:wrap;
+}
+.month-title{
+  font-family:var(--serif);
+  font-size:clamp(20px,2.5vw,30px);
+  font-weight:600;
+  letter-spacing:.06em;
+  color:var(--ink);
+  min-width:200px;
+}
+.nav-btn{
+  background:none;
+  border:1px solid var(--hair);
+  border-radius:var(--r);
+  width:34px;height:34px;
+  cursor:pointer;
+  display:flex;align-items:center;justify-content:center;
+  color:var(--ink2);
+  font-size:16px;
+  transition:border-color .15s var(--ease),background .15s var(--ease),color .15s var(--ease);
+}
+.nav-btn:hover{border-color:var(--accent);background:var(--accent);color:#fff}
+.today-btn{
+  background:none;border:1px solid var(--hair);border-radius:var(--r);
+  padding:6px 14px;font-size:12px;letter-spacing:.08em;text-transform:uppercase;
+  cursor:pointer;color:var(--ink2);
+  transition:all .15s var(--ease);
+  font-family:var(--sans);
+}
+.today-btn:hover{border-color:var(--accent);color:var(--accent)}
+.stats{
+  margin-left:auto;
+  display:flex;gap:18px;align-items:center;flex-wrap:wrap;
+}
+.stat{
+  font-size:12px;color:var(--ink3);
+  display:flex;align-items:center;gap:6px;
+}
+.stat strong{color:var(--ink);font-size:14px}
+
+/* ── week strip ── */
+.week-strip{
+  background:var(--bg2);
+  border:1px solid var(--hair);
+  border-radius:var(--r);
+  padding:12px 16px;
+  display:flex;
+  gap:8px;
+  overflow-x:auto;
+  flex-shrink:0;
+}
+.week-strip::-webkit-scrollbar{height:4px}
+.week-strip::-webkit-scrollbar-thumb{background:var(--hair);border-radius:2px}
+.week-day{
+  flex:0 0 auto;
+  min-width:90px;
+  background:var(--panel);
+  border:1px solid var(--hair);
+  border-radius:var(--r);
+  padding:8px 10px;
+  font-size:11px;
+}
+.week-day .wd-label{font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink3);margin-bottom:4px}
+.week-day .wd-date{font-family:var(--serif);font-size:16px;color:var(--ink);font-weight:600}
+.week-day .wd-chips{display:flex;flex-wrap:wrap;gap:3px;margin-top:5px}
+
+/* ── calendar grid ── */
+.cal-wrap{border:1px solid var(--hair);border-radius:var(--r);overflow:hidden;background:var(--panel)}
+.cal-head{
+  display:grid;grid-template-columns:repeat(7,1fr);
+  border-bottom:1px solid var(--hair);
+  background:var(--bg2);
+}
+.cal-dow{
+  padding:8px 6px;
+  font-size:11px;letter-spacing:.12em;text-transform:uppercase;
+  color:var(--ink3);text-align:center;font-family:var(--sans);
+}
+.cal-grid{
+  display:grid;
+  grid-template-columns:repeat(7,1fr);
+}
+.cal-cell{
+  border-right:1px solid var(--hair);
+  border-bottom:1px solid var(--hair);
+  min-height:110px;
+  padding:8px 6px 6px;
+  cursor:pointer;
+  position:relative;
+  transition:background .12s var(--ease);
+  display:flex;flex-direction:column;gap:4px;
+}
+.cal-cell:hover{background:var(--bg2)}
+.cal-cell.empty{background:var(--bg);cursor:default}
+.cal-cell.empty:hover{background:var(--bg)}
+.cal-cell.today .cell-num{
+  background:var(--accent);color:#fff;
+  width:22px;height:22px;border-radius:50%;
+  display:flex;align-items:center;justify-content:center;
+}
+.cal-cell.selected{background:rgba(201,169,97,.07);outline:2px solid var(--accent);outline-offset:-2px;}
+.cell-num{font-size:12px;color:var(--ink2);font-weight:500;width:22px;height:22px;display:flex;align-items:center;justify-content:center}
+.cell-chips{display:flex;flex-wrap:wrap;gap:3px;flex:1}
+.add-hint{
+  font-size:10px;color:var(--hair);
+  position:absolute;bottom:6px;right:6px;
+  opacity:0;transition:opacity .15s var(--ease);
+  pointer-events:none;
+}
+.cal-cell:not(.empty):hover .add-hint{opacity:1}
+
+/* ── chips ── */
+.chip{
+  display:inline-flex;align-items:center;gap:3px;
+  padding:3px 7px;
+  border-radius:20px;
+  font-size:10px;font-weight:600;letter-spacing:.04em;
+  cursor:pointer;
+  position:relative;
+  user-select:none;
+  border:none;
+  white-space:nowrap;
+  transition:transform .12s var(--ease),filter .12s var(--ease);
+  animation:chipPop .2s var(--ease);
+}
+.chip:hover{transform:translateY(-1px);filter:brightness(1.08)}
+@keyframes chipPop{
+  0%{transform:scale(.6);opacity:0}
+  70%{transform:scale(1.08)}
+  100%{transform:scale(1);opacity:1}
+}
+.chip-reel{background:#e8d8b8;color:#6b4c18}
+.chip-carousel{background:#d4e4d8;color:#1e4d2b}
+.chip-story{background:#ddd8f0;color:#3d2e82}
+.chip-sale{background:#f0d8d8;color:#7a1f1f}
+.chip-editorial{background:#d8eaf0;color:#1a4a5c}
+.chip-sm{padding:2px 5px;font-size:9px}
+
+/* ── tooltip ── */
+[data-tip]{position:relative}
+[data-tip]::after{
+  content:attr(data-tip);
+  position:absolute;
+  bottom:calc(100% + 6px);
+  left:50%;transform:translateX(-50%);
+  background:var(--ink);color:#f4f1ea;
+  font-size:11px;font-weight:400;letter-spacing:.01em;
+  padding:5px 9px;border-radius:4px;
+  white-space:nowrap;max-width:200px;white-space:normal;
+  pointer-events:none;
+  opacity:0;transition:opacity .15s var(--ease);
+  z-index:100;
+  line-height:1.4;
+}
+[data-tip]:hover::after{opacity:1}
+
+/* ── legend ── */
+.legend{
+  display:flex;gap:12px;flex-wrap:wrap;align-items:center;
+}
+.legend-item{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--ink2);cursor:pointer}
+.legend-dot{width:10px;height:10px;border-radius:50%}
+
+/* ── right panel ── */
+.side-panel{
+  width:var(--panel-w);
+  flex-shrink:0;
+  border-left:1px solid var(--hair);
+  background:var(--panel);
+  display:flex;flex-direction:column;
+  overflow:hidden;
+  transform:translateX(var(--panel-w));
+  visibility:hidden;
+  transition:transform .28s var(--ease),visibility 0s .28s;
+}
+.side-panel.open{
+  transform:translateX(0);
+  visibility:visible;
+  transition:transform .28s var(--ease),visibility 0s 0s;
+}
+.panel-head{
+  padding:20px 20px 14px;
+  border-bottom:1px solid var(--hair);
+  display:flex;align-items:flex-start;justify-content:space-between;gap:12px;
+}
+.panel-date{font-family:var(--serif);font-size:22px;font-weight:600;color:var(--ink)}
+.panel-close{
+  background:none;border:none;cursor:pointer;
+  width:28px;height:28px;border-radius:50%;
+  display:flex;align-items:center;justify-content:center;
+  color:var(--ink3);font-size:18px;
+  transition:background .12s var(--ease),color .12s var(--ease);
+  flex-shrink:0;
+}
+.panel-close:hover{background:var(--bg3);color:var(--ink)}
+.panel-body{flex:1;overflow-y:auto;padding:16px 20px}
+.panel-body::-webkit-scrollbar{width:4px}
+.panel-body::-webkit-scrollbar-thumb{background:var(--hair);border-radius:2px}
+.post-card{
+  border:1px solid var(--hair);border-radius:var(--r);
+  margin-bottom:12px;overflow:hidden;
+}
+.post-card-head{
+  padding:8px 12px;
+  display:flex;align-items:center;justify-content:space-between;
+}
+.post-card-type{font-size:11px;font-weight:700;letter-spacing:.1em;text-transform:uppercase}
+.post-card-del{
+  background:none;border:none;cursor:pointer;
+  color:var(--ink3);font-size:16px;line-height:1;
+  padding:2px 4px;border-radius:3px;
+  transition:color .12s var(--ease),background .12s var(--ease);
+}
+.post-card-del:hover{color:#7a1f1f;background:#f0d8d8}
+.post-card-body{padding:8px 12px 12px}
+.caption-area{
+  width:100%;border:1px solid var(--hair);border-radius:4px;
+  background:var(--bg);color:var(--ink);
+  font-family:var(--sans);font-size:12px;line-height:1.5;
+  padding:7px 10px;resize:vertical;min-height:72px;
+  outline:none;transition:border-color .15s var(--ease);
+}
+.caption-area:focus{border-color:var(--accent)}
+.hashtags{
+  margin-top:6px;font-size:10px;color:var(--ink3);
+  line-height:1.5;letter-spacing:.01em;
+}
+.add-post-bar{
+  padding:12px 20px;border-top:1px solid var(--hair);
+  display:flex;gap:8px;flex-wrap:wrap;
+}
+.add-post-btn{
+  flex:1;min-width:60px;
+  padding:7px 6px;
+  border-radius:20px;font-size:10px;font-weight:700;
+  letter-spacing:.05em;text-transform:uppercase;
+  cursor:pointer;border:2px solid transparent;
+  transition:transform .12s var(--ease),filter .12s var(--ease),border-color .15s var(--ease);
+}
+.add-post-btn:hover{transform:translateY(-1px);filter:brightness(1.1)}
+.add-post-btn:active{transform:scale(.96)}
+.add-post-btn.reel{background:#e8d8b8;color:#6b4c18}
+.add-post-btn.carousel{background:#d4e4d8;color:#1e4d2b}
+.add-post-btn.story{background:#ddd8f0;color:#3d2e82}
+.add-post-btn.sale{background:#f0d8d8;color:#7a1f1f}
+.add-post-btn.editorial{background:#d8eaf0;color:#1a4a5c}
+.empty-state{
+  text-align:center;padding:32px 16px;color:var(--ink3);font-size:13px;line-height:1.6;
+}
+.empty-state span{display:block;font-size:28px;margin-bottom:8px;opacity:.5}
+
+/* ── this week strip ── */
+.strip-label{
+  font-size:11px;letter-spacing:.12em;text-transform:uppercase;
+  color:var(--ink3);margin-bottom:8px;
+}
+
+/* ── responsive ── */
+@media(max-width:800px){
+  :root{--panel-w:100vw}
+  .shell{flex-direction:column}
+  .side-panel{
+    position:fixed;bottom:0;left:0;right:0;
+    width:100%;height:60vh;
+    border-left:none;border-top:1px solid var(--hair);
+    transform:translateY(60vh);
+  }
+  .side-panel.open{transform:translateY(0)}
+  .cal-cell{min-height:80px}
+  .stats{margin-left:0}
+}
+@media(max-width:480px){
+  .main{padding:12px}
+  .cal-cell{min-height:60px;padding:4px}
+  header{padding:12px 16px 10px}
+}
+</style>
+</head>
+<body>
+
+<header>
+  <span class="wordmark">Designer Wallcoverings</span>
+  <span class="sub">Content Calendar</span>
+</header>
+
+<div class="shell">
+  <main class="main">
+    <!-- month nav -->
+    <div class="month-nav">
+      <button class="nav-btn" id="prevBtn" aria-label="Previous month">
+        <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M10 12L6 8l4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
+      </button>
+      <div class="month-title" id="monthTitle"></div>
+      <button class="nav-btn" id="nextBtn" aria-label="Next month">
+        <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
+      </button>
+      <button class="today-btn" id="todayBtn">Today</button>
+      <div class="stats">
+        <div class="stat"><strong id="statTotal">0</strong> posts this month</div>
+        <div id="typeStats" class="stat" style="gap:10px;flex-wrap:wrap"></div>
+      </div>
+    </div>
+
+    <!-- legend -->
+    <div class="legend" id="legend">
+      <div class="legend-item"><div class="legend-dot" style="background:#e8d8b8"></div>Reel</div>
+      <div class="legend-item"><div class="legend-dot" style="background:#d4e4d8"></div>Carousel</div>
+      <div class="legend-item"><div class="legend-dot" style="background:#ddd8f0"></div>Story</div>
+      <div class="legend-item"><div class="legend-dot" style="background:#f0d8d8"></div>Sale</div>
+      <div class="legend-item"><div class="legend-dot" style="background:#d8eaf0"></div>Editorial</div>
+    </div>
+
+    <!-- this week strip -->
+    <div>
+      <div class="strip-label">This Week at a Glance</div>
+      <div class="week-strip" id="weekStrip"></div>
+    </div>
+
+    <!-- calendar -->
+    <div class="cal-wrap">
+      <div class="cal-head" id="calHead"></div>
+      <div class="cal-grid" id="calGrid"></div>
+    </div>
+  </main>
+
+  <!-- right panel -->
+  <aside class="side-panel" id="sidePanel" aria-label="Day editor" role="complementary">
+    <div class="panel-head">
+      <div>
+        <div class="panel-date" id="panelDate">—</div>
+        <div style="font-size:11px;color:var(--ink3);margin-top:2px" id="panelDow"></div>
+      </div>
+      <button class="panel-close" id="panelClose" aria-label="Close panel">
+        <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 2l10 10M12 2L2 12" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
+      </button>
+    </div>
+    <div class="panel-body" id="panelBody"></div>
+    <div class="add-post-bar">
+      <button class="add-post-btn reel" data-type="Reel">+ Reel</button>
+      <button class="add-post-btn carousel" data-type="Carousel">+ Carousel</button>
+      <button class="add-post-btn story" data-type="Story">+ Story</button>
+      <button class="add-post-btn sale" data-type="Sale">+ Sale</button>
+      <button class="add-post-btn editorial" data-type="Editorial">+ Editorial</button>
+    </div>
+  </aside>
+</div>
+
+<script>
+(function(){
+'use strict';
+
+/* ─── config ─── */
+const TYPES = {
+  Reel:      {cls:'chip-reel',      icon:'🎬', hashtags:'#reels #wallcovering #designerwallcoverings #interiordesign #luxuryhome'},
+  Carousel:  {cls:'chip-carousel',  icon:'🖼',  hashtags:'#carousel #wallpaper #homedesign #designerwallcoverings #patterndesign'},
+  Story:     {cls:'chip-story',     icon:'✨',  hashtags:'#stories #designerwallcoverings #behindthescenes #luxuryinteriors'},
+  Sale:      {cls:'chip-sale',      icon:'🏷',  hashtags:'#sale #designerwallcoverings #wallcovering #limitedtime #luxurydecor'},
+  Editorial: {cls:'chip-editorial', icon:'📰',  hashtags:'#editorial #designerwallcoverings #interiordesign #luxuryliving #wallpaper'},
+};
+
+const CAPTIONS = {
+  Reel:      ['15-sec installation walk-through — premium grasscloth in situ','Slow pan across our new metallic linen collection','Behind the seams: how our artisans hang large-format murals'],
+  Carousel:  ['Mood board: Coastal Serene palette — 5 slides','Before & after: a foyer transformation','Pattern pairing guide: 3 ways to layer texture'],
+  Story:     ['Today\'s showroom pick: burnished gold foil','Quick poll — matte or shimmer?','Peek inside our latest editorial shoot'],
+  Sale:      ['30% off select natural-fiber rolls — ends Sunday','Flash: sample sets $12 through midnight','Clearance spotlight: runway-ready remnants'],
+  Editorial: ['The case for bold pattern in the primary bedroom','Quiet luxury — why tone-on-tone never fails','How to choose the right wallcovering weight for high-traffic spaces'],
+};
+
+const DAYS_OF_WEEK = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
+const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
+
+/* ─── state ─── */
+const now = new Date();
+let curYear = now.getFullYear();
+let curMonth = now.getMonth();
+let selectedKey = null;
+let data = {}; // {dateKey: [{id,type,caption}]}
+
+/* ─── persistence ─── */
+function save(){ try{localStorage.setItem('dw-cal-v2', JSON.stringify(data))}catch(e){} }
+function load(){ try{const s=localStorage.getItem('dw-cal-v2');if(s)data=JSON.parse(s)}catch(e){} }
+load();
+
+function dateKey(y,m,d){ return `${y}-${String(m+1).padStart(2,'0')}-${String(d).padStart(2,'0')}` }
+function uid(){ return Math.random().toString(36).slice(2,8) }
+
+/* ─── suggestion ─── */
+function suggestCaption(type){
+  const pool = CAPTIONS[type];
+  return pool[Math.floor(Math.random()*pool.length)];
+}
+
+/* ─── render chip ─── */
+function makeChip(post, small=false){
+  const cfg = TYPES[post.type];
+  const el = document.createElement('span');
+  el.className = `chip ${cfg.cls}${small?' chip-sm':''}`;
+  el.setAttribute('data-tip', post.caption || post.type);
+  el.setAttribute('data-id', post.id);
+  el.setAttribute('tabindex','0');
+  el.setAttribute('role','button');
+  el.setAttribute('aria-label',`${post.type}: ${post.caption||'no caption'}`);
+  el.textContent = (small ? '' : cfg.icon+' ') + post.type;
+  return el;
+}
+
+/* ─── calendar render ─── */
+function renderCalendar(){
+  const head = document.getElementById('calHead');
+  const grid = document.getElementById('calGrid');
+  head.innerHTML = '';
+  grid.innerHTML = '';
+
+  DAYS_OF_WEEK.forEach(d=>{
+    const el = document.createElement('div');
+    el.className='cal-dow';
+    el.textContent=d;
+    head.appendChild(el);
+  });
+
+  const first = new Date(curYear, curMonth, 1).getDay();
+  const daysInMonth = new Date(curYear, curMonth+1, 0).getDate();
+  const total = first + daysInMonth;
+  const rows = Math.ceil(total/7)*7;
+
+  for(let i=0;i<rows;i++){
+    const day = i - first + 1;
+    const cell = document.createElement('div');
+    cell.className='cal-cell';
+
+    if(day<1||day>daysInMonth){
+      cell.classList.add('empty');
+      grid.appendChild(cell);
+      continue;
+    }
+
+    const key = dateKey(curYear, curMonth, day);
+    const isToday = day===now.getDate()&&curMonth===now.getMonth()&&curYear===now.getFullYear();
+    if(isToday) cell.classList.add('today');
+    if(key===selectedKey) cell.classList.add('selected');
+
+    cell.setAttribute('role','button');
+    cell.setAttribute('tabindex','0');
+    cell.setAttribute('aria-label',`${MONTHS[curMonth]} ${day}`);
+
+    const numEl = document.createElement('div');
+    numEl.className='cell-num';
+    numEl.textContent=day;
+    cell.appendChild(numEl);
+
+    const chipsEl = document.createElement('div');
+    chipsEl.className='cell-chips';
+    const posts = data[key]||[];
+    posts.forEach(p=>{
+      const chip = makeChip(p);
+      chip.addEventListener('click',e=>{
+        e.stopPropagation();
+        openPanel(key, day);
+        setTimeout(()=>{
+          const ta = document.querySelector(`[data-card="${p.id}"] textarea`);
+          if(ta) ta.focus();
+        },60);
+      });
+      chip.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();chip.click()}});
+      chipsEl.appendChild(chip);
+    });
+    cell.appendChild(chipsEl);
+
+    const hint = document.createElement('span');
+    hint.className='add-hint';
+    hint.textContent='+ add';
+    cell.appendChild(hint);
+
+    cell.addEventListener('click',()=>openPanel(key,day));
+    cell.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();openPanel(key,day)}});
+
+    grid.appendChild(cell);
+  }
+
+  updateStats();
+  renderWeekStrip();
+  updateMonthTitle();
+}
+
+function updateMonthTitle(){
+  document.getElementById('monthTitle').textContent = `${MONTHS[curMonth]} ${curYear}`;
+}
+
+/* ─── stats ─── */
+function updateStats(){
+  let total=0;
+  const counts={};
+  Object.keys(TYPES).forEach(t=>counts[t]=0);
+
+  // only count current month
+  for(let d=1;d<=31;d++){
+    const key=dateKey(curYear,curMonth,d);
+    if(data[key]){
+      data[key].forEach(p=>{total++;counts[p.type]=(counts[p.type]||0)+1});
+    }
+  }
+  document.getElementById('statTotal').textContent=total;
+
+  const ts = document.getElementById('typeStats');
+  ts.innerHTML='';
+  Object.entries(counts).forEach(([t,n])=>{
+    if(n===0)return;
+    const cfg=TYPES[t];
+    const sp=document.createElement('span');
+    sp.innerHTML=`<span class="chip chip-sm ${cfg.cls}">${cfg.icon} ${t} ${n}</span>`;
+    ts.appendChild(sp);
+  });
+}
+
+/* ─── week strip ─── */
+function renderWeekStrip(){
+  const strip = document.getElementById('weekStrip');
+  strip.innerHTML='';
+  // find the start of the current week (Sun)
+  const ref = new Date();
+  const dow = ref.getDay();
+  const weekStart = new Date(ref);
+  weekStart.setDate(ref.getDate()-dow);
+
+  for(let i=0;i<7;i++){
+    const d = new Date(weekStart);
+    d.setDate(weekStart.getDate()+i);
+    const key = dateKey(d.getFullYear(), d.getMonth(), d.getDate());
+    const posts = data[key]||[];
+
+    const wd = document.createElement('div');
+    wd.className='week-day';
+    wd.innerHTML=`<div class="wd-label">${DAYS_OF_WEEK[d.getDay()]}</div><div class="wd-date">${d.getDate()}</div>`;
+
+    const wChips = document.createElement('div');
+    wChips.className='wd-chips';
+    posts.forEach(p=>{
+      const c=makeChip(p,true);
+      wChips.appendChild(c);
+    });
+    if(posts.length===0){
+      const none=document.createElement('span');
+      none.style.cssText='font-size:10px;color:var(--hair)';
+      none.textContent='—';
+      wChips.appendChild(none);
+    }
+    wd.appendChild(wChips);
+    strip.appendChild(wd);
+  }
+}
+
+/* ─── panel ─── */
+function openPanel(key, day){
+  selectedKey=key;
+  const panel=document.getElementById('sidePanel');
+  const [y,m,d_]=key.split('-').map(Number);
+  const date=new Date(y,m-1,d_);
+  document.getElementById('panelDate').textContent=`${MONTHS[m-1]} ${d_}, ${y}`;
+  document.getElementById('panelDow').textContent=DAYS_OF_WEEK[date.getDay()];
+  renderPanel();
+  panel.classList.add('open');
+  // mark selected cell
+  document.querySelectorAll('.cal-cell').forEach(c=>c.classList.remove('selected'));
+  document.querySelectorAll('.cal-cell').forEach(c=>{
+    if(c.querySelector('.cell-num')?.textContent==String(d_)&&!c.classList.contains('empty')){
+      // match by day number — refine by data-key
+    }
+  });
+  // re-render to pick up selected class
+  renderCalendar();
+  selectedKey=key; // preserve after re-render
+}
+
+function closePanel(){
+  document.getElementById('sidePanel').classList.remove('open');
+  selectedKey=null;
+  document.querySelectorAll('.cal-cell.selected').forEach(c=>c.classList.remove('selected'));
+}
+
+function renderPanel(){
+  const body=document.getElementById('panelBody');
+  body.innerHTML='';
+  if(!selectedKey){return}
+  const posts=data[selectedKey]||[];
+
+  if(posts.length===0){
+    const es=document.createElement('div');
+    es.className='empty-state';
+    es.innerHTML='<span>📋</span>No posts planned yet.<br>Add a post type below.';
+    body.appendChild(es);
+    return;
+  }
+
+  posts.forEach((post,idx)=>{
+    const cfg=TYPES[post.type];
+    const card=document.createElement('div');
+    card.className='post-card';
+    card.setAttribute('data-card',post.id);
+
+    const cardHead=document.createElement('div');
+    cardHead.className='post-card-head';
+    cardHead.style.background=getComputedStyle(document.documentElement).getPropertyValue('--bg2');
+
+    const typeLabel=document.createElement('span');
+    typeLabel.className='post-card-type';
+    typeLabel.textContent=`${cfg.icon} ${post.type}`;
+
+    const delBtn=document.createElement('button');
+    delBtn.className='post-card-del';
+    delBtn.setAttribute('aria-label',`Delete ${post.type} post`);
+    delBtn.innerHTML='&times;';
+    delBtn.addEventListener('click',()=>{
+      const arr=data[selectedKey]||[];
+      data[selectedKey]=arr.filter(p=>p.id!==post.id);
+      save();renderPanel();renderCalendar();selectedKey=selectedKey;
+    });
+
+    cardHead.appendChild(typeLabel);
+    cardHead.appendChild(delBtn);
+
+    const cardBody=document.createElement('div');
+    cardBody.className='post-card-body';
+
+    const label=document.createElement('label');
+    label.style.cssText='font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink3);display:block;margin-bottom:4px';
+    label.textContent='Caption idea';
+
+    const ta=document.createElement('textarea');
+    ta.className='caption-area';
+    ta.setAttribute('aria-label','Caption');
+    ta.value=post.caption||'';
+    ta.placeholder='Write your caption idea here…';
+    ta.addEventListener('input',()=>{
+      post.caption=ta.value;
+      save();
+      // update chip tooltip
+      const chip=document.querySelector(`[data-id="${post.id}"]`);
+      if(chip) chip.setAttribute('data-tip',post.caption||post.type);
+    });
+
+    const hashEl=document.createElement('div');
+    hashEl.className='hashtags';
+    hashEl.textContent=cfg.hashtags;
+
+    cardBody.appendChild(label);
+    cardBody.appendChild(ta);
+    cardBody.appendChild(hashEl);
+
+    card.appendChild(cardHead);
+    card.appendChild(cardBody);
+    body.appendChild(card);
+  });
+}
+
+/* ─── add post ─── */
+document.querySelectorAll('.add-post-btn').forEach(btn=>{
+  btn.addEventListener('click',()=>{
+    if(!selectedKey)return;
+    if(!data[selectedKey]) data[selectedKey]=[];
+    const type=btn.getAttribute('data-type');
+    const post={id:uid(),type,caption:suggestCaption(type)};
+    data[selectedKey].push(post);
+    save();
+    renderPanel();
+    renderCalendar();
+    // restore panel open state
+    document.getElementById('sidePanel').classList.add('open');
+    selectedKey=selectedKey;
+  });
+});
+
+/* ─── nav ─── */
+document.getElementById('prevBtn').addEventListener('click',()=>{
+  if(curMonth===0){curMonth=11;curYear--;}else{curMonth--;}
+  renderCalendar();
+});
+document.getElementById('nextBtn').addEventListener('click',()=>{
+  if(curMonth===11){curMonth=0;curYear++;}else{curMonth++;}
+  renderCalendar();
+});
+document.getElementById('todayBtn').addEventListener('click',()=>{
+  curYear=now.getFullYear();curMonth=now.getMonth();
+  renderCalendar();
+});
+document.getElementById('panelClose').addEventListener('click',closePanel);
+
+/* ─── keyboard ─── */
+document.addEventListener('keydown',e=>{
+  if(e.key==='Escape') closePanel();
+  if(e.key==='ArrowLeft'&&(e.metaKey||e.ctrlKey)) document.getElementById('prevBtn').click();
+  if(e.key==='ArrowRight'&&(e.metaKey||e.ctrlKey)) document.getElementById('nextBtn').click();
+});
+
+/* ─── init ─── */
+renderCalendar();
+
+})();
+</script>
+</body>
+</html>
diff --git a/public/games/dw-damask-kaleidoscope.html b/public/games/dw-damask-kaleidoscope.html
new file mode 100644
index 0000000..620e836
--- /dev/null
+++ b/public/games/dw-damask-kaleidoscope.html
@@ -0,0 +1,502 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Damask Kaleidoscope — Designer Wallcoverings</title>
+<style>
+:root {
+  --bg: #0d0b09;
+  --ink: #1c1a17;
+  --accent: #c9a961;
+  --ivory: #f2ead8;
+  --serif: 'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif;
+  --sans: 'Optima','Gill Sans','Trebuchet MS',sans-serif;
+  --ease: cubic-bezier(.45,.05,.55,.95);
+}
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+html, body { width: 100%; height: 100%; overflow: hidden; background: var(--bg); }
+canvas { display: block; position: fixed; inset: 0; width: 100%; height: 100%; cursor: crosshair; }
+header {
+  position: fixed; top: 0; left: 0; z-index: 10;
+  padding: 18px 22px 14px;
+  pointer-events: none;
+}
+.wordmark {
+  font-family: var(--serif);
+  font-size: 13px;
+  font-weight: 600;
+  letter-spacing: .18em;
+  text-transform: uppercase;
+  color: var(--ivory);
+  opacity: .92;
+  line-height: 1;
+  display: block;
+  margin-bottom: 5px;
+}
+.palette-name {
+  font-family: var(--sans);
+  font-size: 10px;
+  letter-spacing: .12em;
+  text-transform: uppercase;
+  color: var(--accent);
+  opacity: .8;
+  display: block;
+}
+.hint {
+  font-family: var(--sans);
+  font-size: 9.5px;
+  letter-spacing: .08em;
+  color: var(--ivory);
+  opacity: .38;
+  display: block;
+  margin-top: 6px;
+  line-height: 1.5;
+}
+#controls {
+  position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
+  z-index: 10;
+  display: flex; align-items: center; gap: 18px;
+  background: rgba(13,11,9,.72);
+  border: 1px solid rgba(201,169,97,.22);
+  border-radius: 40px;
+  padding: 9px 20px;
+  pointer-events: auto;
+}
+.ctrl-label {
+  font-family: var(--sans);
+  font-size: 9px;
+  letter-spacing: .1em;
+  text-transform: uppercase;
+  color: var(--ivory);
+  opacity: .5;
+}
+#scale-slider {
+  -webkit-appearance: none;
+  appearance: none;
+  width: 90px; height: 2px;
+  background: rgba(201,169,97,.35);
+  border-radius: 2px;
+  outline: none;
+  cursor: pointer;
+}
+#scale-slider::-webkit-slider-thumb {
+  -webkit-appearance: none;
+  width: 12px; height: 12px;
+  border-radius: 50%;
+  background: var(--accent);
+  cursor: pointer;
+}
+#dl-btn {
+  font-family: var(--sans);
+  font-size: 9px;
+  letter-spacing: .12em;
+  text-transform: uppercase;
+  color: var(--accent);
+  background: none;
+  border: 1px solid rgba(201,169,97,.4);
+  border-radius: 20px;
+  padding: 5px 12px;
+  cursor: pointer;
+  transition: border-color .2s, opacity .2s;
+  pointer-events: auto;
+}
+#dl-btn:hover { border-color: var(--accent); opacity: 1; }
+</style>
+</head>
+<body>
+<canvas id="c"></canvas>
+<header>
+  <span class="wordmark">Designer Wallcoverings</span>
+  <span class="palette-name" id="pal-name">Brass &amp; Ink</span>
+  <span class="hint">click canvas · shift palette<br>move · warp symmetry</span>
+</header>
+<div id="controls">
+  <span class="ctrl-label">Scale</span>
+  <input id="scale-slider" type="range" min="0.4" max="2.2" step="0.01" value="1">
+  <button id="dl-btn">Download PNG</button>
+</div>
+<script>
+(function(){
+'use strict';
+
+// ── Palettes ──────────────────────────────────────────────────────────────────
+const PALETTES = [
+  { name: 'Brass & Ink',
+    bg: '#0d0b09', motif: ['#c9a961','#a07c3a','#e8d49a','#7a5c20','#f0e6c0'],
+    glow: 'rgba(201,169,97,0.18)', center: '#c9a961' },
+  { name: 'Celadon & Cream',
+    bg: '#0e1210', motif: ['#8fbfb0','#c2d9d0','#4a8c7a','#d8ede8','#2d6655'],
+    glow: 'rgba(143,191,176,0.16)', center: '#8fbfb0' },
+  { name: 'Aubergine & Gold',
+    bg: '#0e0b12', motif: ['#7c4d8a','#c9a961','#b07dbb','#e8d49a','#4a2060'],
+    glow: 'rgba(124,77,138,0.2)', center: '#c9a961' },
+  { name: 'Alabaster',
+    bg: '#12100e', motif: ['#d8cfbb','#b0a48a','#ede8da','#8a7d65','#f5f0e6'],
+    glow: 'rgba(216,207,187,0.12)', center: '#ede8da' },
+  { name: 'Moody Emerald',
+    bg: '#080f0b', motif: ['#2d7a50','#6ab88a','#1a4d30','#9dd4b0','#0d2b1a'],
+    glow: 'rgba(45,122,80,0.2)', center: '#6ab88a' },
+];
+
+let palIdx = 0;
+let pal = PALETTES[0];
+
+// ── Canvas setup ──────────────────────────────────────────────────────────────
+const canvas = document.getElementById('c');
+const ctx = canvas.getContext('2d');
+let W, H, CX, CY, offscreen, octx;
+
+function resize(){
+  W = canvas.width  = window.innerWidth;
+  H = canvas.height = window.innerHeight;
+  CX = W/2; CY = H/2;
+  offscreen = document.createElement('canvas');
+  offscreen.width  = Math.ceil(Math.max(W,H) * 0.6);
+  offscreen.height = offscreen.width;
+  octx = offscreen.getContext('2d');
+}
+resize();
+window.addEventListener('resize', resize);
+
+// ── State ─────────────────────────────────────────────────────────────────────
+let t = 0;
+let mx = 0, my = 0;   // mouse offset from center, normalised -1..1
+let scale = 1;
+const FOLDS = 8;       // 8-fold symmetry
+
+// ── Damask Bezier Forms ───────────────────────────────────────────────────────
+// Each "stamp" is a function(ctx, t, variant) → draws one canonical motif unit
+// All drawn in a local coord system; the kaleidoscope engine will mirror/rotate.
+
+function leaf(ctx, sx, sy, len, width, rot, col, alpha){
+  ctx.save();
+  ctx.globalAlpha = alpha;
+  ctx.strokeStyle = col;
+  ctx.fillStyle = col;
+  ctx.lineWidth = 1.2;
+  ctx.translate(sx, sy);
+  ctx.rotate(rot);
+  ctx.beginPath();
+  // Classic pointed leaf form via cubic bezier
+  ctx.moveTo(0, 0);
+  ctx.bezierCurveTo( width, -len*0.35,  width*0.8, -len*0.65,  0, -len);
+  ctx.bezierCurveTo(-width*0.8, -len*0.65, -width, -len*0.35,  0, 0);
+  ctx.closePath();
+  ctx.fill();
+  // Midrib
+  ctx.globalAlpha = alpha * 0.5;
+  ctx.strokeStyle = 'rgba(0,0,0,0.4)';
+  ctx.lineWidth = 0.7;
+  ctx.beginPath();
+  ctx.moveTo(0,0);
+  ctx.lineTo(0,-len);
+  ctx.stroke();
+  ctx.restore();
+}
+
+function scroll(ctx, x, y, r, turns, col, alpha){
+  ctx.save();
+  ctx.globalAlpha = alpha;
+  ctx.strokeStyle = col;
+  ctx.lineWidth = 1.5;
+  ctx.lineCap = 'round';
+  ctx.translate(x,y);
+  ctx.beginPath();
+  // Tight inward scroll (acanthus-like)
+  const steps = 36;
+  for(let i=0; i<=steps; i++){
+    const a = (i/steps)*Math.PI*2*turns;
+    const rr = r * (1 - i/steps * 0.72);
+    const px = Math.cos(a)*rr;
+    const py = Math.sin(a)*rr;
+    if(i===0) ctx.moveTo(px,py); else ctx.lineTo(px,py);
+  }
+  ctx.stroke();
+  ctx.restore();
+}
+
+function urn(ctx, x, y, w, h, col, alpha){
+  ctx.save();
+  ctx.globalAlpha = alpha;
+  ctx.strokeStyle = col;
+  ctx.lineWidth = 1.4;
+  ctx.translate(x,y);
+  ctx.beginPath();
+  // Urn silhouette: rim → wide belly → pedestal
+  const hw = w*0.5;
+  // left side
+  ctx.moveTo(-hw*0.6, -h*0.5);           // rim left
+  ctx.bezierCurveTo(-hw*0.9,-h*0.3, -hw,-h*0.1, -hw*0.85, h*0.1);   // shoulder
+  ctx.bezierCurveTo(-hw*0.6, h*0.3, -hw*0.3, h*0.38, -hw*0.22, h*0.45); // waist to base
+  ctx.lineTo(-hw*0.35, h*0.5);           // pedestal left
+  ctx.lineTo( hw*0.35, h*0.5);           // pedestal right
+  ctx.lineTo( hw*0.22, h*0.45);
+  ctx.bezierCurveTo( hw*0.3,h*0.38,  hw*0.6,h*0.3,  hw*0.85,h*0.1);
+  ctx.bezierCurveTo( hw, -h*0.1, hw*0.9,-h*0.3, hw*0.6,-h*0.5);
+  ctx.lineTo(-hw*0.6, -h*0.5);
+  ctx.closePath();
+  ctx.stroke();
+  // lip
+  ctx.beginPath();
+  ctx.moveTo(-hw*0.72,-h*0.5);
+  ctx.lineTo( hw*0.72,-h*0.5);
+  ctx.stroke();
+  ctx.restore();
+}
+
+function petal(ctx, x, y, r, n, rot, col, alpha){
+  ctx.save();
+  ctx.globalAlpha = alpha;
+  ctx.strokeStyle = col;
+  ctx.fillStyle = col+'44';
+  ctx.lineWidth = 1;
+  ctx.translate(x,y);
+  ctx.rotate(rot);
+  ctx.beginPath();
+  for(let i=0;i<n;i++){
+    const a = (i/n)*Math.PI*2;
+    const na = ((i+1)/n)*Math.PI*2;
+    const ma = (a+na)/2;
+    ctx.moveTo(0,0);
+    ctx.bezierCurveTo(
+      Math.cos(a)*r*0.6, Math.sin(a)*r*0.6,
+      Math.cos(ma)*r*1.05, Math.sin(ma)*r*1.05,
+      Math.cos(na)*r*0.6, Math.sin(na)*r*0.6
+    );
+    ctx.lineTo(0,0);
+  }
+  ctx.fill();
+  ctx.stroke();
+  ctx.restore();
+}
+
+function trefoil(ctx, x, y, r, rot, col, alpha){
+  ctx.save();
+  ctx.globalAlpha = alpha;
+  ctx.strokeStyle = col;
+  ctx.lineWidth = 1.2;
+  ctx.translate(x,y);
+  ctx.rotate(rot);
+  for(let i=0;i<3;i++){
+    const a = (i/3)*Math.PI*2;
+    ctx.beginPath();
+    ctx.arc(Math.cos(a)*r*0.55, Math.sin(a)*r*0.55, r*0.45, 0, Math.PI*2);
+    ctx.stroke();
+  }
+  ctx.restore();
+}
+
+function diaper(ctx, x, y, size, col, alpha){
+  // Diaper lattice diamond
+  ctx.save();
+  ctx.globalAlpha = alpha;
+  ctx.strokeStyle = col;
+  ctx.lineWidth = 0.8;
+  ctx.translate(x,y);
+  ctx.rotate(Math.PI/4);
+  ctx.strokeRect(-size/2,-size/2,size,size);
+  ctx.restore();
+}
+
+// ── Draw one "wedge" tile (canonical 1/FOLDS slice) ─────────────────────────
+function drawWedge(oc, R, t, mouseX, mouseY, sc){
+  oc.clearRect(0,0,offscreen.width,offscreen.height);
+  const s = sc;
+  const p = pal;
+  const base = R * s;
+  const warp = 1 + mouseX * 0.22;
+  const warpY = 1 + mouseY * 0.15;
+
+  // Animate phase offsets
+  const slow = t * 0.00018;
+  const med  = t * 0.00032;
+  const fast = t * 0.00055;
+
+  const c0 = p.motif[0], c1 = p.motif[1], c2 = p.motif[2],
+        c3 = p.motif[3], c4 = p.motif[4];
+
+  // Central urn — anchored at centre
+  urn(oc, 0,0, base*0.18*warp, base*0.28*warpY, c0, 0.85 + 0.12*Math.sin(slow));
+
+  // Ring of trefoils
+  for(let i=0;i<FOLDS;i++){
+    const a = (i/FOLDS)*Math.PI*2 + slow;
+    const rr = base * (0.28 + 0.04*Math.sin(med+i));
+    trefoil(oc, Math.cos(a)*rr, Math.sin(a)*rr, base*0.045, fast+i, c2,
+      0.55 + 0.3*Math.sin(fast+i*1.3));
+  }
+
+  // Scrollwork arms — 8 arms radiating
+  for(let i=0;i<FOLDS;i++){
+    const a = (i/FOLDS)*Math.PI*2 + med*0.5;
+    const rr = base * (0.18 + 0.05*Math.sin(slow+i));
+    scroll(oc, Math.cos(a)*rr, Math.sin(a)*rr,
+      base*(0.055+0.02*Math.sin(fast+i)), 1.4 + 0.4*Math.sin(slow+i),
+      i%2===0?c0:c1, 0.7+0.2*Math.sin(fast+i));
+  }
+
+  // Leaf pairs on diagonal axes
+  for(let i=0;i<FOLDS;i++){
+    const a = (i/FOLDS)*Math.PI*2 + slow*0.7;
+    const rr = base * (0.38 + 0.06*Math.sin(med+i*0.8));
+    const leafLen = base*(0.13+0.04*Math.sin(fast+i));
+    leaf(oc, Math.cos(a)*rr, Math.sin(a)*rr, leafLen, leafLen*0.32,
+      a + Math.PI*0.5 + 0.2*Math.sin(slow+i), c1,
+      0.65+0.25*Math.sin(med+i*1.7));
+    // smaller counter-leaf
+    leaf(oc, Math.cos(a)*rr*0.72, Math.sin(a)*rr*0.72, leafLen*0.6, leafLen*0.22,
+      a - Math.PI*0.5 + 0.15*Math.sin(fast+i), c4,
+      0.45+0.2*Math.sin(slow+i*2.1));
+  }
+
+  // Outer petal rosettes
+  for(let i=0;i<FOLDS;i++){
+    const a = (i/FOLDS)*Math.PI*2 + Math.PI/FOLDS + fast*0.4;
+    const rr = base * (0.55 + 0.08*Math.sin(slow+i));
+    petal(oc, Math.cos(a)*rr, Math.sin(a)*rr,
+      base*(0.06+0.02*Math.sin(med+i)), 6, med+i, c3,
+      0.6+0.3*Math.sin(fast+i*0.9));
+  }
+
+  // Diaper lattice — subtle outer ring
+  for(let i=0;i<FOLDS*2;i++){
+    const a = (i/(FOLDS*2))*Math.PI*2 + slow*0.3;
+    const rr = base*(0.7+0.05*Math.sin(med+i));
+    diaper(oc, Math.cos(a)*rr, Math.sin(a)*rr,
+      base*(0.04+0.012*Math.sin(fast+i)), c0,
+      0.28+0.15*Math.sin(slow+i*1.4));
+  }
+
+  // Secondary scrolls mid-ring
+  for(let i=0;i<FOLDS;i++){
+    const a = (i/FOLDS)*Math.PI*2 + Math.PI/FOLDS + med;
+    const rr = base*(0.45+0.07*Math.sin(slow+i*1.2));
+    scroll(oc, Math.cos(a)*rr, Math.sin(a)*rr,
+      base*(0.04+0.015*Math.sin(fast+i)), 1.1+0.3*Math.sin(med+i),
+      i%2===0?c2:c3, 0.5+0.25*Math.sin(slow+i*2));
+  }
+
+  // Centre rosette
+  petal(oc, 0, 0, base*0.09, 12, fast, c0, 0.9+0.08*Math.sin(slow));
+  petal(oc, 0, 0, base*0.055, 8, -fast*0.7, c4, 0.7+0.2*Math.sin(med));
+}
+
+// ── Render kaleidoscope ───────────────────────────────────────────────────────
+function render(){
+  const R = Math.min(W,H)*0.5;
+  const sz = offscreen.width;
+  const hs = sz/2;
+
+  // Draw wedge tile centred at (hs,hs)
+  octx.save();
+  octx.translate(hs,hs);
+  drawWedge(octx, R, t, mx, my, scale);
+  octx.restore();
+
+  // Main canvas
+  ctx.fillStyle = pal.bg;
+  ctx.fillRect(0,0,W,H);
+
+  // Fold & stamp FOLDS times + mirror
+  ctx.save();
+  ctx.translate(CX,CY);
+
+  const wedgeAngle = (Math.PI*2)/FOLDS;
+  const slow = t * 0.00012;
+
+  for(let f=0;f<FOLDS;f++){
+    const baseA = f*wedgeAngle + slow;
+
+    // Original
+    ctx.save();
+    ctx.rotate(baseA);
+    ctx.drawImage(offscreen, -hs, -hs);
+    ctx.restore();
+
+    // Mirror (flip X)
+    ctx.save();
+    ctx.rotate(baseA);
+    ctx.scale(-1,1);
+    ctx.drawImage(offscreen, -hs, -hs);
+    ctx.restore();
+  }
+  ctx.restore();
+
+  // Gold glow at centre
+  const grd = ctx.createRadialGradient(CX,CY,0, CX,CY,R*0.22);
+  grd.addColorStop(0, pal.glow.replace('0.18','0.45').replace('0.20','0.45').replace('0.16','0.45').replace('0.12','0.45').replace('0.2)','0.45)'));
+  grd.addColorStop(0.4, pal.glow);
+  grd.addColorStop(1,'transparent');
+  ctx.fillStyle = grd;
+  ctx.fillRect(0,0,W,H);
+
+  // Vignette
+  const vig = ctx.createRadialGradient(CX,CY,R*0.45, CX,CY,R*1.1);
+  vig.addColorStop(0,'transparent');
+  vig.addColorStop(1,'rgba(0,0,0,0.78)');
+  ctx.fillStyle = vig;
+  ctx.fillRect(0,0,W,H);
+
+  // Hairline circle frame
+  ctx.save();
+  ctx.strokeStyle = pal.motif[0]+'55';
+  ctx.lineWidth = 0.7;
+  ctx.beginPath();
+  ctx.arc(CX,CY,R*0.92,0,Math.PI*2);
+  ctx.stroke();
+  ctx.beginPath();
+  ctx.arc(CX,CY,R*0.78,0,Math.PI*2);
+  ctx.stroke();
+  ctx.restore();
+}
+
+// ── Loop ──────────────────────────────────────────────────────────────────────
+function loop(ts){
+  t = ts;
+  render();
+  requestAnimationFrame(loop);
+}
+requestAnimationFrame(loop);
+
+// ── Input ─────────────────────────────────────────────────────────────────────
+function updateMouse(x,y){
+  mx = (x-CX)/(W*0.5);
+  my = (y-CY)/(H*0.5);
+}
+canvas.addEventListener('mousemove', e=>updateMouse(e.clientX,e.clientY));
+canvas.addEventListener('touchmove', e=>{
+  e.preventDefault();
+  updateMouse(e.touches[0].clientX, e.touches[0].clientY);
+},{passive:false});
+
+canvas.addEventListener('click', ()=>{
+  palIdx = (palIdx+1)%PALETTES.length;
+  pal = PALETTES[palIdx];
+  document.getElementById('pal-name').textContent = pal.name;
+  document.body.style.background = pal.bg;
+});
+canvas.addEventListener('touchend', ()=>{
+  palIdx = (palIdx+1)%PALETTES.length;
+  pal = PALETTES[palIdx];
+  document.getElementById('pal-name').textContent = pal.name;
+  document.body.style.background = pal.bg;
+});
+
+// ── Controls ──────────────────────────────────────────────────────────────────
+document.getElementById('scale-slider').addEventListener('input', e=>{
+  scale = parseFloat(e.target.value);
+});
+
+document.getElementById('dl-btn').addEventListener('click', ()=>{
+  // Render one frame to full-res temp canvas
+  const link = document.createElement('a');
+  link.download = 'dw-damask-kaleidoscope.png';
+  link.href = canvas.toDataURL('image/png');
+  link.click();
+});
+
+})();
+</script>
+</body>
+</html>

← ef2e59b Agent-driven editorial: DW The Lookbook (frontend-developer)  ·  back to Model Arena  ·  Agent-driven social tool: DW Content Calendar (frontend-deve b20881d →