[object Object]

← back to Model Arena

Agent-driven high-detail: DW Mood Board Studio (frontend-developer, plan+execute, draggable board + 5 style presets + export) — registered as featured arcade game

35ba974e977b1934aa2bab6ab467ef42586cade4 · 2026-07-25 09:53:00 -0700 · Steve Abrams

Files touched

Diff

commit 35ba974e977b1934aa2bab6ab467ef42586cade4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Jul 25 09:53:00 2026 -0700

    Agent-driven high-detail: DW Mood Board Studio (frontend-developer, plan+execute, draggable board + 5 style presets + export) — registered as featured arcade game
---
 data/arcade-games.json                 |    6 +
 public/games/dw-mood-board-studio.html | 2108 ++++++++++++++++++++++++++++++++
 2 files changed, 2114 insertions(+)

diff --git a/data/arcade-games.json b/data/arcade-games.json
index 9d32459..93aa9c7 100644
--- a/data/arcade-games.json
+++ b/data/arcade-games.json
@@ -17,6 +17,12 @@
       "title": "DW Color Story Studio",
       "credit": "frontend-developer",
       "note": "Agent-built flagship \u2014 seasonal wallcovering color-story tool for the DW social team"
+    },
+    {
+      "slug": "dw-mood-board-studio",
+      "title": "DW Mood Board Studio",
+      "credit": "frontend-developer",
+      "note": "Agent-built \u2014 draggable interior mood board with 5 style presets + PNG export"
     }
   ]
 }
\ No newline at end of file
diff --git a/public/games/dw-mood-board-studio.html b/public/games/dw-mood-board-studio.html
new file mode 100644
index 0000000..538ccf8
--- /dev/null
+++ b/public/games/dw-mood-board-studio.html
@@ -0,0 +1,2108 @@
+<!--
+================================================================================
+DESIGNER WALLCOVERINGS — INTERIOR MOOD BOARD STUDIO
+Plan Document (v1.0 — Flagship Build)
+================================================================================
+
+CONCEPT
+-------
+A professional creative tool for interior designers and the DW social team to
+compose, refine, and export luxury interior mood boards. The studio presents a
+draggable-and-arrangeable canvas where designers can assemble wallcovering
+texture swatches, color chips, material chips, and a type-pairing sample into
+a cohesive visual statement. Five curated style presets instantly repopulate
+the canvas with thematically coherent arrangements — from Grandmillennial's
+richly layered maximalism to Modern Minimalist's restrained precision.
+
+The deliverable: a 1080×1350 export card (Instagram Portrait ratio) that looks
+like it came from a luxury interiors studio — not a web tool.
+
+TARGET USER
+-----------
+1. Interior designer: assembling a client presentation or concept board, pulling
+   DW materials to visually propose a room's direction before spec-ing product.
+2. DW social manager: generating shareable content (Instagram, Pinterest boards)
+   that visually articulates the brand's sensibility. Needs speed, polish, and
+   brand-correct output with zero design skill required.
+
+UX FLOW
+-------
+[1] LANDING / HEADER
+    Full-width header: wordmark "DESIGNER WALLCOVERINGS" in 48px Cormorant Garamond
+    uppercase, letter-spacing .18em, darkest ink on brass strip or warm surface.
+    Subtitle "Interior Mood Board Studio" in sans. Studio tools in a top rail.
+
+[2] STYLE PRESET SELECTOR (5 pills above canvas)
+    • Grandmillennial — chinoiserie, damask, warm golds, antique roses
+    • Coastal — linen, raffia, seagrass, celadon, white oak
+    • Moody Maximalist — dark silk, jewel tones, rich damask, deep ink
+    • Modern Minimalist — clean linen, matte stone, quiet stripe, bone
+    • Art Deco — geometric silk, brass chevron, ink, gold, lacquer black
+    Clicking a preset triggers a smooth crossfade: swatches morph textures,
+    color chips repopulate with named hues, materials update — 400ms ease-out.
+
+[3] MOOD BOARD CANVAS (the central interactive surface)
+    A gridded artboard (subtle 20px grid lines) where draggable tiles live:
+    • 6 WALLCOVERING SWATCH TILES — each 160×160px with distinct CSS/SVG textures:
+        - Grasscloth: woven horizontal fiber lines, warm tan/caramel
+        - Damask: intricate mirrored motif in cream on ivory
+        - Silk Stripe: thin alternating satin/matte vertical bands
+        - Linen: natural woven mesh pattern, oat/flax
+        - Raffia: diagonal bast fiber weave, warm straw
+        - Chinoiserie: hand-painted branch/blossom on pale background
+    • 6 COLOR CHIPS — rounded rectangular chips, each labeled:
+        - Oatmeal #e8dccb, Celadon #a8c4b0, Terracotta #c1623f,
+          Ink #1c2b3a, Brass #c9a961, Sage #7a9472
+    • 1 TYPE PAIRING TILE — shows a serif headline + sans body combination
+    • 3 MATERIAL CHIPS — small rounded badges: Brass / Marble / White Oak
+    Each tile: draggable to anywhere on canvas, z-index promoted on pickup,
+    hover lift (translateY -4px, shadow deepen), snaps to 10px grid on drop.
+
+[4] CANVAS CONTROLS (right rail)
+    • Shuffle / Recompose button — re-arranges all tiles with easing (stagger 60ms)
+    • Clear board — fade tiles to corner
+    • Layer controls (Bring to Front / Send to Back)
+    • Delete selected tile
+
+[5] EXPORT MOOD BOARD
+    Renders the current board as a 1080×1350 Instagram-portrait card on an
+    HTML5 Canvas element, then triggers download. The card includes:
+    • Dark warm-black (#0d0b09) background with hairline brass frame
+    • Board tiles composited in their current positions (scaled)
+    • Brass wordmark lockup + style label at bottom
+    • Color palette strip with hex names along bottom edge
+    • Subtle vignette overlay for print-quality feel
+
+[6] MICRO-INTERACTIONS
+    • Tile hover: lift + shadow (200ms ease)
+    • Tile drag: cursor 'grabbing', drop shadow deepens, slight scale 1.03
+    • Preset switch: stagger-fade swatches out then in (80ms intervals)
+    • Shuffle: all tiles simultaneously tween to new positions (spring-ish ease)
+    • Export: progress shimmer on button, then download fires
+
+VISUAL SYSTEM (Design Tokens)
+------------------------------
+--bg:           #0d0b09   warm near-black
+--bg-raised:    #161310   lifted panel surface
+--bg-canvas:    #f4f1ea   warm ivory artboard
+--ink:          #1c1a17   darkest text
+--ink-inv:      #e8e3da   primary text on dark
+--ink-dim:      #9a9184   muted/secondary
+--accent:       #c9a961   DW brass
+--accent-dim:   #7a6538   brass hover
+--line:         rgba(201,169,97,0.20)
+--line-canvas:  rgba(180,170,150,0.25)
+--shadow-tile:  0 6px 24px rgba(0,0,0,0.18)
+--shadow-drag:  0 18px 48px rgba(0,0,0,0.32)
+--serif:        'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif
+--sans:         -apple-system,'Helvetica Neue',Arial,sans-serif
+--ease:         cubic-bezier(0.25,0.46,0.45,0.94)
+--ease-spring:  cubic-bezier(0.34,1.56,0.64,1)
+
+TYPE SCALE
+----------
+Wordmark:    48px serif, uppercase, ls .18em, weight 600
+Section:     13px sans, uppercase, ls .14em
+Tile label:  11px sans, uppercase, ls .10em
+Body:        16px serif
+
+SPACING SCALE
+-------------
+4 / 8 / 12 / 16 / 24 / 32 / 48 / 64 / 96px (multiples of 4)
+
+MOTION BUDGET
+-------------
+Hover transitions: 180–220ms ease
+Drag feedback: immediate (0ms)
+Preset switch: 400ms stagger across N tiles
+Shuffle: 500ms spring ease
+Export progress: 800ms shimmer
+================================================================================
+-->
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Designer Wallcoverings — Interior Mood Board Studio</title>
+<style>
+/* ============================================================
+   DESIGN TOKENS
+   ============================================================ */
+:root {
+  --bg:            #0d0b09;
+  --bg-raised:     #161310;
+  --bg-canvas:     #f4f1ea;
+  --ink:           #1c1a17;
+  --ink-inv:       #e8e3da;
+  --ink-dim:       #9a9184;
+  --accent:        #c9a961;
+  --accent-dim:    #7a6538;
+  --accent-pale:   rgba(201,169,97,0.10);
+  --line:          rgba(201,169,97,0.20);
+  --line-canvas:   rgba(160,148,128,0.22);
+  --shadow-tile:   0 4px 20px rgba(0,0,0,0.14);
+  --shadow-hover:  0 10px 32px rgba(0,0,0,0.22);
+  --shadow-drag:   0 20px 56px rgba(0,0,0,0.38);
+  --serif:         'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif;
+  --sans:          -apple-system,'Helvetica Neue',Arial,sans-serif;
+  --ease:          cubic-bezier(0.25,0.46,0.45,0.94);
+  --ease-spring:   cubic-bezier(0.34,1.56,0.64,1);
+  --radius:        3px;
+}
+
+/* ============================================================
+   RESET & BASE
+   ============================================================ */
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+html { font-size: 16px; -webkit-font-smoothing: antialiased; }
+body {
+  background: var(--bg);
+  color: var(--ink-inv);
+  font-family: var(--sans);
+  min-height: 100vh;
+  overflow-x: hidden;
+}
+button { cursor: pointer; border: none; background: none; font-family: inherit; }
+svg { display: block; }
+
+/* ============================================================
+   HEADER
+   ============================================================ */
+header {
+  position: relative;
+  padding: 12px 48px;
+  background: var(--bg-raised);
+  border-bottom: 1px solid var(--line);
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 24px;
+  min-height: 80px;
+}
+
+.header-lockup {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+}
+
+.wordmark {
+  font-family: var(--serif);
+  font-size: 42px;
+  font-weight: 600;
+  letter-spacing: .18em;
+  text-transform: uppercase;
+  color: var(--ink);
+  background: var(--accent);
+  padding: 4px 20px 4px 0;
+  margin-left: -20px;
+  padding-left: 20px;
+  line-height: 1.05;
+  white-space: nowrap;
+}
+
+.header-sub {
+  font-family: var(--sans);
+  font-size: 10px;
+  letter-spacing: .14em;
+  text-transform: uppercase;
+  color: var(--accent);
+  padding-left: 1px;
+}
+
+.header-actions {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.btn {
+  display: inline-flex;
+  align-items: center;
+  gap: 7px;
+  font-family: var(--sans);
+  font-size: 11px;
+  letter-spacing: .10em;
+  text-transform: uppercase;
+  padding: 9px 18px;
+  border-radius: var(--radius);
+  transition: background 180ms var(--ease), color 180ms var(--ease), transform 120ms var(--ease), box-shadow 180ms var(--ease);
+  white-space: nowrap;
+}
+.btn:active { transform: translateY(1px); }
+
+.btn-ghost {
+  color: var(--ink-dim);
+  border: 1px solid rgba(201,169,97,0.25);
+}
+.btn-ghost:hover {
+  color: var(--ink-inv);
+  border-color: rgba(201,169,97,0.50);
+  background: rgba(201,169,97,0.06);
+}
+
+.btn-accent {
+  background: var(--accent);
+  color: var(--ink);
+  font-weight: 600;
+}
+.btn-accent:hover {
+  background: #d4b872;
+  box-shadow: 0 4px 20px rgba(201,169,97,0.35);
+  transform: translateY(-1px);
+}
+
+.btn-primary {
+  background: var(--ink-inv);
+  color: var(--ink);
+  font-weight: 600;
+}
+.btn-primary:hover {
+  background: #fff;
+  box-shadow: 0 4px 20px rgba(255,255,255,0.15);
+  transform: translateY(-1px);
+}
+
+/* ============================================================
+   STYLE PRESET RAIL
+   ============================================================ */
+.preset-rail {
+  padding: 16px 48px;
+  background: var(--bg);
+  border-bottom: 1px solid var(--line);
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  flex-wrap: wrap;
+}
+
+.preset-label {
+  font-size: 10px;
+  letter-spacing: .14em;
+  text-transform: uppercase;
+  color: var(--ink-dim);
+  margin-right: 8px;
+  white-space: nowrap;
+}
+
+.preset-pill {
+  font-family: var(--sans);
+  font-size: 11px;
+  letter-spacing: .08em;
+  text-transform: uppercase;
+  padding: 7px 16px;
+  border-radius: 100px;
+  border: 1px solid rgba(201,169,97,0.22);
+  color: var(--ink-dim);
+  transition: all 220ms var(--ease);
+  white-space: nowrap;
+}
+.preset-pill:hover {
+  border-color: rgba(201,169,97,0.55);
+  color: var(--ink-inv);
+  background: rgba(201,169,97,0.07);
+}
+.preset-pill.active {
+  background: var(--accent);
+  border-color: var(--accent);
+  color: var(--ink);
+  font-weight: 600;
+}
+
+/* ============================================================
+   STUDIO LAYOUT
+   ============================================================ */
+.studio {
+  display: grid;
+  grid-template-columns: 1fr 240px;
+  height: calc(100vh - var(--chrome-height, 130px));
+  min-height: 560px;
+}
+
+/* ============================================================
+   CANVAS
+   ============================================================ */
+.canvas-wrap {
+  position: relative;
+  overflow: hidden;
+  background: var(--bg-canvas);
+}
+
+/* Subtle grid */
+.canvas-wrap::before {
+  content: '';
+  position: absolute;
+  inset: 0;
+  background-image:
+    linear-gradient(var(--line-canvas) 1px, transparent 1px),
+    linear-gradient(90deg, var(--line-canvas) 1px, transparent 1px);
+  background-size: 20px 20px;
+  pointer-events: none;
+  z-index: 0;
+}
+
+.canvas-inner {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  z-index: 1;
+}
+
+/* ============================================================
+   DRAGGABLE TILES (shared base)
+   ============================================================ */
+.tile {
+  position: absolute;
+  cursor: grab;
+  user-select: none;
+  border-radius: 2px;
+  transition:
+    box-shadow 200ms var(--ease),
+    transform 200ms var(--ease);
+  box-shadow: var(--shadow-tile);
+  will-change: transform;
+  touch-action: none;
+}
+.tile:hover {
+  transform: translateY(-4px) scale(1.01);
+  box-shadow: var(--shadow-hover);
+  z-index: 50;
+}
+.tile.dragging {
+  cursor: grabbing;
+  transform: scale(1.04) translateY(-6px);
+  box-shadow: var(--shadow-drag);
+  z-index: 999 !important;
+  transition: box-shadow 100ms, transform 100ms;
+}
+.tile.animating {
+  transition: left 500ms var(--ease-spring), top 500ms var(--ease-spring), opacity 400ms var(--ease);
+}
+.tile.fading-out { opacity: 0; }
+.tile.fading-in  { opacity: 1; }
+
+/* Tile label strip */
+.tile-label {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  padding: 5px 8px 6px;
+  font-family: var(--sans);
+  font-size: 9px;
+  letter-spacing: .09em;
+  text-transform: uppercase;
+  font-weight: 600;
+  pointer-events: none;
+  line-height: 1.2;
+}
+
+/* ============================================================
+   SWATCH TILES (160×160)
+   ============================================================ */
+.swatch-tile {
+  width: 160px;
+  height: 160px;
+  overflow: hidden;
+}
+
+.swatch-tile .tile-label {
+  background: rgba(0,0,0,0.45);
+  color: rgba(255,255,255,0.90);
+  backdrop-filter: blur(4px);
+}
+
+.swatch-svg {
+  width: 100%;
+  height: 100%;
+}
+
+/* ============================================================
+   COLOR CHIP TILES
+   ============================================================ */
+.color-chip {
+  width: 80px;
+  height: 110px;
+  border-radius: 4px;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+.chip-swatch {
+  flex: 1;
+}
+.chip-info {
+  padding: 6px 7px 7px;
+  background: #fff;
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+}
+.chip-name {
+  font-family: var(--sans);
+  font-size: 8px;
+  letter-spacing: .06em;
+  text-transform: uppercase;
+  font-weight: 700;
+  color: var(--ink);
+  line-height: 1.2;
+}
+.chip-hex {
+  font-family: var(--sans);
+  font-size: 7px;
+  color: #888;
+  letter-spacing: .04em;
+}
+
+/* ============================================================
+   TYPE PAIRING TILE
+   ============================================================ */
+.type-tile {
+  width: 220px;
+  height: 130px;
+  background: #fff;
+  padding: 16px 18px 14px;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  border-left: 3px solid var(--accent);
+}
+.type-tile-serif {
+  font-family: var(--serif);
+  font-size: 22px;
+  font-weight: 600;
+  color: var(--ink);
+  line-height: 1.1;
+  letter-spacing: .02em;
+}
+.type-tile-sans {
+  font-family: var(--sans);
+  font-size: 9.5px;
+  color: #666;
+  letter-spacing: .06em;
+  line-height: 1.5;
+}
+.type-tile-label {
+  position: absolute;
+  top: 7px; right: 8px;
+  font-size: 7px;
+  letter-spacing: .08em;
+  text-transform: uppercase;
+  color: #bbb;
+}
+
+/* ============================================================
+   MATERIAL CHIP TILES
+   ============================================================ */
+.material-chip {
+  width: 100px;
+  height: 56px;
+  border-radius: 4px;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 0 12px;
+  overflow: hidden;
+}
+.material-chip .mat-icon {
+  width: 28px;
+  height: 28px;
+  border-radius: 50%;
+  flex-shrink: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.material-chip .mat-label {
+  font-family: var(--sans);
+  font-size: 9px;
+  letter-spacing: .09em;
+  text-transform: uppercase;
+  font-weight: 700;
+  line-height: 1.2;
+}
+
+/* ============================================================
+   SIDEBAR
+   ============================================================ */
+.sidebar {
+  background: var(--bg-raised);
+  border-left: 1px solid var(--line);
+  overflow-y: auto;
+  display: flex;
+  flex-direction: column;
+}
+
+.sidebar-section {
+  padding: 20px 20px 0;
+  border-bottom: 1px solid var(--line);
+  padding-bottom: 20px;
+}
+.sidebar-section:last-child {
+  border-bottom: none;
+}
+
+.sidebar-heading {
+  font-size: 9px;
+  letter-spacing: .14em;
+  text-transform: uppercase;
+  color: var(--accent);
+  margin-bottom: 14px;
+  font-weight: 600;
+}
+
+.sidebar-btn {
+  display: flex;
+  align-items: center;
+  gap: 9px;
+  width: 100%;
+  padding: 9px 12px;
+  margin-bottom: 6px;
+  border-radius: var(--radius);
+  font-family: var(--sans);
+  font-size: 11px;
+  letter-spacing: .07em;
+  text-transform: uppercase;
+  color: var(--ink-dim);
+  border: 1px solid rgba(201,169,97,0.15);
+  background: transparent;
+  transition: all 180ms var(--ease);
+  text-align: left;
+}
+.sidebar-btn:hover {
+  color: var(--ink-inv);
+  border-color: rgba(201,169,97,0.40);
+  background: rgba(201,169,97,0.06);
+}
+.sidebar-btn:last-child { margin-bottom: 0; }
+
+.sidebar-btn svg {
+  flex-shrink: 0;
+  opacity: 0.7;
+}
+
+/* Style preview thumbnail */
+.style-thumb {
+  width: 100%;
+  aspect-ratio: 4/3;
+  border-radius: 2px;
+  margin-bottom: 10px;
+  overflow: hidden;
+  border: 1px solid var(--line);
+}
+
+/* Color palette preview row */
+.palette-strip {
+  display: flex;
+  height: 28px;
+  border-radius: 2px;
+  overflow: hidden;
+  margin-bottom: 6px;
+  border: 1px solid var(--line);
+}
+.palette-strip-swatch {
+  flex: 1;
+}
+
+.palette-names {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 4px;
+  margin-bottom: 6px;
+}
+.palette-name-chip {
+  font-size: 8px;
+  letter-spacing: .06em;
+  text-transform: uppercase;
+  color: var(--ink-dim);
+  background: rgba(255,255,255,0.05);
+  border-radius: 2px;
+  padding: 2px 6px;
+}
+
+/* ============================================================
+   EXPORT OVERLAY
+   ============================================================ */
+.overlay {
+  display: none;
+  position: fixed;
+  inset: 0;
+  background: rgba(13,11,9,0.92);
+  z-index: 10000;
+  align-items: center;
+  justify-content: center;
+  backdrop-filter: blur(8px);
+}
+.overlay.open { display: flex; }
+
+.overlay-card {
+  background: var(--bg-raised);
+  border: 1px solid var(--line);
+  border-radius: 4px;
+  padding: 32px;
+  max-width: 560px;
+  width: 90%;
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+  box-shadow: 0 32px 96px rgba(0,0,0,0.7);
+}
+
+.overlay-title {
+  font-family: var(--serif);
+  font-size: 24px;
+  font-weight: 600;
+  color: var(--ink-inv);
+  letter-spacing: .04em;
+}
+
+.overlay-body {
+  font-size: 13px;
+  color: var(--ink-dim);
+  line-height: 1.6;
+}
+
+.overlay-canvas-wrap {
+  border: 1px solid var(--line);
+  border-radius: 2px;
+  overflow: hidden;
+  line-height: 0;
+}
+#exportCanvas {
+  width: 100%;
+  height: auto;
+  display: block;
+}
+
+.overlay-actions {
+  display: flex;
+  gap: 10px;
+  justify-content: flex-end;
+}
+
+/* ============================================================
+   TOAST NOTIFICATION
+   ============================================================ */
+.toast {
+  position: fixed;
+  bottom: 32px;
+  left: 50%;
+  transform: translateX(-50%) translateY(80px);
+  background: var(--accent);
+  color: var(--ink);
+  font-family: var(--sans);
+  font-size: 11px;
+  letter-spacing: .08em;
+  text-transform: uppercase;
+  font-weight: 700;
+  padding: 10px 22px;
+  border-radius: 100px;
+  z-index: 99999;
+  transition: transform 320ms var(--ease-spring), opacity 320ms var(--ease);
+  opacity: 0;
+  pointer-events: none;
+}
+.toast.show {
+  transform: translateX(-50%) translateY(0);
+  opacity: 1;
+}
+
+/* ============================================================
+   LOADING SHIMMER on EXPORT BTN
+   ============================================================ */
+@keyframes shimmer {
+  0% { background-position: -200% center; }
+  100% { background-position: 200% center; }
+}
+.btn-shimmer {
+  background: linear-gradient(90deg, var(--accent) 25%, #e8c87a 50%, var(--accent) 75%) !important;
+  background-size: 200% auto !important;
+  animation: shimmer 0.8s linear !important;
+}
+
+/* ============================================================
+   RESPONSIVE ADJUSTMENTS
+   ============================================================ */
+@media (max-width: 900px) {
+  .studio { grid-template-columns: 1fr; }
+  .sidebar { display: none; }
+  header { padding: 0 24px; }
+  .preset-rail { padding: 12px 24px; }
+}
+
+/* ============================================================
+   SCROLLBAR STYLING
+   ============================================================ */
+.sidebar::-webkit-scrollbar { width: 4px; }
+.sidebar::-webkit-scrollbar-track { background: transparent; }
+.sidebar::-webkit-scrollbar-thumb { background: var(--line); border-radius: 2px; }
+
+/* ============================================================
+   TILE SELECTED STATE
+   ============================================================ */
+.tile.selected {
+  outline: 2px solid var(--accent);
+  outline-offset: 2px;
+}
+
+</style>
+</head>
+<body>
+
+<!-- HEADER -->
+<header>
+  <div class="header-lockup">
+    <div class="wordmark">Designer Wallcoverings</div>
+    <div class="header-sub">Interior Mood Board Studio</div>
+  </div>
+  <div class="header-actions">
+    <button class="btn btn-ghost" id="btnShuffle">
+      <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
+        <path d="M1 4h8.5M9.5 4l-2-2M9.5 4l-2 2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
+        <path d="M13 10H4.5M4.5 10l2-2M4.5 10l2 2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
+        <path d="M1 10h1.5M11.5 4H13" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
+      </svg>
+      Shuffle
+    </button>
+    <button class="btn btn-ghost" id="btnClear">
+      <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
+        <path d="M2 2l10 10M12 2L2 12" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
+      </svg>
+      Clear
+    </button>
+    <button class="btn btn-accent" id="btnExport">
+      <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
+        <path d="M7 1v8M4 6l3 3 3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
+        <path d="M2 10v2h10v-2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
+      </svg>
+      Export Board
+    </button>
+  </div>
+</header>
+
+<!-- PRESET RAIL -->
+<div class="preset-rail">
+  <span class="preset-label">Style</span>
+  <button class="preset-pill active" data-preset="grandmillennial">Grandmillennial</button>
+  <button class="preset-pill" data-preset="coastal">Coastal</button>
+  <button class="preset-pill" data-preset="moody">Moody Maximalist</button>
+  <button class="preset-pill" data-preset="minimal">Modern Minimalist</button>
+  <button class="preset-pill" data-preset="deco">Art Deco</button>
+</div>
+
+<!-- STUDIO BODY -->
+<div class="studio">
+  <!-- CANVAS -->
+  <div class="canvas-wrap" id="canvasWrap">
+    <div class="canvas-inner" id="canvasInner"></div>
+  </div>
+
+  <!-- SIDEBAR -->
+  <aside class="sidebar">
+
+    <!-- Style Preview -->
+    <div class="sidebar-section">
+      <div class="sidebar-heading">Active Style</div>
+      <div class="style-thumb" id="styleThumb"></div>
+      <div class="sidebar-heading" style="margin-bottom:8px;">Palette</div>
+      <div class="palette-strip" id="paletteSidebarStrip"></div>
+      <div class="palette-names" id="paletteSidebarNames"></div>
+    </div>
+
+    <!-- Board Actions -->
+    <div class="sidebar-section">
+      <div class="sidebar-heading">Board Actions</div>
+      <button class="sidebar-btn" id="sidebarShuffle">
+        <svg width="13" height="13" viewBox="0 0 14 14" fill="none"><path d="M1 4h8.5M9.5 4l-2-2M9.5 4l-2 2M13 10H4.5M4.5 10l2-2M4.5 10l2 2M1 10h1.5M11.5 4H13" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
+        Recompose Board
+      </button>
+      <button class="sidebar-btn" id="sidebarReset">
+        <svg width="13" height="13" viewBox="0 0 14 14" fill="none"><path d="M7 2a5 5 0 1 0 4.33 2.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><path d="M11 1v3.5H7.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
+        Reset Layout
+      </button>
+      <button class="sidebar-btn" id="sidebarFront">
+        <svg width="13" height="13" viewBox="0 0 14 14" fill="none"><rect x="4" y="4" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="2" y="2" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1.2"/></svg>
+        Bring to Front
+      </button>
+      <button class="sidebar-btn" id="sidebarBack">
+        <svg width="13" height="13" viewBox="0 0 14 14" fill="none"><rect x="2" y="2" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="4" y="4" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1.2"/></svg>
+        Send to Back
+      </button>
+      <button class="sidebar-btn" id="sidebarDelete" style="color:#c1623f;border-color:rgba(193,98,63,0.25);">
+        <svg width="13" height="13" viewBox="0 0 14 14" fill="none"><path d="M2 3.5h10M5 3.5V2h4v1.5M5.5 6v4M8.5 6v4M3 3.5l.7 8h6.6l.7-8" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
+        Delete Selected
+      </button>
+    </div>
+
+    <!-- Tip -->
+    <div class="sidebar-section" style="margin-top:auto;padding-bottom:24px;">
+      <div class="sidebar-heading">Tips</div>
+      <p style="font-size:10px;line-height:1.7;color:var(--ink-dim);">
+        Drag swatches to compose.<br>
+        Click a tile to select it.<br>
+        Switch style presets to<br>
+        repopulate the palette.<br>
+        Export at 1080&times;1350.
+      </p>
+    </div>
+
+  </aside>
+</div>
+
+<!-- EXPORT OVERLAY -->
+<div class="overlay" id="exportOverlay" role="dialog" aria-modal="true" aria-label="Export Mood Board">
+  <div class="overlay-card">
+    <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:16px;">
+      <div class="overlay-title">Export Mood Board</div>
+      <button class="btn btn-ghost" id="btnCloseOverlay" style="padding:6px 10px;min-width:0;">&#x2715;</button>
+    </div>
+    <div class="overlay-body">
+      Your board rendered as a 1080&times;1350 portrait card — ready for Instagram, Pinterest, or client decks.
+    </div>
+    <div class="overlay-canvas-wrap">
+      <canvas id="exportCanvas" width="1080" height="1350"></canvas>
+    </div>
+    <div class="overlay-actions">
+      <button class="btn btn-ghost" id="btnCloseOverlay2">Cancel</button>
+      <button class="btn btn-accent" id="btnDownload">
+        <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 1v8M4 6l3 3 3-3M2 10v2h10v-2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
+        Download PNG
+      </button>
+    </div>
+  </div>
+</div>
+
+<!-- TOAST -->
+<div class="toast" id="toast" role="status" aria-live="polite"></div>
+
+<script>
+/* ============================================================
+   STYLE PRESET DATA
+   ============================================================ */
+const PRESETS = {
+  grandmillennial: {
+    label: 'Grandmillennial',
+    palette: [
+      { name: 'Antique Rose',  hex: '#c9917e' },
+      { name: 'Ivory Cream',   hex: '#f0e8d8' },
+      { name: 'Dusty Gold',    hex: '#c9a961' },
+      { name: 'Sage Velvet',   hex: '#7a9472' },
+      { name: 'Blush Linen',   hex: '#e0c8bc' },
+      { name: 'Umber',         hex: '#7a5a3c' },
+    ],
+    swatches: ['chinoiserie','damask','grasscloth','raffia','silk','linen'],
+    thumbColors: ['#c9917e','#f0e8d8','#c9a961','#7a9472','#e0c8bc'],
+  },
+  coastal: {
+    label: 'Coastal',
+    palette: [
+      { name: 'Sea Salt',     hex: '#dde8e5' },
+      { name: 'Celadon',      hex: '#a8c4b0' },
+      { name: 'White Oak',    hex: '#d4c4a4' },
+      { name: 'Dune',         hex: '#d9ccb0' },
+      { name: 'Horizon',      hex: '#7fa8b8' },
+      { name: 'Oatmeal',      hex: '#e8dccb' },
+    ],
+    swatches: ['linen','raffia','grasscloth','silk','damask','chinoiserie'],
+    thumbColors: ['#dde8e5','#a8c4b0','#d4c4a4','#d9ccb0','#7fa8b8'],
+  },
+  moody: {
+    label: 'Moody Maximalist',
+    palette: [
+      { name: 'Ink',          hex: '#1c2b3a' },
+      { name: 'Jewel Plum',   hex: '#5c3a5e' },
+      { name: 'Forest Night', hex: '#2a3d2e' },
+      { name: 'Antique Brass',hex: '#c9a961' },
+      { name: 'Bordeaux',     hex: '#7a2a2a' },
+      { name: 'Midnight',     hex: '#1a1a2e' },
+    ],
+    swatches: ['damask','silk','chinoiserie','raffia','grasscloth','linen'],
+    thumbColors: ['#1c2b3a','#5c3a5e','#2a3d2e','#c9a961','#7a2a2a'],
+  },
+  minimal: {
+    label: 'Modern Minimalist',
+    palette: [
+      { name: 'Bone',         hex: '#e8e2d8' },
+      { name: 'Mist',         hex: '#d4d0c8' },
+      { name: 'Warm White',   hex: '#f5f2ec' },
+      { name: 'Concrete',     hex: '#b8b4aa' },
+      { name: 'Pale Stone',   hex: '#c8c0b0' },
+      { name: 'Flax',         hex: '#d8cc9c' },
+    ],
+    swatches: ['linen','silk','grasscloth','raffia','damask','chinoiserie'],
+    thumbColors: ['#e8e2d8','#d4d0c8','#f5f2ec','#b8b4aa','#c8c0b0'],
+  },
+  deco: {
+    label: 'Art Deco',
+    palette: [
+      { name: 'Lacquer Black',hex: '#0a0a0a' },
+      { name: 'Aged Gold',    hex: '#c9a961' },
+      { name: 'Ivory',        hex: '#f0ead8' },
+      { name: 'Jade',         hex: '#4a7a6a' },
+      { name: 'Deep Navy',    hex: '#1a2440' },
+      { name: 'Champagne',    hex: '#d8c88a' },
+    ],
+    swatches: ['silk','damask','chinoiserie','grasscloth','linen','raffia'],
+    thumbColors: ['#0a0a0a','#c9a961','#f0ead8','#4a7a6a','#1a2440'],
+  },
+};
+
+/* ============================================================
+   TEXTURE SVG GENERATORS
+   Each returns an SVG string for a 160x160 swatch
+   ============================================================ */
+function svgGrasscloth(palette) {
+  const bg  = palette[4]?.hex || '#d4c4a4';
+  const fg  = palette[5]?.hex || '#b09a7a';
+  const lines = [];
+  // Horizontal fiber lines at varying weights
+  for (let y = 2; y < 160; y += 4) {
+    const op = 0.25 + Math.random() * 0.35;
+    const w  = 0.5 + Math.random() * 0.8;
+    lines.push(`<line x1="0" y1="${y}" x2="160" y2="${y}" stroke="${fg}" stroke-width="${w.toFixed(1)}" stroke-opacity="${op.toFixed(2)}"/>`);
+    if (y % 16 === 2) {
+      lines.push(`<line x1="0" y1="${y+1}" x2="160" y2="${y+1}" stroke="${fg}" stroke-width="0.4" stroke-opacity="0.12"/>`);
+    }
+  }
+  // Vertical warp threads
+  for (let x = 0; x < 160; x += 6) {
+    const op = 0.08 + Math.random() * 0.10;
+    lines.push(`<line x1="${x}" y1="0" x2="${x}" y2="160" stroke="${fg}" stroke-width="0.6" stroke-opacity="${op.toFixed(2)}"/>`);
+  }
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" viewBox="0 0 160 160">
+    <rect width="160" height="160" fill="${bg}"/>
+    ${lines.join('')}
+  </svg>`;
+}
+
+function svgDamask(palette) {
+  const bg   = palette[1]?.hex || '#f0e8d8';
+  const fg   = palette[0]?.hex || '#c9917e';
+  const acc  = palette[2]?.hex || '#c9a961';
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" viewBox="0 0 160 160">
+    <rect width="160" height="160" fill="${bg}"/>
+    <defs>
+      <pattern id="dmpat" x="0" y="0" width="80" height="80" patternUnits="userSpaceOnUse">
+        <!-- Central medallion -->
+        <ellipse cx="40" cy="40" rx="18" ry="26" fill="none" stroke="${fg}" stroke-width="0.8" stroke-opacity="0.45"/>
+        <ellipse cx="40" cy="40" rx="10" ry="15" fill="none" stroke="${fg}" stroke-width="0.6" stroke-opacity="0.35"/>
+        <!-- Four-point petals -->
+        <path d="M40 14 C36 20 34 28 40 34 C46 28 44 20 40 14Z" fill="${fg}" fill-opacity="0.20"/>
+        <path d="M40 46 C36 52 34 60 40 66 C46 60 44 52 40 46Z" fill="${fg}" fill-opacity="0.20"/>
+        <path d="M14 40 C20 36 28 34 34 40 C28 46 20 44 14 40Z" fill="${fg}" fill-opacity="0.20"/>
+        <path d="M46 40 C52 36 60 34 66 40 C60 46 52 44 46 40Z" fill="${fg}" fill-opacity="0.20"/>
+        <!-- Diagonal acanthus -->
+        <path d="M20 20 C22 26 30 30 28 36 C26 30 18 28 20 20Z" fill="${acc}" fill-opacity="0.12"/>
+        <path d="M60 20 C58 26 50 30 52 36 C54 30 62 28 60 20Z" fill="${acc}" fill-opacity="0.12"/>
+        <path d="M20 60 C22 54 30 50 28 44 C26 50 18 52 20 60Z" fill="${acc}" fill-opacity="0.12"/>
+        <path d="M60 60 C58 54 50 50 52 44 C54 50 62 52 60 60Z" fill="${acc}" fill-opacity="0.12"/>
+        <!-- Corner dots -->
+        <circle cx="0" cy="0" r="3" fill="${fg}" fill-opacity="0.15"/>
+        <circle cx="80" cy="0" r="3" fill="${fg}" fill-opacity="0.15"/>
+        <circle cx="0" cy="80" r="3" fill="${fg}" fill-opacity="0.15"/>
+        <circle cx="80" cy="80" r="3" fill="${fg}" fill-opacity="0.15"/>
+        <!-- Hairline grid -->
+        <line x1="40" y1="0" x2="40" y2="80" stroke="${fg}" stroke-width="0.3" stroke-opacity="0.15"/>
+        <line x1="0" y1="40" x2="80" y2="40" stroke="${fg}" stroke-width="0.3" stroke-opacity="0.15"/>
+      </pattern>
+    </defs>
+    <rect width="160" height="160" fill="url(#dmpat)"/>
+  </svg>`;
+}
+
+function svgSilkStripe(palette) {
+  const cols = [
+    palette[0]?.hex || '#c9a961',
+    palette[1]?.hex || '#f0e8d8',
+    palette[2]?.hex || '#d4c4a4',
+    palette[0]?.hex || '#c9a961',
+    palette[4]?.hex || '#e0c8bc',
+  ];
+  const widths = [8, 20, 6, 14, 4, 20, 6, 14, 8];
+  const stripes = [];
+  let x = 0;
+  const total = widths.reduce((a,b) => a+b, 0); // 100
+  const reps = Math.ceil(160 / total) + 1;
+  for (let r = 0; r < reps; r++) {
+    let cx = x + r * total;
+    widths.forEach((w, i) => {
+      const c = cols[i % cols.length];
+      // Alternate satin/matte effect via slight opacity variation
+      const op = i % 2 === 0 ? 1 : 0.75;
+      stripes.push(`<rect x="${cx}" y="0" width="${w}" height="160" fill="${c}" fill-opacity="${op}"/>`);
+      cx += w;
+    });
+  }
+  // Sheen overlay
+  const sheen = [];
+  for (let s = 0; s < 160; s += 28) {
+    sheen.push(`<rect x="${s}" y="0" width="2" height="160" fill="white" fill-opacity="0.06"/>`);
+  }
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" viewBox="0 0 160 160">
+    <rect width="160" height="160" fill="${cols[1]}"/>
+    ${stripes.join('')}
+    ${sheen.join('')}
+  </svg>`;
+}
+
+function svgLinen(palette) {
+  const bg = palette[1]?.hex || '#e8dccb';
+  const fg = palette[5]?.hex || '#c4a878';
+  const weave = [];
+  // Warp threads
+  for (let x = 0; x < 160; x += 5) {
+    weave.push(`<line x1="${x}" y1="0" x2="${x}" y2="160" stroke="${fg}" stroke-width="1.2" stroke-opacity="0.28"/>`);
+  }
+  // Weft threads
+  for (let y = 0; y < 160; y += 5) {
+    weave.push(`<line x1="0" y1="${y}" x2="160" y2="${y}" stroke="${fg}" stroke-width="1" stroke-opacity="0.22"/>`);
+  }
+  // Intersection dots (subtle nub)
+  for (let x = 0; x < 160; x += 10) {
+    for (let y = 0; y < 160; y += 10) {
+      weave.push(`<circle cx="${x}" cy="${y}" r="0.7" fill="${fg}" fill-opacity="0.25"/>`);
+    }
+  }
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" viewBox="0 0 160 160">
+    <rect width="160" height="160" fill="${bg}"/>
+    ${weave.join('')}
+  </svg>`;
+}
+
+function svgRaffia(palette) {
+  const bg  = palette[3]?.hex || '#d9ccb0';
+  const fg  = palette[5]?.hex || '#a08060';
+  const diag = [];
+  // Diagonal bast-fiber weave — NE direction
+  for (let d = -160; d < 320; d += 7) {
+    const op = 0.18 + (Math.abs(d) % 30) / 120;
+    diag.push(`<line x1="${d}" y1="0" x2="${d+160}" y2="160" stroke="${fg}" stroke-width="1.5" stroke-opacity="${op.toFixed(2)}"/>`);
+  }
+  // SW direction overlay
+  for (let d = -160; d < 320; d += 14) {
+    diag.push(`<line x1="${d+160}" y1="0" x2="${d}" y2="160" stroke="${fg}" stroke-width="0.6" stroke-opacity="0.12"/>`);
+  }
+  // Occasional thick bundle
+  for (let d = -160; d < 320; d += 42) {
+    diag.push(`<line x1="${d}" y1="0" x2="${d+160}" y2="160" stroke="${fg}" stroke-width="2.5" stroke-opacity="0.18"/>`);
+  }
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" viewBox="0 0 160 160">
+    <rect width="160" height="160" fill="${bg}"/>
+    ${diag.join('')}
+  </svg>`;
+}
+
+function svgChinoiserie(palette) {
+  const bg    = palette[1]?.hex || '#f4f0e0';
+  const blue  = palette[4]?.hex || '#4a7a9e';
+  const green = palette[3]?.hex || '#6a9a6a';
+  const blush = palette[0]?.hex || '#d4907a';
+  return `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" viewBox="0 0 160 160">
+    <rect width="160" height="160" fill="${bg}"/>
+    <!-- Main branch -->
+    <path d="M30 140 C40 120 50 100 60 80 C70 60 75 40 80 20" stroke="${blue}" stroke-width="1.8" fill="none" stroke-opacity="0.7"/>
+    <path d="M80 20 C85 35 90 50 110 55" stroke="${blue}" stroke-width="1.4" fill="none" stroke-opacity="0.5"/>
+    <path d="M60 80 C45 70 35 60 20 55" stroke="${blue}" stroke-width="1.2" fill="none" stroke-opacity="0.45"/>
+    <path d="M50 100 C55 90 70 85 85 88" stroke="${blue}" stroke-width="1" fill="none" stroke-opacity="0.4"/>
+    <!-- Blossoms -->
+    <circle cx="80" cy="20" r="6" fill="${blush}" fill-opacity="0.75"/>
+    <circle cx="80" cy="20" r="3" fill="${blush}" fill-opacity="0.9"/>
+    <circle cx="79" cy="17" r="2" fill="white" fill-opacity="0.5"/>
+    <circle cx="110" cy="55" r="5" fill="${blush}" fill-opacity="0.65"/>
+    <circle cx="20" cy="55" r="4" fill="${blush}" fill-opacity="0.60"/>
+    <circle cx="85" cy="88" r="4.5" fill="${blush}" fill-opacity="0.70"/>
+    <!-- Small buds -->
+    <ellipse cx="68" cy="60" rx="3" ry="4.5" fill="${blush}" fill-opacity="0.55" transform="rotate(-20,68,60)"/>
+    <ellipse cx="95" cy="40" rx="2.5" ry="4" fill="${blush}" fill-opacity="0.50" transform="rotate(15,95,40)"/>
+    <!-- Leaves -->
+    <path d="M55 92 C50 85 42 80 38 84 C42 90 50 92 55 92Z" fill="${green}" fill-opacity="0.55"/>
+    <path d="M65 72 C60 65 52 62 50 66 C54 72 62 73 65 72Z" fill="${green}" fill-opacity="0.50"/>
+    <path d="M88 30 C94 24 102 24 102 28 C96 34 88 32 88 30Z" fill="${green}" fill-opacity="0.48"/>
+    <path d="M100 60 C106 54 114 56 112 60 C106 64 100 62 100 60Z" fill="${green}" fill-opacity="0.45"/>
+    <!-- Second branch (right side) -->
+    <path d="M140 130 C130 110 118 90 115 70 C112 50 118 30 120 15" stroke="${blue}" stroke-width="1.5" fill="none" stroke-opacity="0.55"/>
+    <path d="M115 70 C128 65 138 60 148 62" stroke="${blue}" stroke-width="1" fill="none" stroke-opacity="0.40"/>
+    <circle cx="120" cy="15" r="5" fill="${blush}" fill-opacity="0.65"/>
+    <circle cx="148" cy="62" r="4" fill="${blush}" fill-opacity="0.55"/>
+    <path d="M123 50 C130 44 138 46 136 50 C130 54 123 52 123 50Z" fill="${green}" fill-opacity="0.42"/>
+    <!-- Small birds (abstract) -->
+    <path d="M70 130 C72 128 76 128 78 130 C76 132 72 132 70 130Z" fill="${blue}" fill-opacity="0.35"/>
+    <path d="M74 129 L74 126" stroke="${blue}" stroke-width="0.8" stroke-opacity="0.4"/>
+    <!-- Decorative dots -->
+    <circle cx="40" cy="30" r="2" fill="${blue}" fill-opacity="0.20"/>
+    <circle cx="145" cy="100" r="1.5" fill="${blue}" fill-opacity="0.18"/>
+    <circle cx="10" cy="100" r="1.5" fill="${blue}" fill-opacity="0.15"/>
+  </svg>`;
+}
+
+/* ============================================================
+   SWATCH TEXTURE ROUTER
+   ============================================================ */
+const SWATCH_LABELS = {
+  grasscloth: 'Grasscloth',
+  damask:     'Damask',
+  silk:       'Silk Stripe',
+  linen:      'Natural Linen',
+  raffia:     'Raffia Weave',
+  chinoiserie:'Chinoiserie',
+};
+
+function buildSwatchSVG(type, palette) {
+  switch(type) {
+    case 'grasscloth':  return svgGrasscloth(palette);
+    case 'damask':      return svgDamask(palette);
+    case 'silk':        return svgSilkStripe(palette);
+    case 'linen':       return svgLinen(palette);
+    case 'raffia':      return svgRaffia(palette);
+    case 'chinoiserie': return svgChinoiserie(palette);
+    default:            return svgLinen(palette);
+  }
+}
+
+/* ============================================================
+   MATERIAL CHIP CONFIGS
+   ============================================================ */
+const MATERIALS = [
+  {
+    name: 'Brass',
+    bg: 'linear-gradient(135deg,#d4b44a 0%,#c9a961 45%,#e0c870 100%)',
+    color: '#3a2e10',
+    iconBg: 'rgba(201,169,97,0.3)',
+    icon: `<svg width="16" height="16" viewBox="0 0 16 16"><circle cx="8" cy="8" r="5" stroke="#7a5a20" stroke-width="1.2" fill="none"/><circle cx="8" cy="8" r="2" fill="#c9a961"/></svg>`
+  },
+  {
+    name: 'Marble',
+    bg: 'linear-gradient(135deg,#f0eee8 0%,#e8e4dc 40%,#f4f2ea 100%)',
+    color: '#2a2824',
+    iconBg: 'rgba(100,90,80,0.15)',
+    icon: `<svg width="16" height="16" viewBox="0 0 16 16"><ellipse cx="8" cy="8" rx="6" ry="5" stroke="#aaa" stroke-width="0.8" fill="none"/><path d="M4 6 C6 8 7 10 10 10" stroke="#bbb" stroke-width="0.7" fill="none"/><path d="M6 4 C8 6 9 8 12 9" stroke="#ccc" stroke-width="0.5" fill="none"/></svg>`
+  },
+  {
+    name: 'White Oak',
+    bg: 'linear-gradient(135deg,#d8c8a4 0%,#c4b48e 50%,#d0c09a 100%)',
+    color: '#3c2e14',
+    iconBg: 'rgba(100,80,40,0.2)',
+    icon: `<svg width="16" height="16" viewBox="0 0 16 16"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#8a6a30" stroke-width="0.8" fill="none"/><line x1="3" y1="5" x2="13" y2="5" stroke="#a08040" stroke-width="0.5"/><line x1="3" y1="8" x2="13" y2="8" stroke="#a08040" stroke-width="0.5"/><line x1="3" y1="11" x2="13" y2="11" stroke="#a08040" stroke-width="0.5"/></svg>`
+  },
+];
+
+/* ============================================================
+   LAYOUT COMPOSER — determines tile positions
+   ============================================================ */
+function composeTilePositions(canvasW, canvasH, preset) {
+  // Returns array of {x, y, z} for: 6 swatches, 6 chips, 1 type, 3 materials
+  const pad = 24;
+  const sw = 160, sh = 160; // swatch
+  const cw = 80,  ch = 110; // chip
+  const tw = 220, th = 130; // type
+  const mw = 100, mh = 56;  // material
+
+  // Named layouts per preset — curated, intentional overlap
+  const layouts = {
+    grandmillennial: [
+      // swatches (6)
+      { x: 48,  y: 40  }, { x: 228, y: 24  }, { x: 410, y: 56  },
+      { x: 68,  y: 224 }, { x: 248, y: 210 }, { x: 430, y: 230 },
+      // chips (6)
+      { x: 620, y: 40  }, { x: 712, y: 40  }, { x: 804, y: 40  },
+      { x: 620, y: 162 }, { x: 712, y: 162 }, { x: 804, y: 162 },
+      // type tile
+      { x: 580, y: 300 },
+      // materials (3)
+      { x: 48,  y: 420 }, { x: 162, y: 420 }, { x: 276, y: 420 },
+    ],
+    coastal: [
+      { x: 32,  y: 32  }, { x: 212, y: 48  }, { x: 390, y: 28  },
+      { x: 52,  y: 214 }, { x: 232, y: 228 }, { x: 408, y: 210 },
+      { x: 616, y: 48  }, { x: 706, y: 56  }, { x: 796, y: 48  },
+      { x: 616, y: 170 }, { x: 706, y: 170 }, { x: 796, y: 170 },
+      { x: 570, y: 320 },
+      { x: 40,  y: 410 }, { x: 154, y: 418 }, { x: 268, y: 406 },
+    ],
+    moody: [
+      { x: 24,  y: 24  }, { x: 200, y: 40  }, { x: 376, y: 16  },
+      { x: 40,  y: 202 }, { x: 216, y: 218 }, { x: 392, y: 200 },
+      { x: 608, y: 32  }, { x: 702, y: 32  }, { x: 796, y: 32  },
+      { x: 608, y: 156 }, { x: 702, y: 156 }, { x: 796, y: 156 },
+      { x: 564, y: 296 },
+      { x: 24,  y: 400 }, { x: 138, y: 406 }, { x: 252, y: 396 },
+    ],
+    minimal: [
+      { x: 40,  y: 48  }, { x: 228, y: 32  }, { x: 418, y: 52  },
+      { x: 60,  y: 236 }, { x: 250, y: 220 }, { x: 440, y: 238 },
+      { x: 624, y: 52  }, { x: 716, y: 52  }, { x: 808, y: 52  },
+      { x: 624, y: 174 }, { x: 716, y: 174 }, { x: 808, y: 174 },
+      { x: 590, y: 306 },
+      { x: 48,  y: 428 }, { x: 162, y: 428 }, { x: 276, y: 428 },
+    ],
+    deco: [
+      { x: 36,  y: 36  }, { x: 214, y: 20  }, { x: 392, y: 40  },
+      { x: 54,  y: 220 }, { x: 232, y: 208 }, { x: 410, y: 224 },
+      { x: 612, y: 36  }, { x: 704, y: 36  }, { x: 796, y: 36  },
+      { x: 612, y: 158 }, { x: 704, y: 158 }, { x: 796, y: 158 },
+      { x: 574, y: 304 },
+      { x: 36,  y: 414 }, { x: 150, y: 414 }, { x: 264, y: 414 },
+    ],
+  };
+
+  const base = layouts[preset] || layouts.grandmillennial;
+
+  // Scale to actual canvas dimensions (base designed for ~940×500)
+  const scaleX = Math.min(1, (canvasW - pad*2) / 900);
+  const scaleY = Math.min(1, (canvasH - pad*2) / 500);
+
+  return base.map((p, i) => ({
+    x: Math.round(p.x * scaleX + pad * (1 - scaleX)),
+    y: Math.round(p.y * scaleY + pad * (1 - scaleY)),
+    z: i + 1,
+  }));
+}
+
+/* ============================================================
+   CANVAS ROUNDRECT POLYFILL
+   ============================================================ */
+if (!CanvasRenderingContext2D.prototype.roundRect) {
+  CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
+    r = Math.min(r, w/2, h/2);
+    this.beginPath();
+    this.moveTo(x + r, y);
+    this.lineTo(x + w - r, y);
+    this.quadraticCurveTo(x + w, y, x + w, y + r);
+    this.lineTo(x + w, y + h - r);
+    this.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
+    this.lineTo(x + r, y + h);
+    this.quadraticCurveTo(x, y + h, x, y + h - r);
+    this.lineTo(x, y + r);
+    this.quadraticCurveTo(x, y, x + r, y);
+    this.closePath();
+    return this;
+  };
+}
+
+/* ============================================================
+   CHROME HEIGHT — sets CSS var for studio layout
+   ============================================================ */
+function updateChromeHeight() {
+  const header = document.querySelector('header');
+  const rail   = document.querySelector('.preset-rail');
+  if (header && rail) {
+    const h = header.getBoundingClientRect().height + rail.getBoundingClientRect().height;
+    document.documentElement.style.setProperty('--chrome-height', Math.round(h) + 'px');
+  }
+}
+
+/* ============================================================
+   STATE
+   ============================================================ */
+let currentPreset  = 'grandmillennial';
+let selectedTileId = null;
+let tiles          = [];    // array of { id, el, type, data, x, y, z }
+let zCounter       = 100;
+let isDragging     = false;
+
+/* ============================================================
+   TILE FACTORY
+   ============================================================ */
+function createSwatchTile(type, palette, x, y, z, id) {
+  const el = document.createElement('div');
+  el.className = 'tile swatch-tile';
+  el.dataset.id = id;
+  el.style.left = x + 'px';
+  el.style.top  = y + 'px';
+  el.style.zIndex = z;
+
+  const svgStr = buildSwatchSVG(type, palette);
+  el.innerHTML = `${svgStr}<div class="tile-label">${SWATCH_LABELS[type] || type}</div>`;
+  return el;
+}
+
+function createColorChip(colorData, x, y, z, id) {
+  const el = document.createElement('div');
+  el.className = 'tile color-chip';
+  el.dataset.id = id;
+  el.style.left = x + 'px';
+  el.style.top  = y + 'px';
+  el.style.zIndex = z;
+  el.innerHTML = `
+    <div class="chip-swatch" style="background:${colorData.hex};"></div>
+    <div class="chip-info">
+      <div class="chip-name">${colorData.name}</div>
+      <div class="chip-hex">${colorData.hex}</div>
+    </div>
+  `;
+  return el;
+}
+
+function createTypeTile(palette, x, y, z, id) {
+  const el = document.createElement('div');
+  el.className = 'tile type-tile';
+  el.dataset.id = id;
+  el.style.left = x + 'px';
+  el.style.top  = y + 'px';
+  el.style.zIndex = z;
+  el.style.borderLeftColor = palette[2]?.hex || '#c9a961';
+  el.innerHTML = `
+    <div class="type-tile-serif">Living Room</div>
+    <div class="type-tile-sans" style="font-weight:700;letter-spacing:.08em;font-size:8.5px;text-transform:uppercase;color:${palette[2]?.hex||'#c9a961'};margin-bottom:2px;">Cormorant Garamond + Helvetica</div>
+    <div class="type-tile-sans">Wallcovering specification<br>reference for client presentation.</div>
+    <div class="type-tile-label">Type Pairing</div>
+  `;
+  return el;
+}
+
+function createMaterialChip(mat, x, y, z, id) {
+  const el = document.createElement('div');
+  el.className = 'tile material-chip';
+  el.dataset.id = id;
+  el.style.left = x + 'px';
+  el.style.top  = y + 'px';
+  el.style.zIndex = z;
+  el.style.background = mat.bg;
+  el.innerHTML = `
+    <div class="mat-icon" style="background:${mat.iconBg};">${mat.icon}</div>
+    <div class="mat-label" style="color:${mat.color};">${mat.name}</div>
+  `;
+  return el;
+}
+
+/* ============================================================
+   BOARD POPULATION
+   ============================================================ */
+function populateBoard(preset, animate) {
+  const canvas = document.getElementById('canvasInner');
+  const wrap   = document.getElementById('canvasWrap');
+  const cW     = wrap.clientWidth;
+  const cH     = wrap.clientHeight;
+
+  const data  = PRESETS[preset];
+  const positions = composeTilePositions(cW, cH, preset);
+
+  // If animating (preset switch), fade out existing
+  if (animate && tiles.length > 0) {
+    tiles.forEach((t, i) => {
+      t.el.classList.add('animating');
+      setTimeout(() => t.el.classList.add('fading-out'), i * 40);
+    });
+    setTimeout(() => {
+      canvas.innerHTML = '';
+      tiles = [];
+      spawnTiles(data, positions, canvas, true);
+    }, tiles.length * 40 + 350);
+  } else {
+    canvas.innerHTML = '';
+    tiles = [];
+    spawnTiles(data, positions, canvas, animate);
+  }
+
+  updateSidebar(preset);
+}
+
+let tileIdCounter = 0;
+
+function spawnTiles(data, positions, canvas, animate) {
+  const palette = data.palette;
+  const swOrder = data.swatches;
+  let posIdx    = 0;
+
+  // 6 swatch tiles
+  swOrder.forEach((type, i) => {
+    const pos = positions[posIdx++];
+    const id  = 'tile-' + (++tileIdCounter);
+    const el  = createSwatchTile(type, palette, pos.x, pos.y, pos.z, id);
+    if (animate) { el.style.opacity = '0'; }
+    canvas.appendChild(el);
+    attachDragListeners(el);
+    el.addEventListener('mousedown', () => selectTile(id), { passive: true });
+    el.addEventListener('touchstart', () => selectTile(id), { passive: true });
+    tiles.push({ id, el, type: 'swatch', x: pos.x, y: pos.y, z: pos.z });
+    if (animate) setTimeout(() => { el.classList.add('animating'); el.style.opacity = '1'; }, i * 60 + 20);
+  });
+
+  // 6 color chips
+  palette.forEach((c, i) => {
+    const pos = positions[posIdx++];
+    const id  = 'tile-' + (++tileIdCounter);
+    const el  = createColorChip(c, pos.x, pos.y, pos.z, id);
+    if (animate) { el.style.opacity = '0'; }
+    canvas.appendChild(el);
+    attachDragListeners(el);
+    el.addEventListener('mousedown', () => selectTile(id), { passive: true });
+    el.addEventListener('touchstart', () => selectTile(id), { passive: true });
+    tiles.push({ id, el, type: 'chip', x: pos.x, y: pos.y, z: pos.z });
+    if (animate) setTimeout(() => { el.classList.add('animating'); el.style.opacity = '1'; }, (i + 6) * 60 + 20);
+  });
+
+  // Type tile
+  const typePos = positions[posIdx++];
+  const typeId  = 'tile-' + (++tileIdCounter);
+  const typeEl  = createTypeTile(palette, typePos.x, typePos.y, typePos.z, typeId);
+  if (animate) { typeEl.style.opacity = '0'; }
+  canvas.appendChild(typeEl);
+  attachDragListeners(typeEl);
+  typeEl.addEventListener('mousedown', () => selectTile(typeId), { passive: true });
+  typeEl.addEventListener('touchstart', () => selectTile(typeId), { passive: true });
+  tiles.push({ id: typeId, el: typeEl, type: 'type', x: typePos.x, y: typePos.y, z: typePos.z });
+  if (animate) setTimeout(() => { typeEl.classList.add('animating'); typeEl.style.opacity = '1'; }, 12 * 60 + 20);
+
+  // 3 material chips
+  MATERIALS.forEach((mat, i) => {
+    const pos = positions[posIdx++];
+    const id  = 'tile-' + (++tileIdCounter);
+    const el  = createMaterialChip(mat, pos.x, pos.y, pos.z, id);
+    if (animate) { el.style.opacity = '0'; }
+    canvas.appendChild(el);
+    attachDragListeners(el);
+    el.addEventListener('mousedown', () => selectTile(id), { passive: true });
+    el.addEventListener('touchstart', () => selectTile(id), { passive: true });
+    tiles.push({ id, el, type: 'material', x: pos.x, y: pos.y, z: pos.z });
+    if (animate) setTimeout(() => { el.classList.add('animating'); el.style.opacity = '1'; }, (13 + i) * 60 + 20);
+  });
+}
+
+/* ============================================================
+   SELECTION
+   ============================================================ */
+function selectTile(id) {
+  tiles.forEach(t => t.el.classList.remove('selected'));
+  selectedTileId = id;
+  const t = tiles.find(t => t.id === id);
+  if (t) t.el.classList.add('selected');
+}
+
+document.getElementById('canvasWrap').addEventListener('mousedown', (e) => {
+  if (e.target === document.getElementById('canvasWrap') ||
+      e.target === document.getElementById('canvasInner') ||
+      e.target.tagName === 'svg' || e.target.tagName === 'rect' && !e.target.closest('.tile')) {
+    tiles.forEach(t => t.el.classList.remove('selected'));
+    selectedTileId = null;
+  }
+});
+
+/* ============================================================
+   DRAG & DROP
+   ============================================================ */
+function attachDragListeners(el) {
+  let startX, startY, startLeft, startTop;
+
+  function onStart(cx, cy) {
+    isDragging = true;
+    startX    = cx;
+    startY    = cy;
+    startLeft = parseInt(el.style.left) || 0;
+    startTop  = parseInt(el.style.top)  || 0;
+
+    zCounter++;
+    el.style.zIndex = zCounter;
+    const t = tiles.find(t => t.el === el);
+    if (t) t.z = zCounter;
+
+    el.classList.add('dragging');
+    el.classList.remove('animating');
+    el.style.transition = 'none';
+  }
+
+  function onMove(cx, cy) {
+    if (!isDragging) return;
+    const dx = cx - startX;
+    const dy = cy - startY;
+    const newLeft = Math.max(0, startLeft + dx);
+    const newTop  = Math.max(0, startTop  + dy);
+    el.style.left = newLeft + 'px';
+    el.style.top  = newTop  + 'px';
+  }
+
+  function onEnd(cx, cy) {
+    if (!isDragging) return;
+    isDragging = false;
+    el.classList.remove('dragging');
+
+    // Snap to 10px grid
+    let left = parseInt(el.style.left) || 0;
+    let top  = parseInt(el.style.top)  || 0;
+    left = Math.round(left / 10) * 10;
+    top  = Math.round(top  / 10) * 10;
+    el.style.left = left + 'px';
+    el.style.top  = top  + 'px';
+
+    const t = tiles.find(t => t.el === el);
+    if (t) { t.x = left; t.y = top; }
+  }
+
+  // Mouse
+  el.addEventListener('mousedown', (e) => {
+    e.preventDefault();
+    onStart(e.clientX, e.clientY);
+    const moveH = (e) => onMove(e.clientX, e.clientY);
+    const upH   = (e) => { onEnd(e.clientX, e.clientY); window.removeEventListener('mousemove', moveH); window.removeEventListener('mouseup', upH); };
+    window.addEventListener('mousemove', moveH);
+    window.addEventListener('mouseup', upH);
+  });
+
+  // Touch
+  el.addEventListener('touchstart', (e) => {
+    const t = e.touches[0];
+    onStart(t.clientX, t.clientY);
+  }, { passive: true });
+
+  el.addEventListener('touchmove', (e) => {
+    e.preventDefault();
+    const t = e.touches[0];
+    onMove(t.clientX, t.clientY);
+  }, { passive: false });
+
+  el.addEventListener('touchend', (e) => {
+    const t = e.changedTouches[0];
+    onEnd(t.clientX, t.clientY);
+  }, { passive: true });
+}
+
+/* ============================================================
+   SIDEBAR UPDATER
+   ============================================================ */
+function updateSidebar(preset) {
+  const data = PRESETS[preset];
+
+  // Style thumb
+  const thumb = document.getElementById('styleThumb');
+  const cols  = data.thumbColors;
+  thumb.style.background = `linear-gradient(135deg, ${cols.join(', ')})`;
+
+  // Palette strip
+  const strip = document.getElementById('paletteSidebarStrip');
+  strip.innerHTML = data.palette.map(c =>
+    `<div class="palette-strip-swatch" style="background:${c.hex};" title="${c.name}"></div>`
+  ).join('');
+
+  // Palette names
+  const names = document.getElementById('paletteSidebarNames');
+  names.innerHTML = data.palette.map(c =>
+    `<div class="palette-name-chip">${c.name}</div>`
+  ).join('');
+}
+
+/* ============================================================
+   PRESET SWITCHING
+   ============================================================ */
+document.querySelectorAll('.preset-pill').forEach(pill => {
+  pill.addEventListener('click', () => {
+    document.querySelectorAll('.preset-pill').forEach(p => p.classList.remove('active'));
+    pill.classList.add('active');
+    currentPreset = pill.dataset.preset;
+    populateBoard(currentPreset, true);
+  });
+});
+
+/* ============================================================
+   SHUFFLE / RECOMPOSE
+   ============================================================ */
+function shuffleBoard() {
+  const wrap = document.getElementById('canvasWrap');
+  const cW   = wrap.clientWidth;
+  const cH   = wrap.clientHeight;
+  const data  = PRESETS[currentPreset];
+
+  // Generate slightly randomized positions within canvas bounds
+  tiles.forEach((t, i) => {
+    const maxX = Math.max(40, cW - 220);
+    const maxY = Math.max(40, cH - 170);
+    const newX = Math.round((Math.random() * maxX) / 10) * 10;
+    const newY = Math.round((Math.random() * maxY) / 10) * 10;
+    t.el.classList.add('animating');
+    setTimeout(() => {
+      t.el.style.left = newX + 'px';
+      t.el.style.top  = newY + 'px';
+      t.x = newX;
+      t.y = newY;
+    }, i * 35);
+  });
+
+  showToast('Board recomposed');
+}
+
+document.getElementById('btnShuffle').addEventListener('click', shuffleBoard);
+document.getElementById('sidebarShuffle').addEventListener('click', shuffleBoard);
+document.getElementById('sidebarReset').addEventListener('click', () => { populateBoard(currentPreset, true); showToast('Layout reset'); });
+
+/* ============================================================
+   CLEAR
+   ============================================================ */
+document.getElementById('btnClear').addEventListener('click', () => {
+  tiles.forEach((t, i) => {
+    t.el.classList.add('animating');
+    setTimeout(() => {
+      t.el.style.opacity = '0';
+      t.el.style.transform = 'scale(0.8)';
+    }, i * 30);
+  });
+  setTimeout(() => {
+    document.getElementById('canvasInner').innerHTML = '';
+    tiles = [];
+    selectedTileId = null;
+    showToast('Board cleared');
+  }, tiles.length * 30 + 400);
+});
+
+/* ============================================================
+   LAYER CONTROLS
+   ============================================================ */
+document.getElementById('sidebarFront').addEventListener('click', () => {
+  if (!selectedTileId) return;
+  zCounter++;
+  const t = tiles.find(t => t.id === selectedTileId);
+  if (t) { t.el.style.zIndex = zCounter; t.z = zCounter; }
+});
+
+document.getElementById('sidebarBack').addEventListener('click', () => {
+  if (!selectedTileId) return;
+  const t = tiles.find(t => t.id === selectedTileId);
+  if (t) { t.el.style.zIndex = 1; t.z = 1; }
+});
+
+document.getElementById('sidebarDelete').addEventListener('click', () => {
+  if (!selectedTileId) return;
+  const idx = tiles.findIndex(t => t.id === selectedTileId);
+  if (idx >= 0) {
+    const t = tiles[idx];
+    t.el.classList.add('animating');
+    t.el.style.opacity = '0';
+    t.el.style.transform = 'scale(0.7)';
+    setTimeout(() => t.el.remove(), 300);
+    tiles.splice(idx, 1);
+    selectedTileId = null;
+    showToast('Tile removed');
+  }
+});
+
+/* ============================================================
+   KEYBOARD
+   ============================================================ */
+document.addEventListener('keydown', (e) => {
+  if (!selectedTileId) return;
+  const t = tiles.find(t => t.id === selectedTileId);
+  if (!t) return;
+  const step = e.shiftKey ? 10 : 2;
+  if (e.key === 'ArrowLeft')  { t.x -= step; t.el.style.left = t.x + 'px'; e.preventDefault(); }
+  if (e.key === 'ArrowRight') { t.x += step; t.el.style.left = t.x + 'px'; e.preventDefault(); }
+  if (e.key === 'ArrowUp')    { t.y -= step; t.el.style.top  = t.y + 'px'; e.preventDefault(); }
+  if (e.key === 'ArrowDown')  { t.y += step; t.el.style.top  = t.y + 'px'; e.preventDefault(); }
+  if (e.key === 'Delete' || e.key === 'Backspace') {
+    document.getElementById('sidebarDelete').click();
+  }
+  if (e.key === 'Escape') {
+    tiles.forEach(t => t.el.classList.remove('selected'));
+    selectedTileId = null;
+  }
+});
+
+/* ============================================================
+   TOAST
+   ============================================================ */
+let toastTimer;
+function showToast(msg) {
+  const toast = document.getElementById('toast');
+  toast.textContent = msg;
+  toast.classList.add('show');
+  clearTimeout(toastTimer);
+  toastTimer = setTimeout(() => toast.classList.remove('show'), 2200);
+}
+
+/* ============================================================
+   EXPORT — render board to canvas
+   ============================================================ */
+document.getElementById('btnExport').addEventListener('click', openExport);
+
+async function openExport() {
+  const btn = document.getElementById('btnExport');
+  btn.classList.add('btn-shimmer');
+  btn.textContent = 'Rendering…';
+
+  await new Promise(r => setTimeout(r, 50));
+
+  const exportCanvas = document.getElementById('exportCanvas');
+  const ctx   = exportCanvas.getContext('2d');
+  const W     = 1080;
+  const H     = 1350;
+  const data  = PRESETS[currentPreset];
+  const wrap  = document.getElementById('canvasWrap');
+  const cW    = wrap.clientWidth;
+  const cH    = wrap.clientHeight;
+  const scaleX = W / cW;
+  const scaleY = H / cH;
+
+  ctx.clearRect(0, 0, W, H);
+
+  // --- Background ---
+  ctx.fillStyle = '#0d0b09';
+  ctx.fillRect(0, 0, W, H);
+
+  // --- Subtle warm texture overlay ---
+  for (let i = 0; i < 3000; i++) {
+    const x  = Math.random() * W;
+    const y  = Math.random() * H;
+    const op = Math.random() * 0.03;
+    ctx.fillStyle = `rgba(201,169,97,${op})`;
+    ctx.fillRect(x, y, 1, 1);
+  }
+
+  // --- Hairline brass frame ---
+  ctx.strokeStyle = 'rgba(201,169,97,0.35)';
+  ctx.lineWidth   = 1;
+  ctx.strokeRect(28, 28, W - 56, H - 56);
+  ctx.strokeStyle = 'rgba(201,169,97,0.15)';
+  ctx.lineWidth   = 1;
+  ctx.strokeRect(36, 36, W - 72, H - 72);
+
+  // --- Grid lines (subtle) ---
+  ctx.strokeStyle = 'rgba(201,169,97,0.07)';
+  ctx.lineWidth   = 1;
+  for (let x = 60; x < W; x += 60) {
+    ctx.beginPath(); ctx.moveTo(x, 60); ctx.lineTo(x, H - 60); ctx.stroke();
+  }
+  for (let y = 60; y < H; y += 60) {
+    ctx.beginPath(); ctx.moveTo(60, y); ctx.lineTo(W - 60, y); ctx.stroke();
+  }
+
+  // --- Render each tile as a colored block with label ---
+  const sortedTiles = [...tiles].sort((a, b) => (a.z || 0) - (b.z || 0));
+
+  for (const t of sortedTiles) {
+    const el  = t.el;
+    const ex  = (t.x * scaleX);
+    const ey  = (t.y * scaleY);
+
+    if (el.classList.contains('swatch-tile')) {
+      const sw = 160 * scaleX;
+      const sh = 160 * scaleY;
+      drawSwatchOnCanvas(ctx, t, ex, ey, sw, sh, data.palette);
+    } else if (el.classList.contains('color-chip')) {
+      const cw = 80 * scaleX;
+      const ch = 110 * scaleY;
+      drawChipOnCanvas(ctx, el, ex, ey, cw, ch);
+    } else if (el.classList.contains('type-tile')) {
+      const tw = 220 * scaleX;
+      const th = 130 * scaleY;
+      drawTypeOnCanvas(ctx, ex, ey, tw, th, data.palette);
+    } else if (el.classList.contains('material-chip')) {
+      const mw = 100 * scaleX;
+      const mh = 56 * scaleY;
+      drawMaterialOnCanvas(ctx, el, ex, ey, mw, mh);
+    }
+  }
+
+  // --- Vignette ---
+  const vig = ctx.createRadialGradient(W/2, H/2, H*0.35, W/2, H/2, H*0.75);
+  vig.addColorStop(0, 'rgba(0,0,0,0)');
+  vig.addColorStop(1, 'rgba(0,0,0,0.55)');
+  ctx.fillStyle = vig;
+  ctx.fillRect(0, 0, W, H);
+
+  // --- Bottom bar ---
+  const barH = 160;
+  const barY = H - barH;
+  const barGrad = ctx.createLinearGradient(0, barY, 0, H);
+  barGrad.addColorStop(0, 'rgba(13,11,9,0)');
+  barGrad.addColorStop(0.3, 'rgba(13,11,9,0.92)');
+  barGrad.addColorStop(1,   'rgba(13,11,9,0.98)');
+  ctx.fillStyle = barGrad;
+  ctx.fillRect(0, barY - 40, W, barH + 40);
+
+  // --- Color palette strip ---
+  const stripY  = H - 108;
+  const stripH  = 10;
+  const stripW  = W - 120;
+  const stripX  = 60;
+  const colW    = stripW / data.palette.length;
+  data.palette.forEach((c, i) => {
+    ctx.fillStyle = c.hex;
+    ctx.fillRect(stripX + i * colW, stripY, colW, stripH);
+  });
+
+  // Palette names
+  ctx.font = `500 ${Math.round(18)}px -apple-system,Helvetica,Arial,sans-serif`;
+  ctx.fillStyle = 'rgba(201,169,97,0.65)';
+  ctx.textAlign = 'left';
+  data.palette.forEach((c, i) => {
+    const cx = stripX + i * colW + colW/2;
+    ctx.save();
+    ctx.font = `500 16px -apple-system,Helvetica,Arial,sans-serif`;
+    ctx.fillStyle = 'rgba(232,227,218,0.55)';
+    ctx.textAlign = 'center';
+    ctx.fillText(c.name.toUpperCase(), cx, stripY + stripH + 24);
+    ctx.restore();
+  });
+
+  // --- Brass separator line ---
+  ctx.strokeStyle = 'rgba(201,169,97,0.50)';
+  ctx.lineWidth   = 1;
+  ctx.beginPath();
+  ctx.moveTo(60, stripY - 18);
+  ctx.lineTo(W - 60, stripY - 18);
+  ctx.stroke();
+
+  // --- Style label ---
+  ctx.font      = `300 22px -apple-system,Helvetica,Arial,sans-serif`;
+  ctx.fillStyle = 'rgba(201,169,97,0.80)';
+  ctx.textAlign = 'center';
+  ctx.fillText(data.label.toUpperCase(), W/2, stripY - 34);
+
+  // --- Wordmark ---
+  const wY = H - 44;
+  ctx.font      = `600 40px Georgia,serif`;
+  ctx.fillStyle = '#e8e3da';
+  ctx.textAlign = 'center';
+  ctx.fillText('DESIGNER WALLCOVERINGS', W/2, wY - 8);
+
+  // Underline
+  ctx.strokeStyle = 'rgba(201,169,97,0.40)';
+  ctx.lineWidth   = 0.8;
+  ctx.beginPath();
+  ctx.moveTo(W/2 - 220, wY - 2);
+  ctx.lineTo(W/2 + 220, wY - 2);
+  ctx.stroke();
+
+  // --- Top label ---
+  ctx.font      = `400 18px -apple-system,Helvetica,Arial,sans-serif`;
+  ctx.fillStyle = 'rgba(201,169,97,0.55)';
+  ctx.textAlign = 'center';
+  ctx.fillText('INTERIOR MOOD BOARD', W/2, 72);
+
+  btn.classList.remove('btn-shimmer');
+  btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 1v8M4 6l3 3 3-3M2 10v2h10v-2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg> Export Board`;
+
+  document.getElementById('exportOverlay').classList.add('open');
+}
+
+/* --- Sub-renderers for canvas export --- */
+function drawSwatchOnCanvas(ctx, t, x, y, w, h, palette) {
+  const swatchType = t.el.querySelector('.tile-label')?.textContent?.toLowerCase().replace(/\s+/g,'') || '';
+
+  if (swatchType.includes('grasscloth')) {
+    const bg = palette[4]?.hex || '#d4c4a4';
+    const fg = palette[5]?.hex || '#b09a7a';
+    ctx.fillStyle = bg;
+    ctx.fillRect(x, y, w, h);
+    ctx.strokeStyle = fg;
+    for (let ly = 0; ly < h; ly += h/40) {
+      ctx.lineWidth = 0.5 + Math.random() * 0.8;
+      ctx.globalAlpha = 0.15 + Math.random() * 0.25;
+      ctx.beginPath();
+      ctx.moveTo(x, y + ly);
+      ctx.lineTo(x + w, y + ly);
+      ctx.stroke();
+    }
+    ctx.globalAlpha = 1;
+  } else if (swatchType.includes('damask')) {
+    const bg = palette[1]?.hex || '#f0e8d8';
+    const fg = palette[0]?.hex || '#c9917e';
+    ctx.fillStyle = bg;
+    ctx.fillRect(x, y, w, h);
+    ctx.fillStyle = fg;
+    ctx.globalAlpha = 0.20;
+    ctx.beginPath();
+    ctx.ellipse(x + w/2, y + h/2, w*0.22, h*0.32, 0, 0, Math.PI*2);
+    ctx.fill();
+    ctx.globalAlpha = 0.12;
+    ctx.beginPath();
+    ctx.ellipse(x + w/2, y + h/2, w*0.12, h*0.18, 0, 0, Math.PI*2);
+    ctx.fill();
+    ctx.globalAlpha = 0.15;
+    ctx.fillRect(x + w/2 - w*0.12, y, w*0.24, h*0.18);
+    ctx.fillRect(x + w/2 - w*0.12, y + h*0.82, w*0.24, h*0.18);
+    ctx.fillRect(x, y + h/2 - h*0.12, w*0.18, h*0.24);
+    ctx.fillRect(x + w*0.82, y + h/2 - h*0.12, w*0.18, h*0.24);
+    ctx.globalAlpha = 1;
+  } else if (swatchType.includes('silk')) {
+    const cols = [palette[0]?.hex||'#c9a961', palette[1]?.hex||'#f0e8d8', palette[2]?.hex||'#d4c4a4'];
+    const relWidths = [0.08, 0.20, 0.06, 0.14, 0.04, 0.20, 0.06, 0.14, 0.08];
+    let cx = x;
+    relWidths.forEach((rw, i) => {
+      ctx.fillStyle = cols[i % cols.length];
+      ctx.globalAlpha = i % 2 === 0 ? 0.9 : 0.6;
+      ctx.fillRect(cx, y, w * rw, h);
+      cx += w * rw;
+    });
+    ctx.globalAlpha = 1;
+  } else if (swatchType.includes('linen')) {
+    const bg = palette[1]?.hex || '#e8dccb';
+    const fg = palette[5]?.hex || '#c4a878';
+    ctx.fillStyle = bg;
+    ctx.fillRect(x, y, w, h);
+    ctx.strokeStyle = fg;
+    ctx.lineWidth = 1;
+    ctx.globalAlpha = 0.20;
+    for (let lx = x; lx < x+w; lx += w/32) {
+      ctx.beginPath(); ctx.moveTo(lx, y); ctx.lineTo(lx, y+h); ctx.stroke();
+    }
+    for (let ly = y; ly < y+h; ly += h/32) {
+      ctx.beginPath(); ctx.moveTo(x, ly); ctx.lineTo(x+w, ly); ctx.stroke();
+    }
+    ctx.globalAlpha = 1;
+  } else if (swatchType.includes('raffia')) {
+    const bg = palette[3]?.hex || '#d9ccb0';
+    const fg = palette[5]?.hex || '#a08060';
+    ctx.fillStyle = bg;
+    ctx.fillRect(x, y, w, h);
+    ctx.strokeStyle = fg;
+    ctx.lineWidth = 1.5;
+    ctx.globalAlpha = 0.22;
+    for (let d = -w; d < w*2; d += w/20) {
+      ctx.beginPath();
+      ctx.moveTo(x + d, y);
+      ctx.lineTo(x + d + h, y + h);
+      ctx.stroke();
+    }
+    ctx.globalAlpha = 1;
+  } else { // chinoiserie
+    const bg = palette[1]?.hex || '#f4f0e0';
+    ctx.fillStyle = bg;
+    ctx.fillRect(x, y, w, h);
+    ctx.strokeStyle = palette[4]?.hex || '#4a7a9e';
+    ctx.lineWidth = 1.5;
+    ctx.globalAlpha = 0.55;
+    ctx.beginPath();
+    ctx.moveTo(x + w*0.3, y + h*0.9);
+    ctx.bezierCurveTo(x+w*0.38, y+h*0.7, x+w*0.4, y+h*0.5, x+w*0.5, y+h*0.12);
+    ctx.stroke();
+    ctx.fillStyle = palette[0]?.hex || '#d4907a';
+    ctx.globalAlpha = 0.65;
+    ctx.beginPath();
+    ctx.arc(x + w*0.5, y + h*0.12, w*0.06, 0, Math.PI*2);
+    ctx.fill();
+    ctx.globalAlpha = 1;
+  }
+
+  // Label strip
+  ctx.fillStyle = 'rgba(0,0,0,0.40)';
+  ctx.fillRect(x, y + h - h*0.16, w, h*0.16);
+  ctx.font = `700 ${Math.round(w * 0.07)}px -apple-system,Helvetica,Arial,sans-serif`;
+  ctx.fillStyle = 'rgba(255,255,255,0.85)';
+  ctx.textAlign = 'left';
+  const labelText = t.el.querySelector('.tile-label')?.textContent || '';
+  ctx.fillText(labelText.toUpperCase(), x + w*0.06, y + h - h*0.04);
+}
+
+function drawChipOnCanvas(ctx, el, x, y, w, h) {
+  const hex  = el.querySelector('.chip-swatch')?.style.background || '#ccc';
+  const name = el.querySelector('.chip-name')?.textContent || '';
+  const hexV = el.querySelector('.chip-hex')?.textContent || '';
+  const swatchH = h * 0.68;
+  const infoH   = h - swatchH;
+
+  ctx.fillStyle = hex;
+  ctx.fillRect(x, y, w, swatchH);
+  ctx.fillStyle = '#fff';
+  ctx.fillRect(x, y + swatchH, w, infoH);
+
+  ctx.font      = `700 ${Math.round(w * 0.11)}px -apple-system,Helvetica,Arial,sans-serif`;
+  ctx.fillStyle = '#1c1a17';
+  ctx.textAlign = 'left';
+  ctx.fillText(name.toUpperCase(), x + w*0.08, y + swatchH + infoH * 0.48);
+
+  ctx.font      = `400 ${Math.round(w * 0.09)}px -apple-system,Helvetica,Arial,sans-serif`;
+  ctx.fillStyle = '#888';
+  ctx.fillText(hexV, x + w*0.08, y + swatchH + infoH * 0.80);
+}
+
+function drawTypeOnCanvas(ctx, x, y, w, h, palette) {
+  const acc = palette[2]?.hex || '#c9a961';
+  ctx.fillStyle = '#ffffff';
+  ctx.fillRect(x, y, w, h);
+  ctx.fillStyle = acc;
+  ctx.fillRect(x, y, 3, h);
+
+  ctx.font      = `600 ${Math.round(h * 0.20)}px Georgia,serif`;
+  ctx.fillStyle = '#1c1a17';
+  ctx.textAlign = 'left';
+  ctx.fillText('Living Room', x + w*0.07, y + h*0.35);
+
+  ctx.font      = `700 ${Math.round(h * 0.083)}px -apple-system,Helvetica,Arial,sans-serif`;
+  ctx.fillStyle = acc;
+  ctx.fillText('CORMORANT + HELVETICA', x + w*0.07, y + h*0.55);
+
+  ctx.font      = `400 ${Math.round(h * 0.083)}px -apple-system,Helvetica,Arial,sans-serif`;
+  ctx.fillStyle = '#888';
+  ctx.fillText('Wallcovering specification', x + w*0.07, y + h*0.73);
+  ctx.fillText('reference for client decks.', x + w*0.07, y + h*0.86);
+}
+
+function drawMaterialOnCanvas(ctx, el, x, y, w, h) {
+  const name = el.querySelector('.mat-label')?.textContent || '';
+  // Gradient based on material
+  let grad;
+  if (name === 'Brass') {
+    grad = ctx.createLinearGradient(x, y, x+w, y+h);
+    grad.addColorStop(0, '#d4b44a');
+    grad.addColorStop(0.5, '#c9a961');
+    grad.addColorStop(1, '#e0c870');
+  } else if (name === 'Marble') {
+    grad = ctx.createLinearGradient(x, y, x+w, y+h);
+    grad.addColorStop(0, '#f0eee8');
+    grad.addColorStop(0.5, '#e8e4dc');
+    grad.addColorStop(1, '#f4f2ea');
+  } else {
+    grad = ctx.createLinearGradient(x, y, x+w, y+h);
+    grad.addColorStop(0, '#d8c8a4');
+    grad.addColorStop(0.5, '#c4b48e');
+    grad.addColorStop(1, '#d0c09a');
+  }
+  ctx.fillStyle = grad;
+  ctx.beginPath();
+  ctx.roundRect(x, y, w, h, 4);
+  ctx.fill();
+
+  const textColor = name === 'Marble' ? '#2a2824' : '#3a2e10';
+  ctx.font      = `700 ${Math.round(h * 0.22)}px -apple-system,Helvetica,Arial,sans-serif`;
+  ctx.fillStyle = textColor;
+  ctx.textAlign = 'center';
+  ctx.fillText(name.toUpperCase(), x + w/2, y + h*0.62);
+}
+
+/* ============================================================
+   EXPORT OVERLAY CONTROLS
+   ============================================================ */
+function closeOverlay() {
+  document.getElementById('exportOverlay').classList.remove('open');
+}
+
+document.getElementById('btnCloseOverlay').addEventListener('click', closeOverlay);
+document.getElementById('btnCloseOverlay2').addEventListener('click', closeOverlay);
+
+document.getElementById('exportOverlay').addEventListener('click', (e) => {
+  if (e.target === document.getElementById('exportOverlay')) closeOverlay();
+});
+
+document.addEventListener('keydown', (e) => {
+  if (e.key === 'Escape') closeOverlay();
+});
+
+document.getElementById('btnDownload').addEventListener('click', () => {
+  const canvas = document.getElementById('exportCanvas');
+  const preset = PRESETS[currentPreset].label.toLowerCase().replace(/\s+/g, '-');
+  const a = document.createElement('a');
+  a.download = `dw-mood-board-${preset}.png`;
+  a.href = canvas.toDataURL('image/png');
+  a.click();
+  showToast('Board downloaded');
+});
+
+/* ============================================================
+   INIT
+   ============================================================ */
+window.addEventListener('load', () => {
+  updateChromeHeight();
+  populateBoard('grandmillennial', true);
+});
+
+// Handle resize
+let resizeTimer;
+window.addEventListener('resize', () => {
+  clearTimeout(resizeTimer);
+  resizeTimer = setTimeout(() => {
+    updateChromeHeight();
+    populateBoard(currentPreset, false);
+  }, 300);
+});
+</script>
+</body>
+</html>

← 52073ae auto-save: 2026-07-25T09:05:11 (2 files) — staging-90210/902  ·  back to Model Arena  ·  Arena sandbox CSP: allow blob: in img-src/media-src (same-or 7962eed →