← back to Model Arena
Agent-driven social tool: DW Reel Storyboard Builder (frontend-developer) — 9:16 preview + storyboard editor. Registered.
dac7e64b3f624fefc23500a2e763c98c960b6225 · 2026-07-25 11:37:54 -0700 · Steve Abrams
Files touched
M data/arcade-games.jsonA public/games/dw-design-iq.htmlA public/games/dw-reel-storyboard.html
Diff
commit dac7e64b3f624fefc23500a2e763c98c960b6225
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Jul 25 11:37:54 2026 -0700
Agent-driven social tool: DW Reel Storyboard Builder (frontend-developer) — 9:16 preview + storyboard editor. Registered.
---
data/arcade-games.json | 6 +
public/games/dw-design-iq.html | 791 +++++++++++++++++++++++++++++
public/games/dw-reel-storyboard.html | 948 +++++++++++++++++++++++++++++++++++
3 files changed, 1745 insertions(+)
diff --git a/data/arcade-games.json b/data/arcade-games.json
index 992b495..bd5866c 100644
--- a/data/arcade-games.json
+++ b/data/arcade-games.json
@@ -47,6 +47,12 @@
"title": "DW Shop by Color",
"credit": "frontend-developer",
"note": "Agent-built \u2014 interactive color-wheel discovery, hue\u2192swatch families, warm/cool + value controls"
+ },
+ {
+ "slug": "dw-reel-storyboard",
+ "title": "DW Reel Storyboard",
+ "credit": "frontend-developer",
+ "note": "Agent-built social tool \u2014 9:16 Reel preview + drag-reorder storyboard editor, scene presets, caption/hashtag export"
}
]
}
\ No newline at end of file
diff --git a/public/games/dw-design-iq.html b/public/games/dw-design-iq.html
new file mode 100644
index 0000000..67adf2c
--- /dev/null
+++ b/public/games/dw-design-iq.html
@@ -0,0 +1,791 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Design IQ — Designer Wallcoverings</title>
+<style>
+:root{
+ --bg:#f4f1ea;--bg2:#ede9df;--ink:#1c1a17;--ink2:#4a4540;
+ --accent:#c9a961;--accent2:#a8873e;--correct:#3a7d5c;--wrong:#8b3a3a;
+ --surface:#ffffff;--rim:rgba(201,169,97,.25);
+ --serif:'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif;
+ --sans:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,sans-serif;
+ --ease:cubic-bezier(.4,0,.2,1);--r:12px;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+html,body{height:100%;background:var(--bg);color:var(--ink);font-family:var(--sans);overflow-x:hidden}
+
+/* HEADER */
+header{
+ display:flex;align-items:center;gap:16px;
+ padding:18px 28px 14px;border-bottom:1px solid var(--rim);
+ background:var(--bg);position:sticky;top:0;z-index:50;
+}
+.wordmark{
+ font-family:var(--serif);font-size:clamp(20px,3.5vw,36px);
+ letter-spacing:.18em;text-transform:uppercase;color:var(--ink);
+ font-weight:400;line-height:1;
+}
+.wordmark span{color:var(--accent);font-size:.7em;letter-spacing:.35em;display:block;margin-top:2px}
+header .right{margin-left:auto;display:flex;align-items:center;gap:12px}
+#muteBtn{
+ background:none;border:1px solid var(--rim);border-radius:50%;
+ width:36px;height:36px;cursor:pointer;font-size:16px;
+ display:flex;align-items:center;justify-content:center;transition:background .2s;
+}
+#muteBtn:hover{background:var(--bg2)}
+#scoreBadge{
+ font-family:var(--serif);font-size:22px;color:var(--accent);font-weight:600;
+ min-width:60px;text-align:right;display:none;
+}
+
+/* SCREENS */
+.screen{
+ display:none;min-height:calc(100vh - 70px);
+ align-items:center;justify-content:center;flex-direction:column;
+ padding:32px 20px;
+}
+.screen.active{display:flex}
+
+/* START SCREEN */
+#startScreen{gap:24px;text-align:center}
+.hero-title{
+ font-family:var(--serif);font-size:clamp(42px,7vw,80px);
+ color:var(--ink);letter-spacing:.06em;line-height:1.05;font-weight:400;
+}
+.hero-sub{color:var(--ink2);font-size:clamp(14px,2vw,17px);max-width:520px;line-height:1.7;margin-top:8px}
+.btn{
+ font-family:var(--serif);font-size:18px;letter-spacing:.1em;
+ background:var(--accent);color:var(--ink);border:none;
+ padding:14px 44px;border-radius:40px;cursor:pointer;
+ transition:transform .2s var(--ease),background .2s;font-weight:600;
+ text-transform:uppercase;
+}
+.btn:hover{background:var(--accent2);transform:translateY(-2px)}
+.btn:active{transform:translateY(0)}
+.best-badge{
+ font-size:13px;color:var(--ink2);
+ background:var(--bg2);border:1px solid var(--rim);
+ padding:6px 18px;border-radius:20px;display:none;
+}
+.ornament{font-size:28px;color:var(--accent);opacity:.6;letter-spacing:.3em}
+
+/* GAME SCREEN */
+#gameScreen{gap:0;justify-content:flex-start;padding:0}
+.game-wrap{width:100%;max-width:760px;margin:0 auto;padding:24px 20px 48px}
+
+/* HUD */
+.hud{display:flex;align-items:center;gap:12px;margin-bottom:18px;flex-wrap:wrap}
+.hud-q{font-size:13px;color:var(--ink2);font-weight:600;letter-spacing:.08em;text-transform:uppercase}
+.hud-streak{
+ margin-left:auto;font-family:var(--serif);font-size:20px;color:var(--accent);
+ font-weight:700;min-width:80px;text-align:right;transition:all .3s;
+}
+.streak-flare{animation:flare .4s var(--ease)}
+@keyframes flare{0%{transform:scale(1)}50%{transform:scale(1.5);color:#ffdd88}100%{transform:scale(1)}}
+
+/* TIMER BAR */
+.timer-track{height:5px;background:var(--bg2);border-radius:3px;margin-bottom:24px;overflow:hidden}
+.timer-fill{height:100%;background:var(--accent);border-radius:3px;transition:width .1s linear;transform-origin:left}
+
+/* QUESTION CARD */
+.q-card{
+ background:var(--surface);border:1px solid var(--rim);border-radius:var(--r);
+ padding:32px;margin-bottom:20px;box-shadow:0 2px 20px rgba(0,0,0,.06);
+}
+.q-label{
+ font-size:11px;letter-spacing:.18em;text-transform:uppercase;
+ color:var(--accent2);font-weight:600;margin-bottom:14px;
+}
+.q-text{
+ font-family:var(--serif);font-size:clamp(20px,3vw,28px);
+ color:var(--ink);line-height:1.4;font-weight:400;
+}
+.q-svg{
+ margin:20px auto 0;display:block;border-radius:8px;
+ border:1px solid var(--rim);max-width:100%;
+}
+
+/* ANSWERS */
+.answers{display:grid;grid-template-columns:1fr 1fr;gap:12px}
+@media(max-width:520px){.answers{grid-template-columns:1fr}}
+.ans-btn{
+ background:var(--surface);border:1.5px solid var(--rim);
+ border-radius:var(--r);padding:16px 20px;cursor:pointer;
+ font-family:var(--serif);font-size:18px;color:var(--ink);
+ text-align:left;line-height:1.3;
+ transition:transform .18s var(--ease),border-color .18s,background .18s,box-shadow .18s;
+ position:relative;overflow:hidden;
+}
+.ans-btn:hover:not(:disabled){
+ transform:translateY(-3px);border-color:var(--accent);
+ box-shadow:0 6px 20px rgba(201,169,97,.18);
+}
+.ans-btn:disabled{cursor:not-allowed}
+.ans-btn.correct{
+ background:#e8f5ee;border-color:var(--correct);color:var(--correct);
+ animation:popIn .3s var(--ease);
+}
+.ans-btn.wrong{background:#f5e8e8;border-color:var(--wrong);color:var(--wrong)}
+.ans-btn.reveal{background:#e8f5ee;border-color:var(--correct);color:var(--correct)}
+@keyframes popIn{0%{transform:scale(.96)}60%{transform:scale(1.04)}100%{transform:scale(1)}}
+
+/* FEEDBACK */
+.feedback{
+ text-align:center;padding:16px;border-radius:var(--r);
+ font-family:var(--serif);font-size:20px;min-height:68px;
+ transition:all .3s var(--ease);opacity:0;
+}
+.feedback.show{opacity:1}
+.feedback.correct-fb{color:var(--correct)}
+.feedback.wrong-fb{color:var(--wrong)}
+.feedback .speed-bonus{font-size:14px;color:var(--accent2);display:block;margin-top:2px;font-family:var(--sans)}
+
+/* NEXT BTN */
+#nextBtn{
+ display:none;margin:12px auto 0;font-size:16px;padding:12px 36px;letter-spacing:.12em;
+}
+
+/* END SCREEN */
+#endScreen{gap:20px;text-align:center;max-width:600px;margin:0 auto}
+.grade-title{
+ font-family:var(--serif);font-size:clamp(36px,6vw,64px);
+ color:var(--accent);font-weight:400;letter-spacing:.06em;
+}
+.final-score{
+ font-family:var(--serif);font-size:clamp(52px,9vw,96px);
+ color:var(--ink);font-weight:600;line-height:1;
+}
+.final-label{font-size:14px;color:var(--ink2);letter-spacing:.1em;text-transform:uppercase}
+.stats-row{display:flex;gap:32px;justify-content:center;flex-wrap:wrap;margin:8px 0}
+.stat{text-align:center}
+.stat-n{font-family:var(--serif);font-size:28px;color:var(--ink);font-weight:600}
+.stat-l{font-size:11px;color:var(--ink2);letter-spacing:.1em;text-transform:uppercase;margin-top:2px}
+.new-best{
+ background:linear-gradient(135deg,var(--accent),var(--accent2));
+ color:var(--ink);font-size:13px;font-weight:700;letter-spacing:.12em;
+ text-transform:uppercase;padding:6px 20px;border-radius:20px;
+ display:inline-block;margin-bottom:4px;animation:pulse 1s ease infinite alternate;
+}
+@keyframes pulse{from{opacity:.8}to{opacity:1}}
+
+/* PROGRESS DOTS */
+.progress-dots{display:flex;gap:6px;justify-content:center;margin-bottom:8px}
+.dot{
+ width:8px;height:8px;border-radius:50%;background:var(--bg2);
+ transition:background .3s;
+}
+.dot.done-correct{background:var(--correct)}
+.dot.done-wrong{background:var(--wrong)}
+.dot.current{background:var(--accent)}
+
+/* OVERLAYS */
+.overlay{
+ position:fixed;inset:0;background:rgba(28,26,23,.7);
+ display:none;align-items:center;justify-content:center;z-index:100;
+ backdrop-filter:blur(4px);
+}
+.overlay.visible{display:flex}
+.overlay-box{
+ background:var(--surface);border-radius:var(--r);padding:40px 48px;
+ max-width:420px;width:90%;text-align:center;
+ box-shadow:0 24px 80px rgba(0,0,0,.25);
+}
+.overlay-box h2{font-family:var(--serif);font-size:36px;color:var(--ink);margin-bottom:12px}
+.overlay-box p{color:var(--ink2);line-height:1.6;margin-bottom:24px}
+</style>
+</head>
+<body>
+
+<header>
+ <div class="wordmark">Designer Wallcoverings<span>Design IQ</span></div>
+ <div class="right">
+ <span id="scoreBadge">0</span>
+ <button id="muteBtn" aria-label="Toggle sound">🔔</button>
+ </div>
+</header>
+
+<!-- START -->
+<div id="startScreen" class="screen active" role="main">
+ <div class="ornament">✦ ✦ ✦</div>
+ <h1 class="hero-title">Design IQ</h1>
+ <p class="hero-sub">Test your knowledge of interior design eras, wallcovering materials, and classic pattern motifs. 12 questions. Beat the clock. Prove your taste.</p>
+ <div class="best-badge" id="bestBadge">Best Score: <strong id="bestNum">0</strong></div>
+ <button class="btn" id="startBtn">Begin the Quiz</button>
+ <div class="ornament">✦</div>
+</div>
+
+<!-- GAME -->
+<div id="gameScreen" class="screen" role="main" aria-live="polite">
+ <div class="game-wrap">
+ <div class="hud">
+ <span class="hud-q" id="qCounter">Question 1 of 12</span>
+ <span class="hud-streak" id="streakDisplay"></span>
+ </div>
+ <div class="progress-dots" id="progressDots"></div>
+ <div class="timer-track"><div class="timer-fill" id="timerFill"></div></div>
+ <div class="q-card" id="qCard">
+ <div class="q-label" id="qLabel"></div>
+ <div class="q-text" id="qText"></div>
+ <div id="qSvg"></div>
+ </div>
+ <div class="answers" id="answers"></div>
+ <div class="feedback" id="feedback"></div>
+ <button class="btn" id="nextBtn">Next →</button>
+ </div>
+</div>
+
+<!-- END -->
+<div id="endScreen" class="screen" role="main">
+ <div class="ornament">✦ ✦ ✦</div>
+ <div id="newBestBadge" class="new-best" style="display:none">New Best!</div>
+ <div class="grade-title" id="gradeTitle">Connoisseur</div>
+ <div class="final-score" id="finalScore">0</div>
+ <div class="final-label">Design IQ Points</div>
+ <div class="stats-row">
+ <div class="stat"><div class="stat-n" id="statCorrect">0</div><div class="stat-l">Correct</div></div>
+ <div class="stat"><div class="stat-n" id="statStreak">0</div><div class="stat-l">Best Streak</div></div>
+ <div class="stat"><div class="stat-n" id="statTime">0s</div><div class="stat-l">Avg Speed</div></div>
+ </div>
+ <p id="gradeBlurb" style="color:var(--ink2);line-height:1.7;max-width:400px"></p>
+ <button class="btn" id="playAgainBtn">Play Again</button>
+</div>
+
+<div class="overlay" id="overlay" role="dialog" aria-modal="true">
+ <div class="overlay-box">
+ <h2 id="overlayTitle"></h2>
+ <p id="overlayMsg"></p>
+ <button class="btn" id="overlayBtn">OK</button>
+ </div>
+</div>
+
+<script>
+// ─── WEB AUDIO ───────────────────────────────────────────────────────────────
+let audioCtx=null,muted=false;
+function getCtx(){if(!audioCtx)audioCtx=new(window.AudioContext||window.webkitAudioContext)();return audioCtx}
+function playTone(freq,dur,type='sine',vol=.18,ramp=true){
+ if(muted)return;
+ try{
+ const c=getCtx(),g=c.createGain(),o=c.createOscillator();
+ o.type=type;o.frequency.value=freq;
+ g.gain.value=vol;
+ if(ramp)g.gain.exponentialRampToValueAtTime(.001,c.currentTime+dur);
+ o.connect(g);g.connect(c.destination);
+ o.start();o.stop(c.currentTime+dur);
+ }catch(e){}
+}
+function sndCorrect(){playTone(523,.12,'sine',.15);setTimeout(()=>playTone(659,.18,'sine',.12),100);setTimeout(()=>playTone(784,.25,'sine',.1),200)}
+function sndWrong(){playTone(220,.08,'sawtooth',.12);setTimeout(()=>playTone(196,.2,'sawtooth',.08),90)}
+function sndStreak(){playTone(880,.08,'sine',.12);setTimeout(()=>playTone(1046,.08,'sine',.1),80);setTimeout(()=>playTone(1318,.22,'sine',.1),160)}
+function sndWin(){[523,659,784,1046].forEach((f,i)=>setTimeout(()=>playTone(f,.3,'sine',.12),i*120))}
+
+document.getElementById('muteBtn').addEventListener('click',()=>{
+ muted=!muted;
+ document.getElementById('muteBtn').textContent=muted?'🔕':'🔔';
+});
+
+// ─── SVGs ────────────────────────────────────────────────────────────────────
+const svgs={
+ damask:`<svg width="180" height="180" xmlns="http://www.w3.org/2000/svg">
+ <rect width="180" height="180" fill="#f4f1ea"/>
+ <g fill="#c9a961" opacity=".85">
+ <ellipse cx="90" cy="90" rx="18" ry="32"/>
+ <ellipse cx="90" cy="90" rx="32" ry="18"/>
+ <ellipse cx="90" cy="90" rx="10" ry="20" fill="#f4f1ea"/>
+ <ellipse cx="90" cy="90" rx="20" ry="10" fill="#f4f1ea"/>
+ <circle cx="90" cy="90" r="6"/>
+ <path d="M90 58 C80 68 70 78 60 90 C70 102 80 112 90 122 C100 112 110 102 120 90 C110 78 100 68 90 58Z" fill="none" stroke="#c9a961" stroke-width="2"/>
+ <path d="M58 90 C68 80 78 70 90 60 C102 70 112 80 122 90 C112 100 102 110 90 120 C78 110 68 100 58 90Z" fill="none" stroke="#c9a961" stroke-width="1.5"/>
+ <circle cx="60" cy="60" r="4"/><circle cx="120" cy="60" r="4"/>
+ <circle cx="60" cy="120" r="4"/><circle cx="120" cy="120" r="4"/>
+ <ellipse cx="90" cy="48" rx="6" ry="10"/><ellipse cx="90" cy="132" rx="6" ry="10"/>
+ <ellipse cx="48" cy="90" rx="10" ry="6"/><ellipse cx="132" cy="90" rx="10" ry="6"/>
+ </g>
+ </svg>`,
+ greekkey:`<svg width="180" height="180" xmlns="http://www.w3.org/2000/svg">
+ <rect width="180" height="180" fill="#1c1a17"/>
+ <g fill="none" stroke="#c9a961" stroke-width="4" stroke-linecap="square">
+ <polyline points="10,10 10,40 40,40 40,20 20,20 20,60 60,60 60,10 10,10"/>
+ <polyline points="90,10 90,40 120,40 120,20 100,20 100,60 140,60 140,10 90,10"/>
+ <polyline points="10,90 10,120 40,120 40,100 20,100 20,140 60,140 60,90 10,90"/>
+ <polyline points="90,90 90,120 120,120 120,100 100,100 100,140 140,140 140,90 90,90"/>
+ <line x1="0" y1="75" x2="180" y2="75" stroke-width="2" opacity=".3"/>
+ <line x1="75" y1="0" x2="75" y2="180" stroke-width="2" opacity=".3"/>
+ </g>
+ </svg>`,
+ ikat:`<svg width="180" height="180" xmlns="http://www.w3.org/2000/svg">
+ <rect width="180" height="180" fill="#2a1f3a"/>
+ <defs>
+ <filter id="blur"><feGaussianBlur stdDeviation="1.4"/></filter>
+ </defs>
+ <g filter="url(#blur)">
+ <ellipse cx="45" cy="45" rx="14" ry="28" fill="#c9a961" transform="rotate(15 45 45)"/>
+ <ellipse cx="135" cy="45" rx="14" ry="28" fill="#e8d5a0" transform="rotate(-15 135 45)"/>
+ <ellipse cx="45" cy="135" rx="14" ry="28" fill="#e8d5a0" transform="rotate(-15 45 135)"/>
+ <ellipse cx="135" cy="135" rx="14" ry="28" fill="#c9a961" transform="rotate(15 135 135)"/>
+ <ellipse cx="90" cy="90" rx="20" ry="36" fill="#b87333" transform="rotate(5 90 90)"/>
+ <ellipse cx="90" cy="90" rx="10" ry="18" fill="#c9a961"/>
+ </g>
+ <g fill="none" stroke="#c9a961" stroke-width="1" opacity=".3">
+ <line x1="0" y1="0" x2="180" y2="180"/>
+ <line x1="0" y1="90" x2="180" y2="90"/>
+ <line x1="90" y1="0" x2="90" y2="180"/>
+ </g>
+ </svg>`,
+ toile:`<svg width="180" height="180" xmlns="http://www.w3.org/2000/svg">
+ <rect width="180" height="180" fill="#f0ede6"/>
+ <g fill="none" stroke="#4a6b8a" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">
+ <path d="M60 80 Q55 60 70 45 Q85 30 95 50 Q100 65 90 75 Q80 85 70 80Z"/>
+ <path d="M70 45 Q75 35 85 30 Q95 28 92 40"/>
+ <circle cx="80" cy="58" r="3" fill="#4a6b8a"/>
+ <path d="M60 80 Q50 95 55 110 Q60 125 70 120 Q80 115 75 100 Q72 90 65 85"/>
+ <path d="M75 100 L85 105 L90 95 L80 90"/>
+ <path d="M90 75 Q100 85 105 100 Q108 115 100 118 Q92 120 88 108 Q85 95 90 75"/>
+ <path d="M55 110 Q45 120 50 135 Q55 145 65 140 Q60 130 58 120"/>
+ <path d="M100 118 Q110 128 108 142 Q105 152 95 148 Q100 138 98 126"/>
+ <line x1="40" y1="155" x2="140" y2="155" stroke-width=".8"/>
+ <path d="M30 30 Q35 25 40 28 Q38 35 32 36Z" fill="#4a6b8a" opacity=".4"/>
+ <path d="M140 120 Q148 115 152 120 Q150 128 143 126Z" fill="#4a6b8a" opacity=".4"/>
+ <path d="M120 40 Q130 30 140 38 Q135 48 125 46Z" fill="#4a6b8a" opacity=".3"/>
+ </g>
+ </svg>`,
+ trellis:`<svg width="180" height="180" xmlns="http://www.w3.org/2000/svg">
+ <rect width="180" height="180" fill="#f4f1ea"/>
+ <g stroke="#c9a961" stroke-width="1.8" fill="none" opacity=".8">
+ <line x1="0" y1="0" x2="180" y2="180"/><line x1="90" y1="0" x2="180" y2="90"/>
+ <line x1="0" y1="90" x2="90" y2="180"/><line x1="0" y1="180" x2="180" y2="0"/>
+ <line x1="90" y1="180" x2="180" y2="90"/><line x1="0" y1="90" x2="90" y2="0"/>
+ </g>
+ <g stroke="#4a4540" stroke-width=".8" fill="none">
+ <rect x="22" y="22" width="46" height="46" rx="6"/>
+ <rect x="112" y="22" width="46" height="46" rx="6"/>
+ <rect x="22" y="112" width="46" height="46" rx="6"/>
+ <rect x="112" y="112" width="46" height="46" rx="6"/>
+ <rect x="67" y="67" width="46" height="46" rx="6"/>
+ </g>
+ <g fill="#c9a961">
+ <circle cx="45" cy="45" r="4"/><circle cx="135" cy="45" r="4"/>
+ <circle cx="45" cy="135" r="4"/><circle cx="135" cy="135" r="4"/>
+ <circle cx="90" cy="90" r="5"/><circle cx="90" cy="0" r="3"/>
+ <circle cx="90" cy="180" r="3"/><circle cx="0" cy="90" r="3"/>
+ <circle cx="180" cy="90" r="3"/>
+ </g>
+ </svg>`,
+ chinoiserie:`<svg width="180" height="180" xmlns="http://www.w3.org/2000/svg">
+ <rect width="180" height="180" fill="#1a3a2a"/>
+ <g fill="none" stroke="#c9d9c0" stroke-width="1" stroke-linecap="round">
+ <path d="M90 20 Q105 40 100 60 Q115 55 120 70 Q105 75 100 90 Q115 95 110 115 Q95 108 90 125"/>
+ <path d="M90 20 Q75 40 80 60 Q65 55 60 70 Q75 75 80 90 Q65 95 70 115 Q85 108 90 125"/>
+ <ellipse cx="90" cy="50" rx="8" ry="12" fill="#c9a961" stroke="#c9a961"/>
+ <ellipse cx="115" cy="72" rx="7" ry="10" fill="#d4b896" stroke="#d4b896"/>
+ <ellipse cx="65" cy="72" rx="7" ry="10" fill="#d4b896" stroke="#d4b896"/>
+ <ellipse cx="105" cy="105" rx="6" ry="9" fill="#8fbc8f" stroke="#8fbc8f"/>
+ <ellipse cx="75" cy="105" rx="6" ry="9" fill="#8fbc8f" stroke="#8fbc8f"/>
+ <path d="M70 140 Q90 130 110 140 Q115 155 90 160 Q65 155 70 140Z" fill="#c9a961" stroke="none"/>
+ <circle cx="90" cy="148" r="4" fill="#1a3a2a"/>
+ <path d="M50 30 Q55 20 62 26 Q58 36 50 30Z" fill="#8fbc8f"/>
+ <path d="M130 30 Q125 20 118 26 Q122 36 130 30Z" fill="#8fbc8f"/>
+ <path d="M30 90 Q20 82 26 74 Q36 78 30 90Z" fill="#c9d9c0"/>
+ <path d="M150 90 Q160 82 154 74 Q144 78 150 90Z" fill="#c9d9c0"/>
+ </g>
+ </svg>`,
+ artdeco:`<svg width="180" height="180" xmlns="http://www.w3.org/2000/svg">
+ <rect width="180" height="180" fill="#0d0b09"/>
+ <g fill="#c9a961">
+ <polygon points="90,10 100,50 140,50 108,74 120,114 90,90 60,114 72,74 40,50 80,50"/>
+ <circle cx="90" cy="90" r="28" fill="none" stroke="#c9a961" stroke-width="2"/>
+ <circle cx="90" cy="90" r="20" fill="none" stroke="#c9a961" stroke-width="1" opacity=".5"/>
+ </g>
+ <g stroke="#c9a961" stroke-width="1.5" fill="none" opacity=".6">
+ <line x1="90" y1="10" x2="90" y2="40"/>
+ <line x1="90" y1="140" x2="90" y2="170"/>
+ <line x1="10" y1="90" x2="40" y2="90"/>
+ <line x1="140" y1="90" x2="170" y2="90"/>
+ </g>
+ <g stroke="#c9a961" stroke-width=".8" fill="none" opacity=".35">
+ <rect x="10" y="10" width="160" height="160"/>
+ <rect x="20" y="20" width="140" height="140"/>
+ </g>
+ </svg>`,
+ grasscloth:`<svg width="180" height="180" xmlns="http://www.w3.org/2000/svg">
+ <rect width="180" height="180" fill="#c4b89a"/>
+ <g stroke="#8a7a5a" stroke-width=".9" opacity=".6">
+ ${Array.from({length:36},(_,i)=>`<line x1="${i*5}" y1="0" x2="${i*5+2}" y2="180"/>`).join('')}
+ </g>
+ <g stroke="#6b5d3f" stroke-width=".5" opacity=".4">
+ ${Array.from({length:18},(_,i)=>`<line x1="0" y1="${i*10}" x2="180" y2="${i*10+3}"/>`).join('')}
+ </g>
+ <g stroke="#a08060" stroke-width="1.2" opacity=".3">
+ <line x1="30" y1="0" x2="30" y2="180"/>
+ <line x1="60" y1="0" x2="60" y2="180"/>
+ <line x1="90" y1="0" x2="90" y2="180"/>
+ <line x1="120" y1="0" x2="120" y2="180"/>
+ <line x1="150" y1="0" x2="150" y2="180"/>
+ </g>
+ <rect width="180" height="180" fill="none" stroke="#6b5d3f" stroke-width="1"/>
+ </svg>`,
+};
+
+// ─── QUESTIONS ───────────────────────────────────────────────────────────────
+const ALL_QUESTIONS=[
+ {
+ label:'Pattern Recognition',
+ text:'What pattern motif is shown below?',
+ svg:'damask',
+ answers:['Damask','Toile de Jouy','Chinoiserie','Ogee'],
+ correct:0,
+ explain:'Damask is characterized by mirrored, reversible woven patterns — typically a central medallion with ornate floral or foliate symmetry, originating in Damascus.'
+ },
+ {
+ label:'Pattern Recognition',
+ text:'This repeating geometric border motif traces back to ancient Greece. Name it.',
+ svg:'greekkey',
+ answers:['Chevron','Greek Key / Meander','Houndstooth','Labyrinth Fret'],
+ correct:1,
+ explain:'The Greek Key (or Meander) is a continuous interlocking border motif derived from ancient Greek pottery and architectural friezes — a perennial in interior design.'
+ },
+ {
+ label:'Technique & Motif',
+ text:'The slightly blurred, feathered edges on this woven pattern are the hallmark of which ancient dyeing technique?',
+ svg:'ikat',
+ answers:['Batik','Ikat','Resist-printing','Shibori'],
+ correct:1,
+ explain:'Ikat is a resist-dyeing method where yarns are bound and dyed before weaving, creating the characteristic soft "halo" bleed at pattern edges.'
+ },
+ {
+ label:'Pattern Recognition',
+ text:'This pastoral scene with fine engraved-style line work depicts which classic wallcovering style?',
+ svg:'toile',
+ answers:['Toile de Jouy','Chinoiserie','Scenic Mural','Grisaille'],
+ correct:0,
+ explain:'Toile de Jouy originated in Jouy-en-Josas, France (c. 1760). Monochromatic pastoral or narrative scenes printed on plain ground cloth.'
+ },
+ {
+ label:'Pattern Recognition',
+ text:'Identify the geometric lattice pattern shown — a perennial in wallcovering design.',
+ svg:'trellis',
+ answers:['Ogee','Trellis / Lattice','Diamond Fret','Quatrefoil'],
+ correct:1,
+ explain:'Trellis patterns simulate garden lattice — diagonal or orthogonal grids with intersecting lines, sometimes with botanical motifs at the nodes.'
+ },
+ {
+ label:'Pattern Recognition',
+ text:'Which design tradition does this stylized botanical motif represent?',
+ svg:'chinoiserie',
+ answers:['Japonisme','Indian Paisley','Chinoiserie','Indienne'],
+ correct:2,
+ explain:'Chinoiserie (French: "Chinese-esque") flourished in 18th-century Europe as a romanticized interpretation of East Asian art — pagodas, exotic birds, and stylized botanicals.'
+ },
+ {
+ label:'Design Era',
+ text:'The starburst medallion and gilded geometric symmetry shown here typify which early 20th-century movement?',
+ svg:'artdeco',
+ answers:['Arts & Crafts','Bauhaus','Art Deco','Mid-Century Modern'],
+ correct:2,
+ explain:'Art Deco (1920s–30s) prized geometric symmetry, metallic glamour, and bold angular forms — influenced by Cubism and the 1925 Paris Exposition Internationale.'
+ },
+ {
+ label:'Materials',
+ text:'The tactile, woven texture you see here is made from natural plant fiber twisted into weft yarns on a cotton warp. What is this wallcovering material?',
+ svg:'grasscloth',
+ answers:['Sisal','Grasscloth','Jute Hessian','Linen Weave'],
+ correct:1,
+ explain:'Grasscloth uses natural plant fibers (seagrass, jute, bamboo) woven into panels. Each roll is unique; vertical seams are visible by design.'
+ },
+ {
+ label:'Design Vocabulary',
+ text:'A wallcovering described as "Type II" refers to what specification?',
+ svg:null,
+ answers:['A double roll','A commercial-grade vinyl ≥20 oz/yd²','An antimicrobial coating','An unpasted backing type'],
+ correct:1,
+ explain:'Type II is a commercial vinyl wallcovering standard (ASTM F793) — minimum 20 oz/sq yd total weight. Required in hospitality, healthcare, and high-traffic spaces.'
+ },
+ {
+ label:'Design Era',
+ text:'Which design aesthetic combines grandmotherly antiques, chintz, needlepoint, and layered pattern-on-pattern to deliberately nostalgic effect?',
+ svg:null,
+ answers:['Cottagecore','Grandmillennial','Dark Academia','Maximalism'],
+ correct:1,
+ explain:'Grandmillennial style (coined c. 2019) is a Gen-X/Millennial reclamation of "granny chic" — fringe, floral chintz, ornate wallpaper, and heirloom objects.'
+ },
+ {
+ label:'Color History',
+ text:'Which pigment gave William Morris\'s iconic Victorian wallpapers their distinctive, and later controversial, emerald green?',
+ svg:null,
+ answers:['Viridian','Malachite','Scheele\'s Green (arsenic)','Chrome Green'],
+ correct:2,
+ explain:'Scheele\'s Green (copper arsenite) produced brilliant emerald tones used in Victorian wallpapers. It could release arsenic gas in damp conditions — linked to illness in arsenic-saturated rooms.'
+ },
+ {
+ label:'Materials',
+ text:'A "Mylar" wallcovering gets its distinctive reflective quality from what material?',
+ svg:null,
+ answers:['Brushed aluminum foil','Polyester film (biaxially oriented PET)','Lacquered cotton','Metallic-ink printed vinyl'],
+ correct:1,
+ explain:'Mylar is a brand name for biaxially oriented polyester (BoPET) film. In wallcovering, a thin PET layer laminated to a substrate creates the mirror-like reflective surface.'
+ },
+];
+
+// ─── GAME STATE ───────────────────────────────────────────────────────────────
+let questions=[],qIdx=0,score=0,streak=0,bestStreak=0;
+let timerInterval=null,timeLeft=0,qStartTime=0;
+let correctCount=0,totalTime=0;
+const TIMER_MAX=18;
+const SPEED_BONUS_MAX=200;
+const BASE_POINTS=300;
+
+function shuffle(arr){
+ const a=[...arr];for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]];}return a;
+}
+
+function startGame(){
+ questions=shuffle(ALL_QUESTIONS);
+ qIdx=0;score=0;streak=0;bestStreak=0;correctCount=0;totalTime=0;
+ showScreen('gameScreen');
+ buildDots();
+ loadQuestion();
+ document.getElementById('scoreBadge').style.display='block';
+ updateScoreBadge();
+}
+
+function buildDots(){
+ const d=document.getElementById('progressDots');
+ d.innerHTML='';
+ questions.forEach((_,i)=>{
+ const dot=document.createElement('div');
+ dot.className='dot'+(i===0?' current':'');
+ dot.id=`dot${i}`;
+ d.appendChild(dot);
+ });
+}
+
+function updateDot(idx,result){
+ const d=document.getElementById(`dot${idx}`);
+ if(d)d.className='dot done-'+result;
+ const next=document.getElementById(`dot${idx+1}`);
+ if(next)next.className='dot current';
+}
+
+function loadQuestion(){
+ const q=questions[qIdx];
+ document.getElementById('qCounter').textContent=`Question ${qIdx+1} of ${questions.length}`;
+ document.getElementById('qLabel').textContent=q.label||'';
+ document.getElementById('qText').textContent=q.text;
+
+ const svgDiv=document.getElementById('qSvg');
+ if(q.svg&&svgs[q.svg]){
+ svgDiv.innerHTML=`<div class="q-svg">${svgs[q.svg]}</div>`;
+ } else {
+ svgDiv.innerHTML='';
+ }
+
+ // shuffle answer order
+ const indices=[0,1,2,3];
+ const shuffled=shuffle(indices);
+ const correctShuffled=shuffled.indexOf(q.correct);
+
+ const aContainer=document.getElementById('answers');
+ aContainer.innerHTML='';
+ shuffled.forEach((origIdx,i)=>{
+ const btn=document.createElement('button');
+ btn.className='ans-btn';
+ btn.textContent=q.answers[origIdx];
+ btn.dataset.orig=origIdx;
+ btn.dataset.isCorrect=origIdx===q.correct?'1':'0';
+ btn.addEventListener('click',()=>handleAnswer(btn,q));
+ btn.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();handleAnswer(btn,q);}});
+ aContainer.appendChild(btn);
+ });
+
+ const fb=document.getElementById('feedback');
+ fb.className='feedback';fb.innerHTML='';
+
+ document.getElementById('nextBtn').style.display='none';
+
+ startTimer();
+}
+
+function startTimer(){
+ clearInterval(timerInterval);
+ timeLeft=TIMER_MAX;
+ qStartTime=Date.now();
+ const fill=document.getElementById('timerFill');
+ fill.style.width='100%';
+ fill.style.background='var(--accent)';
+
+ timerInterval=setInterval(()=>{
+ timeLeft-=.1;
+ const pct=Math.max(0,timeLeft/TIMER_MAX*100);
+ fill.style.width=pct+'%';
+ if(pct<30)fill.style.background='#c96161';
+ else if(pct<60)fill.style.background='#c9a330';
+ else fill.style.background='var(--accent)';
+
+ if(timeLeft<=0){
+ clearInterval(timerInterval);
+ handleTimeout();
+ }
+ },100);
+}
+
+function handleTimeout(){
+ disableAnswers();
+ streak=0;updateStreakDisplay();
+ const q=questions[qIdx];
+ revealCorrect(q);
+ const fb=document.getElementById('feedback');
+ fb.className='feedback wrong-fb show';
+ fb.innerHTML=`Time's up! <span class="speed-bonus">${q.explain}</span>`;
+ sndWrong();
+ updateDot(qIdx,'wrong');
+ showNext();
+}
+
+function handleAnswer(btn,q){
+ if(btn.disabled)return;
+ clearInterval(timerInterval);
+ const elapsed=(Date.now()-qStartTime)/1000;
+ totalTime+=elapsed;
+ disableAnswers();
+
+ const isCorrect=btn.dataset.isCorrect==='1';
+ const fb=document.getElementById('feedback');
+
+ if(isCorrect){
+ const speedPct=Math.max(0,1-(elapsed/TIMER_MAX));
+ const speedBonus=Math.round(speedPct*SPEED_BONUS_MAX);
+ const multiplier=streak>=4?3:streak>=2?2:1;
+ const pts=(BASE_POINTS+speedBonus)*multiplier;
+ score+=pts;
+ streak++;
+ correctCount++;
+ if(streak>bestStreak)bestStreak=streak;
+ btn.classList.add('correct');
+ if(streak>=3){sndStreak();}else{sndCorrect();}
+ updateStreakDisplay();
+ fb.className='feedback correct-fb show';
+ let bonusText='';
+ if(speedBonus>150)bonusText+=` +${speedBonus} speed bonus`;
+ if(multiplier>1)bonusText+=` ×${multiplier} streak!`;
+ fb.innerHTML=`Correct! +${pts} pts${bonusText?'<span class="speed-bonus">'+bonusText+'</span>':''}`;
+ updateDot(qIdx,'correct');
+ } else {
+ btn.classList.add('wrong');
+ streak=0;updateStreakDisplay();
+ revealCorrect(q);
+ sndWrong();
+ fb.className='feedback wrong-fb show';
+ fb.innerHTML=`Not quite. <span class="speed-bonus">${q.explain}</span>`;
+ updateDot(qIdx,'wrong');
+ }
+
+ updateScoreBadge();
+ showNext();
+}
+
+function revealCorrect(q){
+ document.querySelectorAll('.ans-btn').forEach(b=>{
+ if(b.dataset.isCorrect==='1')b.classList.add('reveal');
+ });
+}
+
+function disableAnswers(){
+ document.querySelectorAll('.ans-btn').forEach(b=>b.disabled=true);
+}
+
+function updateStreakDisplay(){
+ const el=document.getElementById('streakDisplay');
+ if(streak>=2){
+ el.textContent=`🔥 ${streak}× Streak`;
+ el.classList.add('streak-flare');
+ setTimeout(()=>el.classList.remove('streak-flare'),400);
+ } else {
+ el.textContent='';
+ }
+}
+
+function updateScoreBadge(){
+ document.getElementById('scoreBadge').textContent=score.toLocaleString();
+}
+
+function showNext(){
+ const nb=document.getElementById('nextBtn');
+ nb.style.display='block';
+ nb.textContent=qIdx<questions.length-1?'Next →':'See Results';
+}
+
+document.getElementById('nextBtn').addEventListener('click',()=>{
+ qIdx++;
+ if(qIdx<questions.length){
+ loadQuestion();
+ } else {
+ showEndScreen();
+ }
+});
+
+function showEndScreen(){
+ clearInterval(timerInterval);
+ const best=parseInt(localStorage.getItem('dw-iq-best')||'0');
+ const isNewBest=score>best;
+ if(isNewBest)localStorage.setItem('dw-iq-best',score);
+
+ const pct=correctCount/questions.length;
+ let grade,blurb;
+ if(pct>=.92){grade='Master Curator';blurb='Extraordinary. Your eye for design history, pattern, and material is at the level of a true tastemaker. The trade knows your name.'}
+ else if(pct>=.75){grade='Connoisseur';blurb='Impressive depth of knowledge. You read rooms and references with confidence — wallcoverings hold no mystery for you.'}
+ else if(pct>=.5){grade='Design Devotee';blurb='A solid foundation. You know your damask from your toile and your Art Deco from your Bauhaus — keep studying the archive.'}
+ else{grade='Inspired Novice';blurb='Every expert begins here. The world of pattern, motif, and material is rich — and now you\'ve had your first proper introduction.'}
+
+ document.getElementById('gradeTitle').textContent=grade;
+ document.getElementById('finalScore').textContent=score.toLocaleString();
+ document.getElementById('statCorrect').textContent=`${correctCount}/${questions.length}`;
+ document.getElementById('statStreak').textContent=bestStreak;
+ const avgTime=correctCount>0?(totalTime/questions.length).toFixed(1)+'s':'—';
+ document.getElementById('statTime').textContent=avgTime;
+ document.getElementById('gradeBlurb').textContent=blurb;
+ document.getElementById('newBestBadge').style.display=isNewBest?'inline-block':'none';
+
+ sndWin();
+ showScreen('endScreen');
+ document.getElementById('scoreBadge').style.display='none';
+}
+
+// ─── SCREEN NAV ──────────────────────────────────────────────────────────────
+function showScreen(id){
+ document.querySelectorAll('.screen').forEach(s=>{s.classList.remove('active')});
+ document.getElementById(id).classList.add('active');
+}
+
+// ─── START / AGAIN ────────────────────────────────────────────────────────────
+document.getElementById('startBtn').addEventListener('click',startGame);
+document.getElementById('playAgainBtn').addEventListener('click',startGame);
+
+// load best score on start screen
+(function(){
+ const best=localStorage.getItem('dw-iq-best');
+ if(best&&parseInt(best)>0){
+ document.getElementById('bestNum').textContent=parseInt(best).toLocaleString();
+ document.getElementById('bestBadge').style.display='block';
+ }
+})();
+
+// ─── KEYBOARD ────────────────────────────────────────────────────────────────
+document.addEventListener('keydown',e=>{
+ if(['1','2','3','4'].includes(e.key)){
+ const idx=parseInt(e.key)-1;
+ const btns=document.querySelectorAll('.ans-btn');
+ if(btns[idx]&&!btns[idx].disabled)btns[idx].click();
+ }
+ if((e.key==='Enter'||e.key===' ')&&document.getElementById('nextBtn').style.display!=='none'){
+ document.getElementById('nextBtn').click();
+ }
+});
+
+// overlay dismiss
+document.getElementById('overlayBtn').addEventListener('click',()=>{
+ document.getElementById('overlay').classList.remove('visible');
+});
+</script>
+</body>
+</html>
diff --git a/public/games/dw-reel-storyboard.html b/public/games/dw-reel-storyboard.html
new file mode 100644
index 0000000..bd4f9b1
--- /dev/null
+++ b/public/games/dw-reel-storyboard.html
@@ -0,0 +1,948 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Reel Storyboard Builder — Designer Wallcoverings</title>
+<style>
+/* ── TOKENS ─────────────────────────────────────────────────────── */
+:root{
+ --bg:#0d0b09; --bg2:#1a1713; --bg3:#231f1b; --bg4:#2e2924;
+ --ink:#f4f1ea; --ink2:#c8bfa8; --ink3:#7a6e5f;
+ --accent:#c9a961; --accent2:#a8843d; --accent3:#e8d4a0;
+ --serif:'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif;
+ --sans:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
+ --ease:cubic-bezier(.4,0,.2,1);
+ --ease-spring:cubic-bezier(.34,1.56,.64,1);
+ --r:8px; --r2:14px;
+ --frame-w:270px; --frame-h:480px;
+ --strip-h:80px;
+}
+*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
+html{scroll-behavior:smooth}
+body{
+ background:var(--bg);color:var(--ink);font-family:var(--sans);
+ min-height:100vh;overflow-x:hidden;
+ display:grid;grid-template-rows:auto 1fr;
+}
+
+/* ── HEADER ─────────────────────────────────────────────────────── */
+header{
+ padding:18px 28px 14px;
+ border-bottom:1px solid rgba(201,169,97,.18);
+ display:flex;align-items:center;gap:20px;
+ background:var(--bg);
+ position:sticky;top:0;z-index:99;
+}
+.wordmark{
+ font-family:var(--serif);font-size:clamp(26px,3vw,42px);
+ font-weight:600;letter-spacing:.18em;text-transform:uppercase;
+ color:var(--ink);line-height:1;white-space:nowrap;
+}
+.wordmark span{color:var(--accent)}
+.header-sub{
+ font-size:11px;letter-spacing:.12em;text-transform:uppercase;
+ color:var(--ink3);padding-top:4px;
+}
+.header-badge{
+ margin-left:auto;
+ background:linear-gradient(135deg,var(--accent2),var(--accent));
+ color:#0d0b09;font-size:10px;font-weight:700;letter-spacing:.12em;
+ text-transform:uppercase;padding:5px 12px;border-radius:20px;
+}
+
+/* ── MAIN LAYOUT ─────────────────────────────────────────────────── */
+main{
+ display:grid;
+ grid-template-columns:var(--frame-w) 1fr;
+ grid-template-rows:auto auto;
+ gap:24px;padding:24px 28px 40px;
+ align-items:start;
+}
+@media(max-width:820px){
+ main{grid-template-columns:1fr;justify-items:center}
+ .right-panel{width:100%;max-width:600px}
+}
+
+/* ── PHONE FRAME ─────────────────────────────────────────────────── */
+.phone-wrap{
+ grid-row:1/3;
+ display:flex;flex-direction:column;align-items:center;gap:16px;
+ position:sticky;top:90px;
+}
+.phone{
+ width:var(--frame-w);height:var(--frame-h);
+ border-radius:32px;
+ border:3px solid rgba(201,169,97,.35);
+ box-shadow:0 0 0 1px rgba(0,0,0,.8),0 24px 80px rgba(0,0,0,.7),
+ inset 0 0 0 1px rgba(255,255,255,.04);
+ background:#000;
+ position:relative;overflow:hidden;
+}
+.phone-notch{
+ position:absolute;top:0;left:50%;transform:translateX(-50%);
+ width:90px;height:26px;background:#000;border-radius:0 0 16px 16px;z-index:20;
+}
+/* IG UI chrome */
+.phone-chrome{
+ position:absolute;top:30px;left:0;right:0;z-index:15;
+ padding:8px 14px;
+ display:flex;align-items:center;justify-content:space-between;
+}
+.chrome-avatar{
+ width:28px;height:28px;border-radius:50%;
+ border:1.5px solid var(--accent);
+ background:linear-gradient(135deg,#c9a961,#7a4f1a);
+ display:flex;align-items:center;justify-content:center;
+ font-size:10px;font-weight:700;color:#fff;
+}
+.chrome-user{font-size:11px;font-weight:700;color:#fff;margin-left:7px;flex:1;text-shadow:0 1px 4px rgba(0,0,0,.8)}
+.chrome-follow{
+ font-size:11px;font-weight:700;color:#fff;
+ background:rgba(255,255,255,.18);border-radius:4px;padding:3px 10px;
+}
+/* Progress bars */
+.progress-bars{
+ position:absolute;top:8px;left:10px;right:10px;z-index:16;
+ display:flex;gap:3px;
+}
+.progress-seg{
+ flex:1;height:2px;border-radius:1px;
+ background:rgba(255,255,255,.25);overflow:hidden;
+}
+.progress-fill{
+ height:100%;background:#fff;width:0%;
+ transition:width 50ms linear;
+}
+.progress-seg.done .progress-fill{width:100%}
+
+/* ── SCENES (canvas) ─────────────────────────────────────────────── */
+.scenes-canvas{
+ position:absolute;inset:0;
+}
+.scene{
+ position:absolute;inset:0;
+ opacity:0;
+ transition:opacity .45s var(--ease);
+ display:flex;align-items:flex-end;
+}
+.scene.active{opacity:1}
+.scene.exiting{opacity:0}
+
+/* Scene SVG bg */
+.scene-bg{
+ position:absolute;inset:0;width:100%;height:100%;
+}
+
+/* Caption overlay */
+.caption-overlay{
+ position:absolute;
+ bottom:0;left:0;right:0;
+ padding:50px 16px 64px;
+ background:linear-gradient(to top,rgba(0,0,0,.82) 0%,rgba(0,0,0,.3) 70%,transparent 100%);
+ z-index:10;
+}
+.caption-label{
+ font-size:9px;letter-spacing:.18em;text-transform:uppercase;
+ color:var(--accent);margin-bottom:6px;font-family:var(--sans);
+}
+.caption-text{
+ font-family:var(--serif);font-size:15px;line-height:1.45;
+ color:#fff;text-shadow:0 1px 6px rgba(0,0,0,.6);
+ animation:fadeSlide .5s var(--ease) both;
+}
+@keyframes fadeSlide{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
+.scene-type-tag{
+ display:inline-block;margin-bottom:8px;
+ font-size:9px;letter-spacing:.14em;text-transform:uppercase;
+ background:var(--accent);color:#0d0b09;
+ padding:2px 8px;border-radius:3px;font-weight:700;
+}
+/* bottom IG actions */
+.phone-actions{
+ position:absolute;bottom:16px;right:12px;z-index:16;
+ display:flex;flex-direction:column;align-items:center;gap:14px;
+}
+.action-icon{font-size:22px;text-shadow:0 1px 4px rgba(0,0,0,.7);cursor:default;}
+.action-count{font-size:10px;color:#fff;text-align:center;text-shadow:0 1px 3px rgba(0,0,0,.7)}
+
+/* ── PLAYBACK CONTROLS ───────────────────────────────────────────── */
+.playback-row{
+ display:flex;align-items:center;gap:10px;
+ width:var(--frame-w);
+}
+.btn-play{
+ width:44px;height:44px;border-radius:50%;
+ background:linear-gradient(135deg,var(--accent2),var(--accent));
+ border:none;cursor:pointer;
+ display:flex;align-items:center;justify-content:center;
+ font-size:17px;color:#0d0b09;
+ transition:transform .15s var(--ease-spring),box-shadow .15s;
+ flex-shrink:0;
+}
+.btn-play:hover{transform:scale(1.08);box-shadow:0 4px 20px rgba(201,169,97,.45)}
+.mood-select{
+ flex:1;background:var(--bg3);border:1px solid rgba(201,169,97,.22);
+ color:var(--ink2);font-family:var(--sans);font-size:12px;
+ padding:6px 10px;border-radius:var(--r);cursor:pointer;
+ appearance:none;
+ background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23c9a961'/%3E%3C/svg%3E");
+ background-repeat:no-repeat;background-position:right 10px center;
+ padding-right:28px;
+}
+.mood-select:focus{outline:1px solid var(--accent)}
+
+/* ── STRIP / TIMELINE ────────────────────────────────────────────── */
+.strip-wrap{
+ width:var(--frame-w);
+}
+.strip-label{
+ font-size:10px;letter-spacing:.14em;text-transform:uppercase;
+ color:var(--ink3);margin-bottom:8px;
+}
+.strip{
+ display:flex;gap:6px;overflow-x:auto;padding-bottom:4px;
+}
+.strip::-webkit-scrollbar{height:3px}
+.strip::-webkit-scrollbar-track{background:var(--bg2)}
+.strip::-webkit-scrollbar-thumb{background:var(--accent2);border-radius:2px}
+.thumb{
+ flex-shrink:0;
+ width:52px;height:52px;
+ border-radius:8px;overflow:hidden;
+ cursor:pointer;position:relative;
+ border:2px solid transparent;
+ transition:border-color .2s,transform .15s var(--ease-spring);
+}
+.thumb.active{border-color:var(--accent);transform:scale(1.08)}
+.thumb svg{width:100%;height:100%;display:block}
+.thumb-num{
+ position:absolute;top:2px;left:4px;
+ font-size:9px;font-weight:700;color:#fff;
+ text-shadow:0 1px 3px rgba(0,0,0,.9);
+}
+
+/* ── RIGHT PANEL ─────────────────────────────────────────────────── */
+.right-panel{
+ display:flex;flex-direction:column;gap:20px;
+}
+.panel-title{
+ font-family:var(--serif);font-size:22px;letter-spacing:.06em;
+ color:var(--ink);margin-bottom:2px;
+}
+.panel-sub{font-size:12px;color:var(--ink3);letter-spacing:.04em}
+
+/* Scene editor cards */
+.scene-cards{display:flex;flex-direction:column;gap:10px}
+.scene-card{
+ background:var(--bg2);border:1px solid rgba(255,255,255,.05);
+ border-radius:var(--r2);padding:14px 16px;
+ cursor:pointer;
+ transition:border-color .2s,box-shadow .2s,background .2s;
+ position:relative;
+}
+.scene-card:hover{border-color:rgba(201,169,97,.3);background:var(--bg3)}
+.scene-card.selected{
+ border-color:var(--accent);
+ box-shadow:0 0 0 1px var(--accent2);
+ background:var(--bg3);
+}
+.card-header{
+ display:flex;align-items:center;gap:10px;margin-bottom:10px;
+}
+.card-num{
+ width:22px;height:22px;border-radius:50%;
+ background:var(--bg4);
+ font-size:11px;font-weight:700;color:var(--accent);
+ display:flex;align-items:center;justify-content:center;flex-shrink:0;
+}
+.scene-card.selected .card-num{background:var(--accent);color:#0d0b09}
+.card-type-badge{
+ font-size:10px;letter-spacing:.12em;text-transform:uppercase;
+ color:var(--accent3);background:rgba(201,169,97,.1);
+ padding:2px 8px;border-radius:20px;
+}
+.card-controls{margin-left:auto;display:flex;gap:6px}
+.card-btn{
+ background:var(--bg4);border:1px solid rgba(255,255,255,.08);
+ color:var(--ink3);font-size:11px;
+ width:24px;height:24px;border-radius:4px;cursor:pointer;
+ display:flex;align-items:center;justify-content:center;
+ transition:color .15s,border-color .15s;
+}
+.card-btn:hover{color:var(--accent);border-color:var(--accent2)}
+.card-body{display:flex;gap:12px;align-items:flex-start}
+.card-thumb-mini{
+ width:40px;height:40px;flex-shrink:0;
+ border-radius:6px;overflow:hidden;border:1px solid rgba(255,255,255,.08);
+}
+.card-thumb-mini svg{width:100%;height:100%;display:block}
+.card-fields{flex:1;display:flex;flex-direction:column;gap:8px}
+.field-row{display:flex;gap:8px;align-items:center}
+.field-label{
+ font-size:10px;letter-spacing:.1em;text-transform:uppercase;
+ color:var(--ink3);width:56px;flex-shrink:0;
+}
+.field-input{
+ flex:1;background:var(--bg);border:1px solid rgba(255,255,255,.08);
+ color:var(--ink);font-family:var(--sans);font-size:12px;
+ padding:5px 8px;border-radius:5px;resize:none;
+ transition:border-color .15s;
+}
+.field-input:focus{outline:none;border-color:var(--accent)}
+.field-select{
+ flex:1;background:var(--bg);border:1px solid rgba(255,255,255,.08);
+ color:var(--ink2);font-family:var(--sans);font-size:12px;
+ padding:5px 8px;border-radius:5px;cursor:pointer;appearance:none;
+ background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%23c9a961'/%3E%3C/svg%3E");
+ background-repeat:no-repeat;background-position:right 8px center;padding-right:22px;
+}
+.field-select:focus{outline:1px solid var(--accent)}
+.dur-input{
+ width:52px;background:var(--bg);border:1px solid rgba(255,255,255,.08);
+ color:var(--ink);font-size:12px;padding:5px 8px;border-radius:5px;text-align:center;
+}
+.dur-input:focus{outline:1px solid var(--accent)}
+
+/* Copy button row */
+.copy-row{
+ display:flex;gap:10px;align-items:center;flex-wrap:wrap;
+ background:var(--bg2);border-radius:var(--r2);padding:14px 16px;
+ border:1px solid rgba(255,255,255,.05);
+}
+.btn-copy{
+ background:linear-gradient(135deg,var(--accent2),var(--accent));
+ color:#0d0b09;font-size:12px;font-weight:700;letter-spacing:.08em;
+ border:none;padding:9px 18px;border-radius:var(--r);cursor:pointer;
+ transition:transform .15s var(--ease-spring),opacity .15s;
+}
+.btn-copy:hover{transform:scale(1.04);opacity:.92}
+.copy-hint{font-size:11px;color:var(--ink3)}
+
+/* ── TOAST ───────────────────────────────────────────────────────── */
+#toast{
+ position:fixed;bottom:32px;left:50%;transform:translateX(-50%) translateY(20px);
+ background:var(--accent);color:#0d0b09;
+ font-size:13px;font-weight:700;letter-spacing:.06em;
+ padding:10px 24px;border-radius:24px;
+ opacity:0;pointer-events:none;z-index:999;
+ transition:opacity .25s,transform .25s var(--ease-spring);
+}
+#toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
+
+/* ── DRAG GHOST ──────────────────────────────────────────────────── */
+.dragging{opacity:.35;border-style:dashed!important}
+.drag-over{border-color:var(--accent)!important;background:var(--bg4)!important}
+</style>
+</head>
+<body>
+
+<header>
+ <div>
+ <div class="wordmark">Designer <span>Wallcoverings</span></div>
+ <div class="header-sub">Reel Storyboard Builder</div>
+ </div>
+ <div class="header-badge">Social Tool</div>
+</header>
+
+<main>
+ <!-- LEFT: phone + strip -->
+ <div class="phone-wrap">
+ <div class="phone" id="phone">
+ <div class="phone-notch"></div>
+ <div class="progress-bars" id="progressBars"></div>
+ <div class="phone-chrome">
+ <div class="chrome-avatar">DW</div>
+ <span class="chrome-user">designerwallcoverings</span>
+ <span class="chrome-follow">Follow</span>
+ </div>
+ <div class="scenes-canvas" id="scenesCanvas"></div>
+ <div class="phone-actions">
+ <div><div class="action-icon">♡</div><div class="action-count">4.2k</div></div>
+ <div><div class="action-icon">💬</div><div class="action-count">87</div></div>
+ <div><div class="action-icon">➤</div><div class="action-count">312</div></div>
+ <div><div class="action-icon">⋯</div></div>
+ </div>
+ </div>
+
+ <div class="playback-row">
+ <button class="btn-play" id="btnPlay" title="Play / Pause">▶</button>
+ <select class="mood-select" id="moodSelect">
+ <option value="sophisticated">♩ Sophisticated & Slow</option>
+ <option value="uplifting">♩ Warm & Uplifting</option>
+ <option value="editorial">♩ Editorial / Cinematic</option>
+ <option value="energetic">♩ Energetic & Bold</option>
+ <option value="ambient">♩ Ambient / Textural</option>
+ </select>
+ </div>
+
+ <div class="strip-wrap">
+ <div class="strip-label">Timeline</div>
+ <div class="strip" id="strip"></div>
+ </div>
+ </div>
+
+ <!-- RIGHT: scene editor -->
+ <div class="right-panel">
+ <div>
+ <div class="panel-title">Storyboard</div>
+ <div class="panel-sub">Edit scenes, drag to reorder, then copy captions</div>
+ </div>
+ <div class="scene-cards" id="sceneCards"></div>
+ <div class="copy-row">
+ <button class="btn-copy" id="btnCopy">Copy All Captions + Hashtags</button>
+ <span class="copy-hint" id="copyHint">Copies all scene captions as one post draft</span>
+ </div>
+ </div>
+</main>
+
+<div id="toast"></div>
+
+<script>
+/* ═══════════════════════════════════════════════════════════════════
+ DATA
+══════════════════════════════════════════════════════════════════ */
+const TYPE_PRESETS={
+ hook:{label:'Hook',color:'#c9a961',defaultCaption:'Stop scrolling. This is the texture your room has been missing.'},
+ reveal:{label:'Room Reveal',color:'#9b7fa0',defaultCaption:'A complete transformation. Same walls, new identity.'},
+ closeup:{label:'Close-up Texture',color:'#7aa0a0',defaultCaption:'Every detail is intentional. Feel the depth.'},
+ swatchgrid:{label:'Swatch Grid',color:'#a07a7a',defaultCaption:'Four finishes. One vision. Which speaks to you?'},
+ cta:{label:'CTA',color:'#a0a07a',defaultCaption:'Shop the link in bio. Free swatches available.'},
+};
+
+let scenes=[
+ {id:1,type:'hook',caption:'Stop scrolling. This is the texture your room has been missing.',dur:3},
+ {id:2,type:'reveal',caption:'A complete transformation — one wall, infinite presence.',dur:4},
+ {id:3,type:'closeup',caption:'Hand-woven grasscloth. The imperfection is the point.',dur:3},
+ {id:4,type:'swatchgrid',caption:'Four colorways. One obsession.',dur:3},
+ {id:5,type:'cta',caption:'Free sample swatches → link in bio. Let the wall do the talking.',dur:4},
+];
+
+let playState={active:false,scene:0,elapsed:0,raf:null,lastTs:null};
+let selectedCard=0;
+let dragSrc=null;
+
+/* ═══════════════════════════════════════════════════════════════════
+ INLINE SVG SCENE RENDERERS
+══════════════════════════════════════════════════════════════════ */
+function svgHook(){
+ return `<svg viewBox="0 0 270 480" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice">
+ <defs>
+ <linearGradient id="hg1" x1="0" y1="0" x2="1" y2="1">
+ <stop offset="0%" stop-color="#1a0e05"/>
+ <stop offset="100%" stop-color="#0d1a14"/>
+ </linearGradient>
+ <pattern id="hw" x="0" y="0" width="18" height="18" patternUnits="userSpaceOnUse">
+ <line x1="0" y1="0" x2="18" y2="18" stroke="#c9a961" stroke-width=".3" opacity=".18"/>
+ <line x1="18" y1="0" x2="0" y2="18" stroke="#c9a961" stroke-width=".2" opacity=".1"/>
+ </pattern>
+ <radialGradient id="hspot" cx="50%" cy="38%" r="55%">
+ <stop offset="0%" stop-color="#c9a961" stop-opacity=".22"/>
+ <stop offset="100%" stop-color="#c9a961" stop-opacity="0"/>
+ </radialGradient>
+ </defs>
+ <rect width="270" height="480" fill="url(#hg1)"/>
+ <rect width="270" height="480" fill="url(#hw)"/>
+ <rect width="270" height="480" fill="url(#hspot)"/>
+ <!-- dramatic vertical line accents -->
+ <line x1="67" y1="0" x2="67" y2="480" stroke="#c9a961" stroke-width=".4" opacity=".25"/>
+ <line x1="203" y1="0" x2="203" y2="480" stroke="#c9a961" stroke-width=".4" opacity=".25"/>
+ <!-- center ellipse glow -->
+ <ellipse cx="135" cy="200" rx="70" ry="90" fill="none" stroke="#c9a961" stroke-width=".6" opacity=".3"/>
+ <ellipse cx="135" cy="200" rx="50" ry="65" fill="none" stroke="#c9a961" stroke-width=".4" opacity=".2"/>
+ <!-- DW monogram -->
+ <text x="135" y="215" text-anchor="middle" font-size="38" font-family="Georgia,serif" fill="#c9a961" opacity=".55" letter-spacing="6">DW</text>
+ <text x="135" y="236" text-anchor="middle" font-size="8" font-family="'Arial',sans-serif" fill="#c9a961" opacity=".4" letter-spacing="5" text-transform="uppercase">DESIGNER WALLCOVERINGS</text>
+</svg>`;
+}
+
+function svgReveal(){
+ return `<svg viewBox="0 0 270 480" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice">
+ <defs>
+ <linearGradient id="rg1" x1="0" y1="0" x2="0" y2="1">
+ <stop offset="0%" stop-color="#1a1208"/>
+ <stop offset="55%" stop-color="#2a1f0e"/>
+ <stop offset="100%" stop-color="#0d0b09"/>
+ </linearGradient>
+ <linearGradient id="wallg" x1="0" y1="0" x2="1" y2="0">
+ <stop offset="0%" stop-color="#3d2a0f"/>
+ <stop offset="100%" stop-color="#5c3e1a"/>
+ </linearGradient>
+ <pattern id="damask" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
+ <rect width="40" height="40" fill="none"/>
+ <ellipse cx="20" cy="20" rx="8" ry="12" fill="none" stroke="#c9a961" stroke-width=".5" opacity=".25"/>
+ <ellipse cx="20" cy="20" rx="4" ry="7" fill="none" stroke="#c9a961" stroke-width=".4" opacity=".18"/>
+ <ellipse cx="20" cy="4" rx="3" ry="4" fill="#c9a961" fill-opacity=".1"/>
+ <ellipse cx="20" cy="36" rx="3" ry="4" fill="#c9a961" fill-opacity=".1"/>
+ <ellipse cx="4" cy="20" rx="4" ry="3" fill="#c9a961" fill-opacity=".1"/>
+ <ellipse cx="36" cy="20" rx="4" ry="3" fill="#c9a961" fill-opacity=".1"/>
+ </pattern>
+ <linearGradient id="floorlg" x1="0" y1="0" x2="0" y2="1">
+ <stop offset="0%" stop-color="#1a1208"/>
+ <stop offset="100%" stop-color="#0d0b09"/>
+ </linearGradient>
+ </defs>
+ <rect width="270" height="480" fill="url(#rg1)"/>
+ <!-- back wall -->
+ <rect x="20" y="60" width="230" height="260" rx="2" fill="url(#wallg)"/>
+ <rect x="20" y="60" width="230" height="260" rx="2" fill="url(#damask)"/>
+ <!-- baseboard -->
+ <rect x="20" y="316" width="230" height="6" fill="#7a5c28" opacity=".8"/>
+ <!-- floor -->
+ <rect x="0" y="322" width="270" height="158" fill="url(#floorlg)"/>
+ <!-- floor planks -->
+ <g opacity=".35" stroke="#c9a961" stroke-width=".4">
+ <line x1="0" y1="340" x2="270" y2="360"/><line x1="0" y1="380" x2="270" y2="400"/>
+ <line x1="0" y1="420" x2="270" y2="440"/><line x1="90" y1="322" x2="70" y2="480"/>
+ <line x1="180" y1="322" x2="160" y2="480"/>
+ </g>
+ <!-- sofa silhouette -->
+ <rect x="45" y="290" width="180" height="45" rx="6" fill="#1a1208"/>
+ <rect x="40" y="278" width="190" height="24" rx="8" fill="#231a0a"/>
+ <rect x="40" y="298" width="12" height="38" rx="4" fill="#1a1208"/>
+ <rect x="218" y="298" width="12" height="38" rx="4" fill="#1a1208"/>
+ <!-- cushions -->
+ <rect x="52" y="280" width="56" height="20" rx="4" fill="#2e2214" stroke="#c9a961" stroke-width=".4" stroke-opacity=".3"/>
+ <rect x="114" y="280" width="56" height="20" rx="4" fill="#2e2214" stroke="#c9a961" stroke-width=".4" stroke-opacity=".3"/>
+ <rect x="176" y="280" width="42" height="20" rx="4" fill="#2e2214" stroke="#c9a961" stroke-width=".4" stroke-opacity=".3"/>
+ <!-- side table + vase -->
+ <rect x="210" y="260" width="30" height="30" rx="2" fill="#15100a"/>
+ <ellipse cx="225" cy="260" rx="8" ry="3" fill="#3d2a0f"/>
+ <rect x="221" y="238" width="8" height="22" rx="4" fill="#4a3318"/>
+ <ellipse cx="225" cy="236" rx="10" ry="4" fill="#5c3e1a" opacity=".8"/>
+ <!-- frame on wall -->
+ <rect x="95" y="100" width="80" height="100" rx="2" stroke="#c9a961" stroke-width="1.5" fill="none" opacity=".5"/>
+ <rect x="100" y="105" width="70" height="90" rx="1" fill="#1a1208" fill-opacity=".6"/>
+ <text x="135" y="157" text-anchor="middle" font-size="20" fill="#c9a961" opacity=".3" font-family="Georgia">DW</text>
+ <!-- window light -->
+ <rect x="30" y="80" width="50" height="70" rx="2" fill="#e8d4a0" fill-opacity=".06" stroke="#c9a961" stroke-width=".5" stroke-opacity=".3"/>
+</svg>`;
+}
+
+function svgCloseup(){
+ return `<svg viewBox="0 0 270 480" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice">
+ <defs>
+ <linearGradient id="cg1" x1="0" y1="0" x2="1" y2="1">
+ <stop offset="0%" stop-color="#1a1510"/>
+ <stop offset="100%" stop-color="#0d1510"/>
+ </linearGradient>
+ <!-- Grasscloth weave -->
+ <pattern id="weave" x="0" y="0" width="8" height="12" patternUnits="userSpaceOnUse">
+ <rect width="8" height="12" fill="#2a2018"/>
+ <rect x="0" y="0" width="4" height="6" fill="#352a1c" rx=".5"/>
+ <rect x="4" y="6" width="4" height="6" fill="#352a1c" rx=".5"/>
+ <rect x="0" y="0" width="8" height=".5" fill="#c9a961" fill-opacity=".12"/>
+ <rect x="0" y="6" width="8" height=".5" fill="#c9a961" fill-opacity=".1"/>
+ <rect x="0" y="12" width="8" height=".5" fill="#c9a961" fill-opacity=".08"/>
+ <rect x="0" y="0" width=".5" height="6" fill="#e8d4a0" fill-opacity=".1"/>
+ <rect x="4" y="6" width=".5" height="6" fill="#e8d4a0" fill-opacity=".1"/>
+ </pattern>
+ <radialGradient id="cspot" cx="40%" cy="35%" r="50%">
+ <stop offset="0%" stop-color="#e8d4a0" stop-opacity=".35"/>
+ <stop offset="100%" stop-color="#e8d4a0" stop-opacity="0"/>
+ </radialGradient>
+ <linearGradient id="cvig" x1="0" y1="0" x2="0" y2="1">
+ <stop offset="0%" stop-color="#000" stop-opacity=".5"/>
+ <stop offset="30%" stop-color="#000" stop-opacity="0"/>
+ <stop offset="70%" stop-color="#000" stop-opacity="0"/>
+ <stop offset="100%" stop-color="#000" stop-opacity=".7"/>
+ </linearGradient>
+ </defs>
+ <rect width="270" height="480" fill="url(#cg1)"/>
+ <rect width="270" height="480" fill="url(#weave)"/>
+ <rect width="270" height="480" fill="url(#cspot)"/>
+ <rect width="270" height="480" fill="url(#cvig)"/>
+ <!-- fiber highlight strands -->
+ <g opacity=".18" stroke="#e8d4a0" stroke-width=".6">
+ <line x1="0" y1="42" x2="270" y2="48"/><line x1="0" y1="90" x2="270" y2="96"/>
+ <line x1="0" y1="138" x2="270" y2="143"/><line x1="0" y1="186" x2="270" y2="191"/>
+ <line x1="0" y1="234" x2="270" y2="239"/><line x1="0" y1="282" x2="270" y2="287"/>
+ <line x1="0" y1="330" x2="270" y2="335"/><line x1="0" y1="378" x2="270" y2="383"/>
+ <line x1="0" y1="426" x2="270" y2="431"/>
+ </g>
+ <!-- vertical warps -->
+ <g opacity=".1" stroke="#c9a961" stroke-width=".4">
+ <line x1="33" y1="0" x2="33" y2="480"/><line x1="99" y1="0" x2="99" y2="480"/>
+ <line x1="165" y1="0" x2="165" y2="480"/><line x1="231" y1="0" x2="231" y2="480"/>
+ </g>
+ <!-- scale indicator -->
+ <line x1="30" y1="52" x2="90" y2="52" stroke="#c9a961" stroke-width=".8" opacity=".6"/>
+ <line x1="30" y1="48" x2="30" y2="56" stroke="#c9a961" stroke-width=".8" opacity=".6"/>
+ <line x1="90" y1="48" x2="90" y2="56" stroke="#c9a961" stroke-width=".8" opacity=".6"/>
+ <text x="60" y="68" text-anchor="middle" font-size="7" fill="#c9a961" opacity=".7" font-family="Arial,sans-serif" letter-spacing="1">2 cm</text>
+</svg>`;
+}
+
+function svgSwatchGrid(){
+ return `<svg viewBox="0 0 270 480" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice">
+ <defs>
+ <linearGradient id="sg1" x1="0" y1="0" x2="0" y2="1">
+ <stop offset="0%" stop-color="#141010"/><stop offset="100%" stop-color="#0d0b09"/>
+ </linearGradient>
+ <!-- swatch textures as patterns -->
+ <pattern id="sw1p" x="0" y="0" width="6" height="6" patternUnits="userSpaceOnUse">
+ <rect width="6" height="6" fill="#3d2a0f"/>
+ <line x1="0" y1="3" x2="6" y2="3" stroke="#c9a961" stroke-width=".3" opacity=".3"/>
+ </pattern>
+ <pattern id="sw2p" x="0" y="0" width="8" height="8" patternUnits="userSpaceOnUse">
+ <rect width="8" height="8" fill="#1a2a2a"/>
+ <rect x="0" y="0" width="4" height="4" fill="#1f3030"/>
+ <rect x="4" y="4" width="4" height="4" fill="#1f3030"/>
+ </pattern>
+ <pattern id="sw3p" x="0" y="0" width="5" height="10" patternUnits="userSpaceOnUse">
+ <rect width="5" height="10" fill="#2a1f2a"/>
+ <rect x="0" y="0" width="5" height="5" fill="#352835"/>
+ <line x1="0" y1="0" x2="5" y2="5" stroke="#9b7fa0" stroke-width=".3" opacity=".3"/>
+ </pattern>
+ <pattern id="sw4p" x="0" y="0" width="6" height="6" patternUnits="userSpaceOnUse">
+ <rect width="6" height="6" fill="#1c1c14"/>
+ <ellipse cx="3" cy="3" rx="2" ry="2" fill="none" stroke="#a0a07a" stroke-width=".4" opacity=".3"/>
+ </pattern>
+ </defs>
+ <rect width="270" height="480" fill="url(#sg1)"/>
+ <!-- label top -->
+ <text x="135" y="52" text-anchor="middle" font-size="8" fill="#c9a961" opacity=".7" font-family="Arial,sans-serif" letter-spacing="4">COLORWAYS</text>
+ <!-- 2x2 swatch grid -->
+ <!-- SW1: Antique Gold -->
+ <rect x="22" y="65" width="108" height="160" rx="8" fill="url(#sw1p)"/>
+ <rect x="22" y="65" width="108" height="160" rx="8" fill="none" stroke="#c9a961" stroke-width=".5" opacity=".4"/>
+ <text x="76" y="198" text-anchor="middle" font-size="8" fill="#c9a961" opacity=".8" font-family="Arial,sans-serif">Antique Gold</text>
+ <!-- SW2: Deep Teal -->
+ <rect x="140" y="65" width="108" height="160" rx="8" fill="url(#sw2p)"/>
+ <rect x="140" y="65" width="108" height="160" rx="8" fill="none" stroke="#7aa0a0" stroke-width=".5" opacity=".4"/>
+ <text x="194" y="198" text-anchor="middle" font-size="8" fill="#7aa0a0" opacity=".8" font-family="Arial,sans-serif">Deep Teal</text>
+ <!-- SW3: Mauve Dusk -->
+ <rect x="22" y="238" width="108" height="160" rx="8" fill="url(#sw3p)"/>
+ <rect x="22" y="238" width="108" height="160" rx="8" fill="none" stroke="#9b7fa0" stroke-width=".5" opacity=".4"/>
+ <text x="76" y="370" text-anchor="middle" font-size="8" fill="#9b7fa0" opacity=".8" font-family="Arial,sans-serif">Mauve Dusk</text>
+ <!-- SW4: Sage Linen -->
+ <rect x="140" y="238" width="108" height="160" rx="8" fill="url(#sw4p)"/>
+ <rect x="140" y="238" width="108" height="160" rx="8" fill="none" stroke="#a0a07a" stroke-width=".5" opacity=".4"/>
+ <text x="194" y="370" text-anchor="middle" font-size="8" fill="#a0a07a" opacity=".8" font-family="Arial,sans-serif">Sage Linen</text>
+ <!-- corner dots -->
+ <circle cx="135" cy="213" r="3" fill="#c9a961" opacity=".5"/>
+</svg>`;
+}
+
+function svgCTA(){
+ return `<svg viewBox="0 0 270 480" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice">
+ <defs>
+ <radialGradient id="ctag" cx="50%" cy="45%" r="60%">
+ <stop offset="0%" stop-color="#2a1f0a"/>
+ <stop offset="100%" stop-color="#0d0b09"/>
+ </radialGradient>
+ <pattern id="ctap" x="0" y="0" width="30" height="30" patternUnits="userSpaceOnUse">
+ <rect width="30" height="30" fill="none"/>
+ <circle cx="15" cy="15" r="10" fill="none" stroke="#c9a961" stroke-width=".3" opacity=".12"/>
+ <circle cx="0" cy="0" r="5" fill="none" stroke="#c9a961" stroke-width=".25" opacity=".08"/>
+ </pattern>
+ </defs>
+ <rect width="270" height="480" fill="url(#ctag)"/>
+ <rect width="270" height="480" fill="url(#ctap)"/>
+ <!-- center composition -->
+ <rect x="40" y="120" width="190" height="240" rx="12" fill="none" stroke="#c9a961" stroke-width=".8" opacity=".3"/>
+ <rect x="52" y="132" width="166" height="216" rx="8" fill="none" stroke="#c9a961" stroke-width=".4" opacity=".18"/>
+ <!-- logo text -->
+ <text x="135" y="215" text-anchor="middle" font-size="12" fill="#c9a961" opacity=".9" font-family="Georgia,serif" letter-spacing="5">DESIGNER</text>
+ <text x="135" y="232" text-anchor="middle" font-size="12" fill="#c9a961" opacity=".9" font-family="Georgia,serif" letter-spacing="5">WALLCOVERINGS</text>
+ <!-- divider -->
+ <line x1="85" y1="245" x2="185" y2="245" stroke="#c9a961" stroke-width=".6" opacity=".5"/>
+ <!-- CTA pill -->
+ <rect x="78" y="262" width="114" height="30" rx="15" fill="#c9a961" fill-opacity=".15" stroke="#c9a961" stroke-width=".8" opacity=".7"/>
+ <text x="135" y="282" text-anchor="middle" font-size="10" fill="#c9a961" opacity=".9" font-family="Arial,sans-serif" letter-spacing="3">SHOP NOW</text>
+ <!-- decorative corners -->
+ <g stroke="#c9a961" stroke-width="1" opacity=".4">
+ <path d="M 50 140 L 50 128 L 62 128"/><path d="M 220 140 L 220 128 L 208 128"/>
+ <path d="M 50 340 L 50 352 L 62 352"/><path d="M 220 340 L 220 352 L 208 352"/>
+ </g>
+ <!-- small swatches at bottom -->
+ <g opacity=".55">
+ <rect x="75" y="310" width="22" height="22" rx="4" fill="#3d2a0f" stroke="#c9a961" stroke-width=".4"/>
+ <rect x="103" y="310" width="22" height="22" rx="4" fill="#1a2a2a" stroke="#7aa0a0" stroke-width=".4"/>
+ <rect x="131" y="310" width="22" height="22" rx="4" fill="#2a1f2a" stroke="#9b7fa0" stroke-width=".4"/>
+ <rect x="159" y="310" width="22" height="22" rx="4" fill="#1c1c14" stroke="#a0a07a" stroke-width=".4"/>
+ </g>
+</svg>`;
+}
+
+const SVG_MAP={hook:svgHook,reveal:svgReveal,closeup:svgCloseup,swatchgrid:svgSwatchGrid,cta:svgCTA};
+
+/* ═══════════════════════════════════════════════════════════════════
+ HASHTAGS
+══════════════════════════════════════════════════════════════════ */
+const HASHTAGS='#WallcoveringDesign #LuxuryInteriors #DesignerWallcoverings #InteriorDesign #WallDecor #TextureLovers #GrassclotheWall #WallpaperDesign #HomeDecor #ElevateYourSpace #WallArt #InteriorInspiration #LuxuryLiving #DesignDetails #WallpapersOfInstagram';
+
+/* ═══════════════════════════════════════════════════════════════════
+ RENDER PHONE SCENES
+══════════════════════════════════════════════════════════════════ */
+function buildPhoneScenes(){
+ const canvas=document.getElementById('scenesCanvas');
+ const bars=document.getElementById('progressBars');
+ canvas.innerHTML='';bars.innerHTML='';
+ scenes.forEach((sc,i)=>{
+ // scene div
+ const div=document.createElement('div');
+ div.className='scene'+(i===0?' active':'');
+ div.id='scene-'+sc.id;
+ // bg
+ const bg=document.createElement('div');
+ bg.className='scene-bg';
+ bg.innerHTML=SVG_MAP[sc.type]();
+ div.appendChild(bg);
+ // caption
+ const cap=document.createElement('div');
+ cap.className='caption-overlay';
+ cap.innerHTML=`<div class="scene-type-tag">${TYPE_PRESETS[sc.type].label}</div>
+ <div class="caption-label">Scene ${i+1} of ${scenes.length}</div>
+ <div class="caption-text" id="captext-${sc.id}">${esc(sc.caption)}</div>`;
+ div.appendChild(cap);
+ canvas.appendChild(div);
+ // progress bar segment
+ const seg=document.createElement('div');
+ seg.className='progress-seg'+(i===0?' active':'');
+ seg.id='seg-'+sc.id;
+ seg.innerHTML='<div class="progress-fill" id="fill-'+sc.id+'"></div>';
+ bars.appendChild(seg);
+ });
+}
+
+/* ═══════════════════════════════════════════════════════════════════
+ RENDER STRIP
+══════════════════════════════════════════════════════════════════ */
+function buildStrip(){
+ const strip=document.getElementById('strip');
+ strip.innerHTML='';
+ scenes.forEach((sc,i)=>{
+ const th=document.createElement('div');
+ th.className='thumb'+(i===playState.scene?' active':'');
+ th.dataset.idx=i;
+ th.innerHTML=`<div style="transform:scale(0.54);transform-origin:top left;width:185%;height:185%;pointer-events:none">${SVG_MAP[sc.type]()}</div><span class="thumb-num">${i+1}</span>`;
+ th.addEventListener('click',()=>jumpToScene(i));
+ strip.appendChild(th);
+ });
+}
+
+/* ═══════════════════════════════════════════════════════════════════
+ RENDER SCENE CARDS (right panel)
+══════════════════════════════════════════════════════════════════ */
+function buildCards(){
+ const container=document.getElementById('sceneCards');
+ container.innerHTML='';
+ scenes.forEach((sc,i)=>{
+ const card=document.createElement('div');
+ card.className='scene-card'+(i===selectedCard?' selected':'');
+ card.dataset.idx=i;
+ card.setAttribute('draggable','true');
+ card.innerHTML=`
+ <div class="card-header">
+ <div class="card-num">${i+1}</div>
+ <select class="field-select" data-field="type" style="font-size:11px;padding:3px 8px;flex:1" onchange="updateField(${i},'type',this.value)">
+ ${Object.entries(TYPE_PRESETS).map(([k,v])=>`<option value="${k}"${sc.type===k?' selected':''}>${v.label}</option>`).join('')}
+ </select>
+ <div class="card-controls">
+ <button class="card-btn" title="Move up" onclick="moveScene(${i},-1)">↑</button>
+ <button class="card-btn" title="Move down" onclick="moveScene(${i},1)">↓</button>
+ </div>
+ </div>
+ <div class="card-body">
+ <div class="card-thumb-mini">${SVG_MAP[sc.type]()}</div>
+ <div class="card-fields">
+ <div class="field-row">
+ <span class="field-label">Caption</span>
+ <textarea class="field-input" rows="2" oninput="updateField(${i},'caption',this.value)">${esc(sc.caption)}</textarea>
+ </div>
+ <div class="field-row">
+ <span class="field-label">Duration</span>
+ <input type="number" class="dur-input field-input" min="1" max="15" value="${sc.dur}" style="width:52px;flex:none" oninput="updateField(${i},'dur',+this.value)">
+ <span style="font-size:11px;color:var(--ink3);margin-left:4px">sec</span>
+ </div>
+ </div>
+ </div>`;
+ // click to select
+ card.addEventListener('click',e=>{
+ if(e.target.closest('button')||e.target.closest('select')||e.target.closest('textarea')||e.target.closest('input'))return;
+ selectedCard=i;jumpToScene(i);buildCards();
+ });
+ // drag
+ card.addEventListener('dragstart',e=>{dragSrc=i;card.classList.add('dragging');e.dataTransfer.effectAllowed='move'});
+ card.addEventListener('dragend',()=>{card.classList.remove('dragging');document.querySelectorAll('.scene-card').forEach(c=>c.classList.remove('drag-over'))});
+ card.addEventListener('dragover',e=>{e.preventDefault();card.classList.add('drag-over')});
+ card.addEventListener('dragleave',()=>card.classList.remove('drag-over'));
+ card.addEventListener('drop',e=>{
+ e.preventDefault();card.classList.remove('drag-over');
+ if(dragSrc===null||dragSrc===i)return;
+ const moved=scenes.splice(dragSrc,1)[0];
+ scenes.splice(i,0,moved);
+ selectedCard=i;dragSrc=null;
+ rebuildAll();
+ });
+ container.appendChild(card);
+ });
+}
+
+function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
+
+/* ═══════════════════════════════════════════════════════════════════
+ FIELD UPDATE
+══════════════════════════════════════════════════════════════════ */
+function updateField(idx,field,val){
+ scenes[idx][field]=val;
+ if(field==='caption'){
+ const el=document.getElementById('captext-'+scenes[idx].id);
+ if(el){el.innerHTML=esc(val);el.style.animation='none';requestAnimationFrame(()=>{el.style.animation=''})}
+ }
+ if(field==='type'){
+ // rebuild that scene's bg + card thumb
+ const scEl=document.getElementById('scene-'+scenes[idx].id);
+ if(scEl){
+ const bg=scEl.querySelector('.scene-bg');
+ if(bg)bg.innerHTML=SVG_MAP[val]();
+ const tag=scEl.querySelector('.scene-type-tag');
+ if(tag)tag.textContent=TYPE_PRESETS[val].label;
+ }
+ buildStrip();
+ buildCards();
+ }
+}
+
+/* ═══════════════════════════════════════════════════════════════════
+ SCENE MOVEMENT
+══════════════════════════════════════════════════════════════════ */
+function moveScene(idx,dir){
+ const tgt=idx+dir;
+ if(tgt<0||tgt>=scenes.length)return;
+ [scenes[idx],scenes[tgt]]=[scenes[tgt],scenes[idx]];
+ selectedCard=tgt;
+ rebuildAll();
+}
+
+function rebuildAll(){
+ buildPhoneScenes();
+ buildStrip();
+ buildCards();
+ // re-apply active
+ updatePhoneActive(playState.scene);
+}
+
+function jumpToScene(idx){
+ playState.scene=idx;playState.elapsed=0;
+ updatePhoneActive(idx);
+ buildStrip();
+}
+
+function updatePhoneActive(idx){
+ document.querySelectorAll('.scene').forEach((el,i)=>{
+ el.classList.toggle('active',i===idx);
+ });
+ document.querySelectorAll('.progress-seg').forEach((el,i)=>{
+ el.classList.toggle('done',i<idx);
+ const fill=el.querySelector('.progress-fill');
+ if(fill)fill.style.width=i<idx?'100%':i===idx?'0%':'0%';
+ });
+ document.querySelectorAll('.thumb').forEach((el,i)=>{
+ el.classList.toggle('active',i===idx);
+ });
+}
+
+/* ═══════════════════════════════════════════════════════════════════
+ PLAYBACK
+══════════════════════════════════════════════════════════════════ */
+function tick(ts){
+ if(!playState.active)return;
+ if(playState.lastTs===null)playState.lastTs=ts;
+ const dt=(ts-playState.lastTs)/1000;
+ playState.lastTs=ts;
+ const sc=scenes[playState.scene];
+ const dur=Math.max(1,sc?sc.dur:3);
+ playState.elapsed+=dt;
+ // update fill
+ const fill=document.getElementById('fill-'+sc.id);
+ if(fill)fill.style.width=Math.min(100,playState.elapsed/dur*100)+'%';
+ if(playState.elapsed>=dur){
+ const next=playState.scene+1;
+ if(next>=scenes.length){
+ // loop
+ playState.elapsed=0;playState.scene=0;playState.lastTs=ts;
+ updatePhoneActive(0);buildStrip();
+ } else {
+ playState.elapsed=0;playState.scene=next;playState.lastTs=ts;
+ updatePhoneActive(next);buildStrip();
+ }
+ }
+ playState.raf=requestAnimationFrame(tick);
+}
+
+document.getElementById('btnPlay').addEventListener('click',()=>{
+ playState.active=!playState.active;
+ document.getElementById('btnPlay').textContent=playState.active?'⏸':'▶';
+ if(playState.active){playState.lastTs=null;playState.raf=requestAnimationFrame(tick);}
+ else{if(playState.raf)cancelAnimationFrame(playState.raf);}
+});
+
+/* ═══════════════════════════════════════════════════════════════════
+ COPY
+══════════════════════════════════════════════════════════════════ */
+document.getElementById('btnCopy').addEventListener('click',()=>{
+ const mood=document.getElementById('moodSelect');
+ const moodLabel=mood.options[mood.selectedIndex].text;
+ let txt=`✨ Designer Wallcoverings — Instagram Reel\nMood: ${moodLabel}\n\n`;
+ scenes.forEach((sc,i)=>{
+ txt+=`[Scene ${i+1} — ${TYPE_PRESETS[sc.type].label} (${sc.dur}s)]\n${sc.caption}\n\n`;
+ });
+ txt+=HASHTAGS;
+ navigator.clipboard.writeText(txt).then(()=>showToast('Captions + hashtags copied!')).catch(()=>showToast('Copy failed — try selecting manually'));
+});
+
+function showToast(msg){
+ const t=document.getElementById('toast');
+ t.textContent=msg;t.classList.add('show');
+ clearTimeout(t._tid);t._tid=setTimeout(()=>t.classList.remove('show'),2600);
+}
+
+/* ═══════════════════════════════════════════════════════════════════
+ KEYBOARD NAV
+══════════════════════════════════════════════════════════════════ */
+document.addEventListener('keydown',e=>{
+ if(e.target.tagName==='TEXTAREA'||e.target.tagName==='INPUT'||e.target.tagName==='SELECT')return;
+ if(e.key==='ArrowRight'||e.key==='ArrowDown'){
+ const n=Math.min(scenes.length-1,playState.scene+1);
+ jumpToScene(n);buildStrip();
+ }
+ if(e.key==='ArrowLeft'||e.key==='ArrowUp'){
+ const n=Math.max(0,playState.scene-1);
+ jumpToScene(n);buildStrip();
+ }
+ if(e.key===' '){
+ e.preventDefault();
+ document.getElementById('btnPlay').click();
+ }
+});
+
+/* ═══════════════════════════════════════════════════════════════════
+ TOUCH SWIPE ON PHONE
+══════════════════════════════════════════════════════════════════ */
+(()=>{
+ const phone=document.getElementById('phone');
+ let tx=null;
+ phone.addEventListener('touchstart',e=>{tx=e.touches[0].clientX},{passive:true});
+ phone.addEventListener('touchend',e=>{
+ if(tx===null)return;
+ const dx=e.changedTouches[0].clientX-tx;
+ if(Math.abs(dx)>40){
+ if(dx<0)jumpToScene(Math.min(scenes.length-1,playState.scene+1));
+ else jumpToScene(Math.max(0,playState.scene-1));
+ buildStrip();
+ }
+ tx=null;
+ },{passive:true});
+})();
+
+/* ═══════════════════════════════════════════════════════════════════
+ INIT
+══════════════════════════════════════════════════════════════════ */
+buildPhoneScenes();
+buildStrip();
+buildCards();
+</script>
+</body>
+</html>
← 2866307 Agent-driven tool: DW Shop by Color (frontend-developer) — i
·
back to Model Arena
·
Agent-driven trivia game: DW Design IQ (frontend-developer) e02303a →